clarity-theme 0.1.4 → 0.2.0

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
@@ -105,9 +105,9 @@ All documents are also available in 简体中文 as sibling `*.zh-CN.md` files.
105
105
  - **Getting started** — [new project](./docs/getting-started/new-project.md), [manual installation](./docs/getting-started/manual-installation.md), [migrating from blog-v3](./docs/getting-started/migration-from-blog-v3.md)
106
106
  - **Guides** — [configuration](./docs/guides/configuration.md), [content](./docs/guides/content.md), [customization](./docs/guides/customization.md), [integrations](./docs/guides/integrations.md)
107
107
  - **Reference** — [public API](./docs/reference/api.md), [routes and outputs](./docs/reference/routes-and-outputs.md), [compatibility matrix](./docs/reference/compatibility.md)
108
- - **Concepts** — [architecture](./docs/concepts/architecture.md)
109
- - **Maintainers** — [development](./docs/maintainers/development.md), [testing](./docs/maintainers/testing.md), [upstream sync](./docs/maintainers/upstream-sync.md), [patches](./docs/maintainers/patches.md), [publishing](./docs/maintainers/publishing.md), [release checklist](./docs/maintainers/release-checklist.md), [project status](./docs/maintainers/project-status.md), [roadmap](./docs/maintainers/roadmap.md), [documentation rules](./docs/maintainers/documentation.md)
110
- - **Release history** — [CHANGELOG](./CHANGELOG.md); historical records live under [docs/history](./docs/history/)
108
+ - **Concepts** — [architecture](./docs/concepts/architecture.md) — the upstream → sync-manifest → `src/` → Layer modules → consumer pipeline, and the SOURCE / TRANSFORM / CLARITY-ONLY / LEGACY source classification
109
+ - **Maintainers** — [development](./docs/maintainers/development.md), [testing & parity](./docs/maintainers/testing.md), [upstream sync](./docs/maintainers/upstream-sync.md), [transform parity](./docs/maintainers/transform-parity.md), [legacy policy](./docs/maintainers/legacy-policy.md), [patches](./docs/maintainers/patches.md), [publishing](./docs/maintainers/publishing.md), [release checklist](./docs/maintainers/release-checklist.md), [project status](./docs/maintainers/project-status.md), [roadmap](./docs/maintainers/roadmap.md), [documentation rules](./docs/maintainers/documentation.md)
110
+ - **Release history** — [CHANGELOG](./CHANGELOG.md); historical records live under [docs/history](./docs/history/) (frozen narratives — not current specification)
111
111
 
112
112
  ## Development
113
113
 
package/README.zh-CN.md CHANGED
@@ -105,9 +105,9 @@ Clarity 属于应用运行时的一部分(Nuxt 构建与静态生成会直接
105
105
  - **入门** — [创建新项目](./docs/getting-started/new-project.zh-CN.md)、[手动安装](./docs/getting-started/manual-installation.zh-CN.md)、[从 blog-v3 迁移](./docs/getting-started/migration-from-blog-v3.zh-CN.md)
106
106
  - **指南** — [配置](./docs/guides/configuration.zh-CN.md)、[内容](./docs/guides/content.zh-CN.md)、[自定义](./docs/guides/customization.zh-CN.md)、[集成](./docs/guides/integrations.zh-CN.md)
107
107
  - **参考** — [公共 API](./docs/reference/api.zh-CN.md)、[路由与输出](./docs/reference/routes-and-outputs.zh-CN.md)、[兼容性矩阵](./docs/reference/compatibility.zh-CN.md)
108
- - **概念** — [架构](./docs/concepts/architecture.zh-CN.md)
109
- - **维护者** — [开发](./docs/maintainers/development.zh-CN.md)、[测试](./docs/maintainers/testing.zh-CN.md)、[上游同步](./docs/maintainers/upstream-sync.zh-CN.md)、[补丁](./docs/maintainers/patches.zh-CN.md)、[发布](./docs/maintainers/publishing.zh-CN.md)、[发布清单](./docs/maintainers/release-checklist.zh-CN.md)、[项目状态](./docs/maintainers/project-status.zh-CN.md)、[路线图](./docs/maintainers/roadmap.zh-CN.md)、[文档规则](./docs/maintainers/documentation.zh-CN.md)
110
- - **发布历史** — [CHANGELOG](./CHANGELOG.md);历史记录位于 [docs/history](./docs/history/)
108
+ - **概念** — [架构](./docs/concepts/architecture.zh-CN.md)——upstream → sync-manifest → `src/` → Layer 模块 → consumer 管线,以及 SOURCE / TRANSFORM / CLARITY-ONLY / LEGACY 源码分类
109
+ - **维护者** — [开发](./docs/maintainers/development.zh-CN.md)、[测试与 Parity](./docs/maintainers/testing.zh-CN.md)、[上游同步](./docs/maintainers/upstream-sync.zh-CN.md)、[Transform 面治理](./docs/maintainers/transform-parity.zh-CN.md)、[Legacy 政策](./docs/maintainers/legacy-policy.zh-CN.md)、[补丁](./docs/maintainers/patches.zh-CN.md)、[发布](./docs/maintainers/publishing.zh-CN.md)、[发布清单](./docs/maintainers/release-checklist.zh-CN.md)、[项目状态](./docs/maintainers/project-status.zh-CN.md)、[路线图](./docs/maintainers/roadmap.zh-CN.md)、[文档规则](./docs/maintainers/documentation.zh-CN.md)
110
+ - **发布历史** — [CHANGELOG](./CHANGELOG.md);历史记录位于 [docs/history](./docs/history/)(冻结叙事——不是现行规范)
111
111
 
112
112
  ## 开发
113
113
 
package/nuxt.config.ts CHANGED
@@ -38,6 +38,8 @@ export default defineNuxtConfig({
38
38
  // "JetBrains Mono", 思源宋体 "Noto Serif SC"
39
39
  { rel: 'preconnect', href: 'https://fonts.gstatic.cn', crossorigin: '' },
40
40
  { rel: 'stylesheet', href: 'https://fonts.googleapis.cn/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Noto+Serif+SC:wght@200..900&display=swap' },
41
+ // 抖音美好体 "DOUYINSANSBOLD-GB"
42
+ { rel: 'stylesheet', href: 'https://fonts.bytedance.com/dfd/api/v1/css?family=DOUYINSANSBOLD-GB&display=swap' },
41
43
  ],
42
44
  templateParams: {
43
45
  separator: '|',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clarity-theme",
3
3
  "type": "module",
4
- "version": "0.1.4",
4
+ "version": "0.2.0",
5
5
  "description": "Clarity Theme — 从 blog-v3 提取的可复用 Nuxt 4 Layer 博客主题",
6
6
  "license": "MIT",
7
7
  "publishConfig": {
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "main": "./nuxt.config.ts",
38
38
  "files": [
39
+ "!src/generated",
39
40
  "LICENSE",
40
41
  "README.md",
41
42
  "nuxt.config.ts",
@@ -135,10 +136,17 @@
135
136
  "docs:check": "node scripts/check-docs.mjs",
136
137
  "verify": "node scripts/verify-theme.mjs",
137
138
  "test:sync": "node --test tests/sync-upstream.test.mjs",
139
+ "test:docs-governance": "node --test tests/docs-governance.test.mjs",
140
+ "test:config": "node --test tests/config-schema.test.mjs",
141
+ "test:transform-parity": "node --test tests/transform-parity.test.mjs",
138
142
  "test:migration": "node --test tests/migration-skill.test.mjs",
139
143
  "test:contract": "node scripts/test-compatibility.mjs --contract-only",
140
144
  "test:release": "pnpm verify && pnpm test:consumer && pnpm test:compatibility",
141
145
  "test:consumer": "node scripts/test-consumer.mjs",
146
+ "test:file-generate": "node scripts/test-file-generate.mjs",
147
+ "test:runtime-parity": "node scripts/test-runtime-parity.mjs",
148
+ "test:visual-parity": "node scripts/test-visual-parity.mjs",
149
+ "test:parity": "pnpm test:upstream-parity && pnpm test:runtime-parity && pnpm test:visual-parity",
142
150
  "test:compatibility": "node scripts/test-compatibility.mjs",
143
151
  "test:create": "pnpm --dir create-clarity-theme test",
144
152
  "test:create:e2e": "pnpm --dir create-clarity-theme test:e2e",
package/src/config/app.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { NavItem } from '../types/nav'
2
- import type { ClarityPublicConfig, ClarityPublicIntegrationsConfig } from './public'
2
+ import type { ClarityPublicIntegrationsConfig } from './public'
3
3
  import type { ClarityArticleConfig, ClarityAuthor, ClarityFeedConfig, ClarityHeadScript, ClarityStatsConfig } from './schema'
4
4
 
5
5
  export interface NavGroup {
@@ -115,20 +115,3 @@ export type ClarityFlatAppConfigInput<T = ClarityFlatAppConfig> = {
115
115
  ? ClarityFlatAppConfigInput<T[K]>
116
116
  : T[K]
117
117
  }
118
-
119
- export type ClarityAppConfig = ClarityPublicConfig & ClarityUiConfig
120
-
121
- /**
122
- * app/app.config.ts 中 `clarity` 键的覆盖输入类型(0.1.x 兼容):
123
- * UI 配置深度可选(数组整体替换,与 defu 行为一致)。
124
- *
125
- * 站点级字段(site / article / feed / stats / integrations / features / changelog)
126
- * 不属于 app.config.ts,请写入 clarity.config.ts。
127
- */
128
- export type ClarityUiConfigInput<T = ClarityUiConfig> = {
129
- [K in keyof T]?: T[K] extends readonly unknown[]
130
- ? T[K]
131
- : T[K] extends object
132
- ? ClarityUiConfigInput<T[K]>
133
- : T[K]
134
- }
@@ -1,6 +1,6 @@
1
1
  export type { Nav, NavItem } from '../types/nav'
2
- export type { ClarityAppConfig, ClarityUiConfig, ClarityUiConfigInput, NavGroup } from './app'
2
+ export type { ClarityUiConfig, NavGroup } from './app'
3
3
  export { defineClarityConfig } from './define'
4
4
  export type { Arch, FeedEntry, FeedGroup } from './feed'
5
- export type { ClarityPublicConfig, ClarityPublicIntegrationsConfig } from './public'
5
+ export type { ClarityPublicIntegrationsConfig } from './public'
6
6
  export * from './schema'
@@ -1,71 +1,13 @@
1
- import type {
2
- ClarityArticleConfig,
3
- ClarityAuthor,
4
- ClarityChangelogEntry,
5
- ClarityConfig,
6
- ClarityIntegrationsConfig,
7
- ClaritySiteConfig,
8
- } from './schema'
1
+ import type { ClarityIntegrationsConfig } from './schema'
9
2
 
10
3
  /**
11
- * 由 clarity.config.ts 注入 appConfig 的客户端可见配置。
12
- *
13
- * 可见性约定(详见 docs/guides/configuration.md):
14
- * - 只有客户端渲染确实需要的字段进入 appConfig;
4
+ * 客户端可见性约定(详见 docs/guides/configuration.md):
15
5
  * - feed、完整 stats、features 与 site.author.email 等服务端/构建期字段
16
6
  * 改由 Nitro 私有 runtimeConfig 提供(config/server.ts),不进入客户端 bundle;
17
7
  * - 仅构建期使用的字段(integrations.scripts、article.hidePostPrefix、
18
8
  * article.robotsNotIndex)由模块直接消费,同样不进入 appConfig。
19
- * 这些配置允许进入客户端 bundle,不包含任何密钥(secret 只允许 runtimeConfig)。
9
+ * 允许进入客户端 bundle 的配置不包含任何密钥(secret 只允许 runtimeConfig)。
20
10
  */
21
11
 
22
12
  /** appConfig 中暴露的 integrations 子集:scripts 仅构建期注入 <head> */
23
13
  export type ClarityPublicIntegrationsConfig = Omit<ClarityIntegrationsConfig, 'scripts'>
24
-
25
- /** appConfig 中暴露的 site 子集:author.email 仅用于服务端 feed/meta 输出 */
26
- export type ClarityPublicSiteConfig = Omit<ClaritySiteConfig, 'author'> & {
27
- author: Omit<ClarityAuthor, 'email'>
28
- }
29
-
30
- /** appConfig 中暴露的 article 子集:其余字段仅构建期使用 */
31
- export type ClarityPublicArticleConfig = Pick<ClarityArticleConfig, 'categories' | 'order'>
32
-
33
- /**
34
- * appConfig 中暴露的 stats 子集:由 includePaths 派生的展示事实,
35
- * 决定 BlogStats 标签使用「文章字数」还是「总字数」,不透出完整匹配规则。
36
- */
37
- export interface ClarityPublicStatsConfig {
38
- postsOnly: boolean
39
- }
40
-
41
- export interface ClarityPublicConfig {
42
- site: ClarityPublicSiteConfig
43
- article: ClarityPublicArticleConfig
44
- stats: ClarityPublicStatsConfig
45
- integrations: ClarityPublicIntegrationsConfig
46
- changelog: ClarityChangelogEntry[]
47
- }
48
-
49
- export function toPublicClarityConfig(config: ClarityConfig): ClarityPublicConfig {
50
- return {
51
- site: {
52
- ...config.site,
53
- author: {
54
- name: config.site.author.name,
55
- avatar: config.site.author.avatar,
56
- homepage: config.site.author.homepage,
57
- },
58
- },
59
- article: {
60
- categories: config.article.categories,
61
- order: config.article.order,
62
- },
63
- stats: {
64
- postsOnly: config.stats.includePaths.length > 0,
65
- },
66
- integrations: config.integrations.twikoo
67
- ? { twikoo: config.integrations.twikoo }
68
- : {},
69
- changelog: config.changelog,
70
- }
71
- }
@@ -1,3 +1,5 @@
1
+ import { join } from 'node:path'
2
+ import { pathToFileURL } from 'node:url'
1
3
  import { defineNuxtModule } from 'nuxt/kit'
2
4
  import { minifySync } from 'oxc-minify'
3
5
  import handleMirror from './runtime/client'
@@ -14,10 +16,13 @@ export default defineNuxtModule({
14
16
  meta: {
15
17
  name: 'anti-mirror',
16
18
  },
17
- async setup(options, nuxt) {
18
- // 生成的数据模块由 clarity-config 在 setup 阶段写入,静态 import 会在
19
- // 模块加载阶段读到上一次构建的旧值,因此改为 setup 内动态 import。
20
- const { default: blogConfig } = await import('../../generated/blog.config.mjs')
19
+ async setup(_options, nuxt) {
20
+ // 生成的数据模块由 clarity-config(先于本模块注册)在 setup 阶段写入
21
+ // <buildDir>/clarity/;静态相对路径会命中安装包内部位置(穿透 pnpm
22
+ // 硬链接污染 store),改用基于 buildDir 的 file URL 动态导入,jiti 与
23
+ // Node 原生加载均可解析。
24
+ const buildInfoUrl = pathToFileURL(join(nuxt.options.buildDir, 'clarity', 'blog.config.mjs')).href
25
+ const { default: blogConfig } = await import(buildInfoUrl)
21
26
  nuxt.options.app.head.script ??= []
22
27
  nuxt.options.app.head.script.push({
23
28
  innerHTML: toIifeString(handleMirror, blacklist.map(btoa), btoa(blogConfig.url)),
@@ -3,6 +3,7 @@ import type { ClarityConfig } from '../../config/schema'
3
3
  import type { ClarityServerConfig } from '../../config/server'
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
5
5
  import { dirname, join, relative, resolve } from 'node:path'
6
+ import { platform } from 'node:process'
6
7
  import { fileURLToPath } from 'node:url'
7
8
  import { createJiti } from 'jiti'
8
9
  import {
@@ -14,7 +15,6 @@ import {
14
15
  } from 'nuxt/kit'
15
16
  import { parse as parseYaml } from 'yaml'
16
17
  import { fallbackPnpmWorkspace } from '../../config/pnpm-workspace'
17
- import { toPublicClarityConfig } from '../../config/public'
18
18
  import { clarityConfigSchema } from '../../config/schema'
19
19
  import { toServerClarityConfig } from '../../config/server'
20
20
  import uiDefaults from '../../config/ui'
@@ -22,6 +22,8 @@ import uiDefaults from '../../config/ui'
22
22
  const moduleDir = dirname(fileURLToPath(import.meta.url))
23
23
  /** Theme 运行时源码根(src/):模块位于 src/modules/clarity-config */
24
24
  const themeSrcDir = resolve(moduleDir, '../..')
25
+ /** Windows 路径比较需要大小写不敏感(vite 解析产物盘符大小写不定) */
26
+ const IS_WINDOWS = platform === 'win32'
25
27
 
26
28
  /** app/app.config.ts 中允许作为 UI 覆盖的顶层键(站点级字段属于 clarity.config.ts) */
27
29
  const uiConfigKeys = new Set(['component', 'footer', 'header', 'link', 'nav', 'pagination', 'themes'])
@@ -31,6 +33,15 @@ export interface ModuleOptions {
31
33
  configFile?: string
32
34
  }
33
35
 
36
+ /** 构建数据重定向插件的最小结构化形状(vite resolveId 子集) */
37
+ interface RedirectPlugin {
38
+ name: string
39
+ resolveId: {
40
+ order: 'pre'
41
+ handler: (source: string) => { id: string } | undefined
42
+ }
43
+ }
44
+
34
45
  export default defineNuxtModule<ModuleOptions>({
35
46
  meta: {
36
47
  name: 'clarity-config',
@@ -65,7 +76,7 @@ export default defineNuxtModule<ModuleOptions>({
65
76
  // @pinia/nuxt 不会自动扫描 Layer 的 stores 目录,需显式注册
66
77
  addImportsDir(resolve(themeSrcDir, 'stores'))
67
78
 
68
- // ---- AppConfig 类型化:上游扁平键 + 0.1.x 的 clarity 键都获得类型提示 ----
79
+ // ---- AppConfig 类型化:上游扁平键获得类型提示 ----
69
80
  // 注意:类型文件生成于 buildDir/types/ 下,相对路径必须以其为基准计算,
70
81
  // 否则导入解析失败会因 skipLibCheck 静默退化为 any(审计发现 #9)。
71
82
  const appConfigTypeDir = resolve(nuxt.options.buildDir, 'types')
@@ -77,15 +88,13 @@ export default defineNuxtModule<ModuleOptions>({
77
88
  // app/types/index.ts 的 Window.twikoo 等环境声明在 node_modules Layer 中
78
89
  // 不会被 tsconfig include 命中(被 node_modules exclude 过滤),需显式导入
79
90
  `import '${globalsTypePath}'`,
80
- `import type { ClarityAppConfig, ClarityAppConfigArticle, ClarityUiConfigInput } from '${appConfigTypePath}'`,
91
+ `import type { ClarityAppConfigArticle, ClarityUiConfig } from '${appConfigTypePath}'`,
81
92
  '',
82
93
  'declare module \'@nuxt/schema\' {',
83
- ' // 读取侧:clarity 用 unknown 回落到注入值;UI 扁平键补充完整类型,',
94
+ ' // 读取侧:UI 扁平键补充完整类型(0.2.0 起不再提供 clarity 嵌套键),',
84
95
  ' // 使上游组件在 JSON 字面量(空数组 / 宽化字符串)下仍获得正确类型。',
85
96
  ' // article 只声明 categories(Record 索引),order / types 保留 Resolved 字面量。',
86
97
  ' interface CustomAppConfig {',
87
- ' /** @deprecated 0.1.x 兼容键:UI 覆盖请直接使用顶层扁平键(上游形状) */',
88
- ' clarity?: unknown',
89
98
  ' article?: ClarityAppConfigArticle',
90
99
  ' component?: ClarityUiConfig[\'component\']',
91
100
  ' footer?: ClarityUiConfig[\'footer\']',
@@ -94,9 +103,6 @@ export default defineNuxtModule<ModuleOptions>({
94
103
  ' nav?: ClarityUiConfig[\'nav\']',
95
104
  ' themes?: ClarityUiConfig[\'themes\']',
96
105
  ' }',
97
- ' interface AppConfigInput {',
98
- ' clarity?: ClarityAppConfig | ClarityUiConfigInput',
99
- ' }',
100
106
  '}',
101
107
  '',
102
108
  ].join('\n'),
@@ -134,42 +140,59 @@ export default defineNuxtModule<ModuleOptions>({
134
140
  // 以 ESM 导入 JSON/YAML),导致含 BlogTech 的页面(首页)在预渲染时崩溃。
135
141
  // 因此在 buildDir 生成等价 ES 模块,运行时别名与 TS 声明都指向生成物。
136
142
  const consumerPkg = (await loadJson(resolve(rootDir, 'package.json'), jiti)) ?? {}
143
+ const missingPkgFields = ['name', 'version'].filter(field => !consumerPkg[field])
144
+ if (missingPkgFields.length) {
145
+ logger.warn(
146
+ `package.json 缺少 [${missingPkgFields.join(', ')}] 字段,BlogTech 等组件将显示空值`
147
+ + '(数据经生成模块归一化,构建不会因此失败)。',
148
+ )
149
+ }
137
150
  const pnpmWorkspace = loadPnpmWorkspace(rootDir)
138
- // 写入 Theme 包内的固定路径:prepare / build / prerenderer 等多个
139
- // Nuxt 实例可能使用不同 buildDir,包内路径在所有上下文中一致可用。
140
- const buildInfoDir = resolve(themeSrcDir, 'generated')
141
- mkdirSync(buildInfoDir, { recursive: true })
151
+ // 写入消费项目 buildDir(<buildDir>/clarity/):生成物是 consumer 私有
152
+ // 构建数据,写入安装包内部(src/generated)会穿透 pnpm 硬链接污染
153
+ // store——并发构建共享同一 store 条目时互相覆盖、读取方拿到半写状态
154
+ // (repair phase 2 定位的静默故障源)。写入必须幂等重入:模块 setup 时
155
+ // 写一次(供 prepare / jiti 加载的 Layer 模块读取),并在 build:before
156
+ // 再写一次——Nuxt build 会在模块运行后清理 buildDir,把 setup 阶段的
157
+ // 生成物删掉,vite 打包前必须重写。
158
+ const buildInfoDir = resolve(nuxt.options.buildDir, 'clarity')
142
159
  const packageJsonModulePath = join(buildInfoDir, 'package-json.mjs').replaceAll('\\', '/')
143
- writeFileSync(packageJsonModulePath, [
144
- `const packageJson = ${JSON.stringify({
145
- name: String(consumerPkg.name ?? ''),
146
- version: String(consumerPkg.version ?? ''),
147
- packageManager: String(consumerPkg.packageManager ?? ''),
148
- })}`,
149
- 'export default packageJson',
150
- `export const packageManager = ${JSON.stringify(String(consumerPkg.packageManager ?? ''))}`,
151
- `export const version = ${JSON.stringify(String(consumerPkg.version ?? ''))}`,
152
- '',
153
- ].join('\n'))
154
160
  const pnpmWorkspaceModulePath = join(buildInfoDir, 'pnpm-workspace.mjs').replaceAll('\\', '/')
155
- writeFileSync(pnpmWorkspaceModulePath, `export default ${JSON.stringify(pnpmWorkspace)}\n`)
161
+ const blogConfigData = toUpstreamBlogConfig(config)
162
+ const blogConfigModulePath = join(buildInfoDir, 'blog.config.mjs').replaceAll('\\', '/')
163
+ const writeBuildInfoModules = () => {
164
+ mkdirSync(buildInfoDir, { recursive: true })
165
+ writeFileSync(packageJsonModulePath, [
166
+ `const packageJson = ${JSON.stringify({
167
+ name: String(consumerPkg.name ?? ''),
168
+ version: String(consumerPkg.version ?? ''),
169
+ packageManager: String(consumerPkg.packageManager ?? ''),
170
+ })}`,
171
+ 'export default packageJson',
172
+ `export const packageManager = ${JSON.stringify(String(consumerPkg.packageManager ?? ''))}`,
173
+ `export const version = ${JSON.stringify(String(consumerPkg.version ?? ''))}`,
174
+ '',
175
+ ].join('\n'))
176
+ writeFileSync(pnpmWorkspaceModulePath, `export default ${JSON.stringify(pnpmWorkspace)}\n`)
177
+ writeFileSync(blogConfigModulePath, [
178
+ `const blogConfig = ${JSON.stringify(blogConfigData)}`,
179
+ 'export default blogConfig',
180
+ `export const myFeed = ${JSON.stringify(toMyFeedEntry(blogConfigData))}`,
181
+ '',
182
+ ].join('\n'))
183
+ }
184
+ writeBuildInfoModules()
185
+ nuxt.hook('build:before', writeBuildInfoModules)
156
186
  nuxt.options.alias = {
157
187
  '~~/package.json': packageJsonModulePath,
158
188
  '~~/pnpm-workspace.yaml': pnpmWorkspaceModulePath,
159
189
  ...nuxt.options.alias,
160
190
  }
161
191
 
162
- // 上游形状的 blog.config 数据模块:anti-mirror 等 Layer 模块由 Nuxt 以
163
- // jiti 加载,无法解析 #clarity/* 别名(src/blog.config.ts 运行时适配层
164
- // 仅适用于 App / Nitro 构建上下文),因此这里把映射结果内联成真实模块。
165
- const blogConfigData = toUpstreamBlogConfig(config)
166
- const blogConfigModulePath = join(buildInfoDir, 'blog.config.mjs').replaceAll('\\', '/')
167
- writeFileSync(blogConfigModulePath, [
168
- `const blogConfig = ${JSON.stringify(blogConfigData)}`,
169
- 'export default blogConfig',
170
- `export const myFeed = ${JSON.stringify(toMyFeedEntry(blogConfigData))}`,
171
- '',
172
- ].join('\n'))
192
+ // 上游形状的 blog.config 数据模块(<buildDir>/clarity/blog.config.mjs):
193
+ // anti-mirror 等 jiti 加载的 Layer 模块无法解析 #clarity/* 别名
194
+ // (src/blog.config.ts 运行时适配层仅适用于 App / Nitro 构建上下文),
195
+ // 因此把映射结果内联成真实模块,供其以 file URL 动态导入。
173
196
 
174
197
  // 两个说明符的 TS 侧声明(app / server 类型工程的路径重定向目标)
175
198
  const yamlDecl = addTypeTemplate({
@@ -229,14 +252,48 @@ export default defineNuxtModule<ModuleOptions>({
229
252
  nitroConfig.typescript.tsConfig.compilerOptions.paths['~~/pnpm-workspace.yaml'] = [withLeadingDot(yamlDecl.filename)]
230
253
  })
231
254
 
232
- // ---- 消费项目 app/app.config.ts:读取 0.1.x `clarity` 键的 UI 覆盖 ----
255
+ // ---- Vite 打包上下文的重定向契约 ----
256
+ // 上方为 `~~/package.json` / `~~/pnpm-workspace.yaml` 设置的精确别名,
257
+ // 在 dev(JS 版 alias 插件,首个匹配生效)中按预期指向生成模块;
258
+ // 但生产构建的 bundled 环境使用 rolldown 原生 alias 插件,其匹配
259
+ // 语义让更短的 `~~` → rootDir 前缀别名抢先,导入被改写为消费项目的
260
+ // 真实文件路径(consumer package.json 缺 version 时即 MISSING_EXPORT)。
261
+ // 消费项目的真实文件是数据源而非打包目标,这里以显式 resolveId 契约
262
+ // 把两个绝对路径重定向到生成模块:client / SSR 打包语义一致,且不再
263
+ // 依赖跨上下文的隐式别名匹配顺序。
264
+ const rootPackageJsonId = resolve(rootDir, 'package.json').replaceAll('\\', '/')
265
+ const rootWorkspaceYamlId = resolve(rootDir, 'pnpm-workspace.yaml').replaceAll('\\', '/')
266
+ const idEquals = (a: string, b: string) => IS_WINDOWS
267
+ ? a.toLowerCase() === b.toLowerCase()
268
+ : a === b
269
+ nuxt.hook('vite:extendConfig', (config) => {
270
+ // InlineConfig.plugins 是 readonly 声明,但 hook 语义允许就地追加;
271
+ // 插件形状用本地结构化类型,避免对 vite 类型的直接依赖
272
+ const plugins = (config as { plugins: Array<RedirectPlugin> }).plugins ??= []
273
+ plugins.unshift({
274
+ name: 'clarity:build-data-redirect',
275
+ resolveId: {
276
+ order: 'pre',
277
+ handler(source) {
278
+ if (idEquals(source, rootPackageJsonId)) {
279
+ return { id: packageJsonModulePath }
280
+ }
281
+ if (idEquals(source, rootWorkspaceYamlId)) {
282
+ return { id: pnpmWorkspaceModulePath }
283
+ }
284
+ return undefined
285
+ },
286
+ },
287
+ })
288
+ })
289
+
290
+ // ---- 消费项目 app/app.config.ts:站点级字段的迁移警告 ----
233
291
  const consumerAppConfig = await loadConsumerAppConfig(nuxt, jiti)
234
- const clarityUi = pickUiOverrides(consumerAppConfig?.clarity)
235
292
  await warnSiteLevelAppConfigOverrides(consumerAppConfig, logger)
236
293
 
237
294
  // ---- 上游形状的扁平 AppConfig 注入(upstream parity 的关键)----
238
295
  // blog-v3 组件一律通过 useAppConfig() 读取扁平键(title / nav / component.*)。
239
- // 这里将 clarity.config.ts + UI 默认值 + 0.1.x clarity 覆盖注入为同一形状;
296
+ // 这里将 clarity.config.ts + UI 默认值注入为同一形状;
240
297
  // 消费项目仍可在自己的 app.config.ts 中按上游习惯直接覆盖任意扁平键。
241
298
  updateAppConfig({
242
299
  // 站点数据(上游来自 blog.config.ts 展开进 app.config.ts)
@@ -269,34 +326,16 @@ export default defineNuxtModule<ModuleOptions>({
269
326
  stats: config.stats,
270
327
  twikoo: integrations.twikoo ?? { envId: '', preload: '' },
271
328
 
272
- // UI 配置:站点派生值覆盖结构默认值,0.1.x clarity 覆盖优先级最高
273
- ...mergeUiConfig(clarityUi, {
274
- ...uiDefaults,
275
- header: {
276
- ...uiDefaults.header,
277
- logo: site.author.avatar ?? site.favicon,
278
- subtitle: site.subtitle ?? site.description,
279
- },
280
- footer: {
281
- ...uiDefaults.footer,
282
- copyright: `© ${new Date().getFullYear()} ${site.author.name}`,
283
- },
284
- }),
285
- })
286
-
287
- // ---- 0.1.x 兼容:继续注入完整的 clarity 键(useClarityConfig() 读取侧)----
288
- updateAppConfig({
289
- clarity: {
290
- ...toPublicClarityConfig(config),
291
- header: {
292
- logo: site.author.avatar ?? '',
293
- subtitle: site.subtitle ?? '',
294
- },
295
- footer: {
296
- copyright: site.copyright?.name
297
- ? `© ${new Date().getFullYear()} ${site.author.name} · ${site.copyright.name}`
298
- : `© ${new Date().getFullYear()} ${site.author.name}`,
299
- },
329
+ // UI 配置:站点派生值覆盖结构默认值
330
+ ...uiDefaults,
331
+ header: {
332
+ ...uiDefaults.header,
333
+ logo: site.author.avatar ?? site.favicon,
334
+ subtitle: site.subtitle ?? site.description,
335
+ },
336
+ footer: {
337
+ ...uiDefaults.footer,
338
+ copyright: `© ${new Date().getFullYear()} ${site.author.name}`,
300
339
  },
301
340
  })
302
341
 
@@ -438,16 +477,7 @@ async function warnSiteLevelAppConfigOverrides(appConfig: Record<string, unknown
438
477
  if (!appConfig) {
439
478
  return
440
479
  }
441
- const offenders = Object.keys(appConfig).filter(key => !uiConfigKeys.has(key) && key !== 'clarity')
442
- if (appConfig.clarity && typeof appConfig.clarity === 'object') {
443
- const clarityOffenders = Object.keys(appConfig.clarity as Record<string, unknown>).filter(key => !uiConfigKeys.has(key))
444
- if (clarityOffenders.length) {
445
- logger.warn(
446
- `app/app.config.ts 的 clarity 中出现站点级字段 [${clarityOffenders.join(', ')}],`
447
- + '请将站点配置迁移到 clarity.config.ts。',
448
- )
449
- }
450
- }
480
+ const offenders = Object.keys(appConfig).filter(key => !uiConfigKeys.has(key))
451
481
  if (offenders.length) {
452
482
  logger.warn(
453
483
  `app/app.config.ts 中出现站点级字段 [${offenders.join(', ')}],`
@@ -456,33 +486,6 @@ async function warnSiteLevelAppConfigOverrides(appConfig: Record<string, unknown
456
486
  }
457
487
  }
458
488
 
459
- /** 深度合并 UI 覆盖:对象递归合并、数组整体替换(覆盖语义) */
460
- function mergeUiConfig(overrides: Record<string, unknown> | undefined, base: Record<string, unknown>) {
461
- if (!overrides) {
462
- return base
463
- }
464
- const result: Record<string, unknown> = { ...base }
465
- for (const [key, value] of Object.entries(overrides)) {
466
- const baseValue = result[key]
467
- result[key]
468
- = value && baseValue && typeof value === 'object' && typeof baseValue === 'object'
469
- && !Array.isArray(value) && !Array.isArray(baseValue)
470
- ? mergeUiConfig(value as Record<string, unknown>, baseValue as Record<string, unknown>)
471
- : value
472
- }
473
- return result
474
- }
475
-
476
- function pickUiOverrides(clarity: unknown) {
477
- if (!clarity || typeof clarity !== 'object') {
478
- return undefined
479
- }
480
- const picked = Object.fromEntries(
481
- Object.entries(clarity as Record<string, unknown>).filter(([key]) => uiConfigKeys.has(key)),
482
- )
483
- return Object.keys(picked).length ? picked : undefined
484
- }
485
-
486
489
  /** clarity.config → 上游 blog.config 扁平形状(与 src/blog.config.ts 适配层保持一致) */
487
490
  function toUpstreamBlogConfig(config: ClarityConfig) {
488
491
  const { site, article, feed, stats, integrations } = config
@@ -4,7 +4,7 @@ import type { ClarityServerConfig } from '../../config/server'
4
4
  * 获取服务端专用 Clarity 配置(完整 site/feed/stats 与 feature 路由开关)。
5
5
  *
6
6
  * 数据来自 Nitro 私有 runtimeConfig,不会进入客户端 bundle;
7
- * 请只在 server/ 侧调用,客户端渲染请使用 useClarityConfig()。
7
+ * 请只在 server/ 侧调用;客户端渲染读取扁平 app config(useAppConfig())。
8
8
  */
9
9
  export function useClarityServerConfig(): ClarityServerConfig {
10
10
  return useRuntimeConfig().clarity as ClarityServerConfig
@@ -1,3 +0,0 @@
1
- const blogConfig = {"title":"Clarity Playground","subtitle":"Clarity Theme 示例站点","description":"Clarity Theme 的最小可运行示例,用于验证 Layer 独立构建与站点配置注入。","author":{"name":"Clarity 用户","avatar":"","email":"user@example.com","homepage":"https://clarity-theme.example.com/"},"copyright":{"abbr":"CC BY 4.0","name":"署名 4.0 国际","url":"https://creativecommons.org/licenses/by/4.0/deed.zh-hans"},"favicon":"/favicon.svg","language":"zh-CN","timeEstablished":"2026-01-01","timeZone":"Asia/Shanghai","url":"http://localhost:22098/","defaultCategory":"未分类","article":{"categories":{"未分类":{"icon":"tabler:circle-dashed"},"技术":{"icon":"tabler:code","color":"#7777ff"},"生活":{"icon":"tabler:leaf","color":"#ff7777"}},"types":{"tech":{},"story":{}},"order":{"date":"创建日期","updated":"更新日期"},"useRandomPremalat":false,"hidePostPrefix":true,"robotsNotIndex":[]},"feed":{"limit":20,"enableStyle":true},"scripts":[],"stats":{"includePaths":["posts/%"]},"twikoo":{"envId":"","preload":""}}
2
- export default blogConfig
3
- export const myFeed = {"author":"Clarity 用户","sitenick":"Clarity Playground","title":"Clarity Playground","desc":"Clarity Theme 示例站点","link":"http://localhost:22098/","feed":"http://localhost:22098/atom.xml","icon":"/favicon.svg","avatar":"","archs":["Nuxt"],"date":"2026-01-01","comment":"这是我自己"}
@@ -1,4 +0,0 @@
1
- const packageJson = {"name":"clarity-theme-playground","version":"0.0.0","packageManager":"pnpm@12.4.1"}
2
- export default packageJson
3
- export const packageManager = "pnpm@12.4.1"
4
- export const version = "0.0.0"
@@ -1 +0,0 @@
1
- export default {"catalogs":{"content":{"@nuxt/content":"^3.16.0"},"framework":{"nuxt":"^4.5.2","vue":"^3.5.42"}}}
@@ -1,34 +0,0 @@
1
- import type { ClarityAppConfig } from '../../config/app'
2
- import type { FeedEntry } from '../../config/feed'
3
- import type { ClaritySiteConfig } from '../../config/schema'
4
-
5
- /** 获取完整 Clarity 配置(站点配置 + UI 配置) */
6
- export function useClarityConfig(): ClarityAppConfig {
7
- return useAppConfig().clarity as unknown as ClarityAppConfig
8
- }
9
-
10
- /** 获取站点配置(标题、作者、URL 等) */
11
- export function useClaritySite(): ClaritySiteConfig {
12
- return useClarityConfig().site
13
- }
14
-
15
- /** 获取文章配置的客户端子集(分类图标/颜色、排序方式) */
16
- export function useClarityArticle(): ClarityAppConfig['article'] {
17
- return useClarityConfig().article
18
- }
19
-
20
- /** 由站点配置生成本站订阅源条目(用于友链页与 OPML) */
21
- export function useClaritySiteFeedEntry(): FeedEntry {
22
- const site = useClaritySite()
23
- return {
24
- author: site.author.name,
25
- title: site.title,
26
- desc: site.subtitle || site.description,
27
- link: site.url,
28
- feed: new URL('/atom.xml', site.url).toString(),
29
- icon: site.favicon,
30
- avatar: site.author.avatar || site.favicon,
31
- date: site.established || '',
32
- comment: '这是我自己',
33
- }
34
- }