clarity-theme 0.1.2 → 0.1.4

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.
Files changed (64) hide show
  1. package/README.md +55 -271
  2. package/README.zh-CN.md +56 -290
  3. package/nuxt.config.ts +17 -1
  4. package/package.json +10 -2
  5. package/src/app.config.ts +8 -104
  6. package/src/assets/icons/zhilu.svg +8 -0
  7. package/src/blog.config.ts +71 -0
  8. package/src/components/blog/BlogFooter.vue +3 -3
  9. package/src/components/blog/BlogHeader.global.vue +9 -9
  10. package/src/components/blog/BlogSidebar.vue +3 -3
  11. package/src/components/blog/ThemeToggle.vue +2 -2
  12. package/src/components/content/Alert.vue +2 -2
  13. package/src/components/content/FeedCard.vue +2 -2
  14. package/src/components/content/Mermaid.vue +1 -1
  15. package/src/components/content/ProseCode.vue +4 -32
  16. package/src/components/content/ProsePre.vue +2 -2
  17. package/src/components/content/ProseTable.vue +1 -1
  18. package/src/components/popover/Search.vue +3 -3
  19. package/src/components/post/Comment.vue +3 -15
  20. package/src/components/post/Excerpt.vue +3 -3
  21. package/src/components/post/OrderToggle.vue +3 -3
  22. package/src/components/post/PostFooter.vue +3 -4
  23. package/src/components/post/PostHeader.vue +4 -4
  24. package/src/components/post/PostSurround.vue +2 -2
  25. package/src/components/post/Slide.vue +2 -2
  26. package/src/components/util/Img.vue +1 -1
  27. package/src/components/widget/BlogLog.vue +9 -19
  28. package/src/components/widget/BlogStats.vue +4 -4
  29. package/src/components/widget/BlogTech.vue +18 -17
  30. package/src/components/widget/CommGroup.vue +43 -0
  31. package/src/composables/useArticle.ts +9 -9
  32. package/src/composables/usePagination.ts +2 -2
  33. package/src/composables/useWidgets.ts +2 -0
  34. package/src/config/app.ts +50 -4
  35. package/src/config/content.ts +6 -0
  36. package/src/config/define.mjs +1 -1
  37. package/src/config/define.ts +1 -1
  38. package/src/config/feed.ts +2 -42
  39. package/src/config/pnpm-workspace.ts +19 -0
  40. package/src/config/public.ts +1 -1
  41. package/src/config/ui.ts +111 -0
  42. package/src/generated/blog.config.mjs +3 -0
  43. package/src/generated/package-json.mjs +4 -0
  44. package/src/generated/pnpm-workspace.mjs +1 -0
  45. package/src/modules/anti-mirror/index.ts +33 -0
  46. package/src/modules/{clarity-config/anti-mirror-client.ts → anti-mirror/runtime/client.ts} +3 -3
  47. package/src/modules/clarity-config/index.ts +335 -77
  48. package/src/modules/clarity-source-layout/index.ts +13 -0
  49. package/src/pages/archive.vue +4 -4
  50. package/src/pages/index.vue +4 -3
  51. package/src/pages/link.vue +5 -5
  52. package/src/pages/preview.vue +2 -2
  53. package/src/server/api/stats.get.ts +9 -19
  54. package/src/server/routes/atom.xml.get.ts +48 -50
  55. package/src/server/routes/subscriptions.opml.get.ts +14 -31
  56. package/src/shared/utils/time.ts +13 -18
  57. package/src/types/article.ts +2 -2
  58. package/src/types/feed.ts +35 -1
  59. package/src/types/upstream-compat.d.ts +17 -0
  60. package/src/types/yaml-modules.d.ts +26 -0
  61. package/src/shared/types/content.ts +0 -30
  62. package/src/types/modal.ts +0 -16
  63. /package/src/assets/icons/{solar-rewind-back-bold-duotone.svg → SolarRewindBackBoldDuotone.svg} +0 -0
  64. /package/src/assets/icons/{solar-rewind-forward-bold-duotone.svg → SolarRewindForwardBoldDuotone.svg} +0 -0
@@ -1,8 +1,8 @@
1
1
  import type { Nuxt } from '@nuxt/schema'
2
2
  import type { ClarityConfig } from '../../config/schema'
3
3
  import type { ClarityServerConfig } from '../../config/server'
4
- import { existsSync } from 'node:fs'
5
- import { dirname, relative, resolve } from 'node:path'
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
5
+ import { dirname, join, relative, resolve } from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
7
7
  import { createJiti } from 'jiti'
8
8
  import {
@@ -12,21 +12,18 @@ import {
12
12
  updateAppConfig,
13
13
  useLogger,
14
14
  } from 'nuxt/kit'
15
- import nuxtPkg from 'nuxt/package.json'
16
- import { minify } from 'oxc-minify'
17
- import vuePkg from 'vue/package.json'
15
+ import { parse as parseYaml } from 'yaml'
16
+ import { fallbackPnpmWorkspace } from '../../config/pnpm-workspace'
18
17
  import { toPublicClarityConfig } from '../../config/public'
19
18
  import { clarityConfigSchema } from '../../config/schema'
20
19
  import { toServerClarityConfig } from '../../config/server'
21
- import handleMirror from './anti-mirror-client'
20
+ import uiDefaults from '../../config/ui'
22
21
 
23
22
  const moduleDir = dirname(fileURLToPath(import.meta.url))
24
23
  /** Theme 运行时源码根(src/):模块位于 src/modules/clarity-config */
25
24
  const themeSrcDir = resolve(moduleDir, '../..')
26
- /** Theme npm 包根(nuxt.config.ts / package.json 所在层) */
27
- const themePkgDir = resolve(moduleDir, '../../..')
28
25
 
29
- /** app/app.config.ts 的 clarity 键只允许 UI 覆盖(站点级字段属于 clarity.config.ts) */
26
+ /** app/app.config.ts 中允许作为 UI 覆盖的顶层键(站点级字段属于 clarity.config.ts) */
30
27
  const uiConfigKeys = new Set(['component', 'footer', 'header', 'link', 'nav', 'pagination', 'themes'])
31
28
 
32
29
  export interface ModuleOptions {
@@ -46,17 +43,29 @@ export default defineNuxtModule<ModuleOptions>({
46
43
 
47
44
  const configPath = resolve(rootDir, options.configFile ?? findConfigFile(rootDir))
48
45
  const feedsPath = findFeedsFile(rootDir, themeSrcDir)
46
+ const consumerBlogConfigPath = findExisting(rootDir, ['blog.config.ts', 'blog.config.mjs', 'blog.config.js'])
47
+ const consumerSrcFeedsPath = findExisting(nuxt.options.srcDir, ['feeds.ts', 'feeds.mjs', 'feeds.js'])
49
48
 
50
49
  // ---- 依赖注入层:Theme 内部不感知消费项目的文件布局 ----
51
- Object.assign(nuxt.options.alias, {
50
+ // 上游文件通过 `~~/blog.config` / `~/feeds` 读取站点数据;消费项目使用
51
+ // clarity.config.ts(或根目录 feeds.ts),这里以前缀更长的别名承接,
52
+ // 使上游同步文件无需改写即可在 Layer 中解析。
53
+ // 新键必须置于 '~' / '~~' 之前,保证 Vite / Nitro 前缀匹配优先命中。
54
+ nuxt.options.alias = {
55
+ ...(consumerBlogConfigPath ? {} : { '~~/blog.config': resolve(themeSrcDir, 'blog.config.ts') }),
56
+ // 上游 server 路由通过 ~~/shared/utils/time 引用工具;消费项目没有
57
+ // shared 目录时指向 Theme 内置实现。
58
+ ...(existsSync(resolve(rootDir, 'shared')) ? {} : { '~~/shared': resolve(themeSrcDir, 'shared') }),
59
+ ...(consumerSrcFeedsPath ? {} : { '~/feeds': feedsPath.resolved }),
52
60
  '#clarity/config': configPath,
53
61
  '#clarity/feeds': feedsPath.resolved,
54
- })
62
+ ...nuxt.options.alias,
63
+ }
55
64
 
56
65
  // @pinia/nuxt 不会自动扫描 Layer 的 stores 目录,需显式注册
57
66
  addImportsDir(resolve(themeSrcDir, 'stores'))
58
67
 
59
- // ---- AppConfig 类型化:消费者 defineAppConfig({ clarity: ... }) 获得完整类型提示 ----
68
+ // ---- AppConfig 类型化:上游扁平键 + 0.1.x 的 clarity 键都获得类型提示 ----
60
69
  // 注意:类型文件生成于 buildDir/types/ 下,相对路径必须以其为基准计算,
61
70
  // 否则导入解析失败会因 skipLibCheck 静默退化为 any(审计发现 #9)。
62
71
  const appConfigTypeDir = resolve(nuxt.options.buildDir, 'types')
@@ -68,14 +77,23 @@ export default defineNuxtModule<ModuleOptions>({
68
77
  // app/types/index.ts 的 Window.twikoo 等环境声明在 node_modules Layer 中
69
78
  // 不会被 tsconfig include 命中(被 node_modules exclude 过滤),需显式导入
70
79
  `import '${globalsTypePath}'`,
71
- `import type { ClarityAppConfig, ClarityUiConfigInput } from '${appConfigTypePath}'`,
80
+ `import type { ClarityAppConfig, ClarityAppConfigArticle, ClarityUiConfigInput } from '${appConfigTypePath}'`,
72
81
  '',
73
82
  'declare module \'@nuxt/schema\' {',
74
- ' // 读取侧:unknown 使 MergedAppConfig 回落到 Theme 注入的完整 Resolved 类型',
83
+ ' // 读取侧:clarity 用 unknown 回落到注入值;UI 扁平键补充完整类型,',
84
+ ' // 使上游组件在 JSON 字面量(空数组 / 宽化字符串)下仍获得正确类型。',
85
+ ' // article 只声明 categories(Record 索引),order / types 保留 Resolved 字面量。',
75
86
  ' interface CustomAppConfig {',
87
+ ' /** @deprecated 0.1.x 兼容键:UI 覆盖请直接使用顶层扁平键(上游形状) */',
76
88
  ' clarity?: unknown',
89
+ ' article?: ClarityAppConfigArticle',
90
+ ' component?: ClarityUiConfig[\'component\']',
91
+ ' footer?: ClarityUiConfig[\'footer\']',
92
+ ' header?: ClarityUiConfig[\'header\']',
93
+ ' link?: ClarityUiConfig[\'link\']',
94
+ ' nav?: ClarityUiConfig[\'nav\']',
95
+ ' themes?: ClarityUiConfig[\'themes\']',
77
96
  ' }',
78
- ' // 输入侧:完整配置(Theme 内部)或 UI 部分覆盖(消费项目 app/app.config.ts)',
79
97
  ' interface AppConfigInput {',
80
98
  ' clarity?: ClarityAppConfig | ClarityUiConfigInput',
81
99
  ' }',
@@ -110,13 +128,163 @@ export default defineNuxtModule<ModuleOptions>({
110
128
  const config = parseClarityConfig((configModule as { default?: unknown })?.default ?? configModule, configPath)
111
129
  const { site, article, integrations, features } = config
112
130
 
113
- // ---- 边界审计:app/app.config.ts 中不应出现站点级字段(其优先级更高,会覆盖站点配置注入) ----
114
- await warnNonUiAppConfigOverrides(nuxt, jiti, logger)
131
+ // ---- 上游构建期数据模块:~~/package.json 与 ~~/pnpm-workspace.yaml ----
132
+ // 上游 BlogTech / atom.xml 通过这两个说明符读取消费项目数据;直接把别名
133
+ // 指到真实 JSON/YAML 文件时,Nitro prerenderer 会将其外部化(Node 无法
134
+ // 以 ESM 导入 JSON/YAML),导致含 BlogTech 的页面(首页)在预渲染时崩溃。
135
+ // 因此在 buildDir 生成等价 ES 模块,运行时别名与 TS 声明都指向生成物。
136
+ const consumerPkg = (await loadJson(resolve(rootDir, 'package.json'), jiti)) ?? {}
137
+ const pnpmWorkspace = loadPnpmWorkspace(rootDir)
138
+ // 写入 Theme 包内的固定路径:prepare / build / prerenderer 等多个
139
+ // Nuxt 实例可能使用不同 buildDir,包内路径在所有上下文中一致可用。
140
+ const buildInfoDir = resolve(themeSrcDir, 'generated')
141
+ mkdirSync(buildInfoDir, { recursive: true })
142
+ 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
+ const pnpmWorkspaceModulePath = join(buildInfoDir, 'pnpm-workspace.mjs').replaceAll('\\', '/')
155
+ writeFileSync(pnpmWorkspaceModulePath, `export default ${JSON.stringify(pnpmWorkspace)}\n`)
156
+ nuxt.options.alias = {
157
+ '~~/package.json': packageJsonModulePath,
158
+ '~~/pnpm-workspace.yaml': pnpmWorkspaceModulePath,
159
+ ...nuxt.options.alias,
160
+ }
161
+
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'))
173
+
174
+ // 两个说明符的 TS 侧声明(app / server 类型工程的路径重定向目标)
175
+ const yamlDecl = addTypeTemplate({
176
+ filename: 'types/clarity-yaml-workspace.d.ts',
177
+ getContents: () => [
178
+ 'declare const value: Record<string, unknown>',
179
+ 'export default value',
180
+ '',
181
+ ].join('\n'),
182
+ })
183
+ const packageJsonDecl = addTypeTemplate({
184
+ filename: 'types/clarity-package-json.d.ts',
185
+ getContents: () => [
186
+ 'declare const value: { name: string, version: string, packageManager: string }',
187
+ 'export default value',
188
+ 'export const packageManager: string',
189
+ 'export const version: string',
190
+ '',
191
+ ].join('\n'),
192
+ })
193
+
194
+ // 上游类型兼容垫片(post.author 可选字段、*.yaml 导入声明)
195
+ // 需要在 app / shared / server 三个类型工程中同样生效。
196
+ const compatTypesPath = resolve(themeSrcDir, 'types/upstream-compat.d.ts')
197
+ const withLeadingDot = (path: string) => path.startsWith('.') ? path : `./${path}`
198
+ nuxt.hook('prepare:types', ({ references, sharedReferences, nodeReferences, tsConfig }) => {
199
+ references.push({ path: compatTypesPath })
200
+ sharedReferences.push({ path: compatTypesPath })
201
+ nodeReferences.push({ path: compatTypesPath })
202
+ // TS 的别名路径替换会去掉 .json/.yaml 扩展名导致解析失败,
203
+ // 重定向到上方声明模块。
204
+ if (tsConfig.compilerOptions?.paths) {
205
+ const packageJsonDeclPath = relative(nuxt.options.buildDir, resolve(nuxt.options.buildDir, packageJsonDecl.filename)).replaceAll('\\', '/')
206
+ tsConfig.compilerOptions.paths['~~/package.json'] = [withLeadingDot(packageJsonDeclPath)]
207
+ const yamlDeclPath = relative(nuxt.options.buildDir, resolve(nuxt.options.buildDir, yamlDecl.filename)).replaceAll('\\', '/')
208
+ tsConfig.compilerOptions.paths['~~/pnpm-workspace.yaml'] = [withLeadingDot(yamlDeclPath)]
209
+ }
210
+ })
211
+ nuxt.hook('nitro:prepare:types', ({ declarations }) => {
212
+ declarations.push(`/// <reference types="${compatTypesPath.replaceAll('\\', '/')}" />`)
213
+ })
214
+
215
+ // prerenderer 是独立的 Nitro 子构建(nitro:config 不会对其生效),
216
+ // 生成模块在其中必须内联:外部化为绝对 Windows 路径后 Node 无法
217
+ // 以 ESM 导入,含 BlogTech 的首页会在预渲染时挂起。
218
+ ;(nuxt.hook as unknown as (event: string, cb: (config: Record<string, any>) => void) => void)('prerender:config', (prerendererConfig) => {
219
+ prerendererConfig.externals ??= {}
220
+ prerendererConfig.externals.inline ??= []
221
+ prerendererConfig.externals.inline.push(packageJsonModulePath, pnpmWorkspaceModulePath)
222
+ })
223
+ nuxt.hook('nitro:config', (nitroConfig) => {
224
+ nitroConfig.typescript ??= {}
225
+ nitroConfig.typescript.tsConfig ??= {}
226
+ nitroConfig.typescript.tsConfig.compilerOptions ??= {}
227
+ nitroConfig.typescript.tsConfig.compilerOptions.paths ??= {}
228
+ nitroConfig.typescript.tsConfig.compilerOptions.paths['~~/package.json'] = [withLeadingDot(packageJsonDecl.filename)]
229
+ nitroConfig.typescript.tsConfig.compilerOptions.paths['~~/pnpm-workspace.yaml'] = [withLeadingDot(yamlDecl.filename)]
230
+ })
231
+
232
+ // ---- 消费项目 app/app.config.ts:读取 0.1.x `clarity` 键的 UI 覆盖 ----
233
+ const consumerAppConfig = await loadConsumerAppConfig(nuxt, jiti)
234
+ const clarityUi = pickUiOverrides(consumerAppConfig?.clarity)
235
+ await warnSiteLevelAppConfigOverrides(consumerAppConfig, logger)
236
+
237
+ // ---- 上游形状的扁平 AppConfig 注入(upstream parity 的关键)----
238
+ // blog-v3 组件一律通过 useAppConfig() 读取扁平键(title / nav / component.*)。
239
+ // 这里将 clarity.config.ts + UI 默认值 + 0.1.x clarity 覆盖注入为同一形状;
240
+ // 消费项目仍可在自己的 app.config.ts 中按上游习惯直接覆盖任意扁平键。
241
+ updateAppConfig({
242
+ // 站点数据(上游来自 blog.config.ts 展开进 app.config.ts)
243
+ title: site.title,
244
+ subtitle: site.subtitle ?? '',
245
+ description: site.description,
246
+ author: {
247
+ name: site.author.name,
248
+ avatar: site.author.avatar ?? '',
249
+ email: site.author.email ?? '',
250
+ homepage: site.author.homepage ?? '/',
251
+ },
252
+ copyright: site.copyright ?? { abbr: '', name: '', url: '' },
253
+ favicon: site.favicon,
254
+ language: site.language,
255
+ timeEstablished: site.established ?? '',
256
+ timeZone: site.timezone,
257
+ url: site.url,
258
+ defaultCategory: article.defaultCategory,
259
+ article: {
260
+ categories: article.categories,
261
+ types: article.types,
262
+ order: article.order,
263
+ useRandomPremalat: false,
264
+ hidePostPrefix: article.hidePostPrefix,
265
+ robotsNotIndex: article.robotsNotIndex,
266
+ },
267
+ feed: config.feed,
268
+ scripts: integrations.scripts,
269
+ stats: config.stats,
270
+ twikoo: integrations.twikoo ?? { envId: '', preload: '' },
271
+
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
+ })
115
286
 
116
- // ---- Site Config → App Config 桥梁 ----
117
- // 注意 Nuxt appConfig 合并优先级:消费项目 app.config > Theme app.config > 模块注入,
118
- // 因此站点派生默认值(header.logo 等)不放入 Theme 的 app.config.ts,
119
- // 由这里注入后仍可被消费项目覆盖。
287
+ // ---- 0.1.x 兼容:继续注入完整的 clarity 键(useClarityConfig() 读取侧)----
120
288
  updateAppConfig({
121
289
  clarity: {
122
290
  ...toPublicClarityConfig(config),
@@ -133,9 +301,8 @@ export default defineNuxtModule<ModuleOptions>({
133
301
  })
134
302
 
135
303
  // ---- 服务端专用配置:注入 Nitro 私有 runtimeConfig(不进入客户端 bundle)----
136
- // Atom/OPML/stats 处理器与 feature 路由守卫从这里读取完整配置。
137
- // 注意:Nuxt 会按消费者实际值推导 runtimeConfig 类型(可选字段可能被推断为必填),
138
- // 此处以 Theme 声明的 ClarityServerConfig 为准,避免与消费者无关的推断形状冲突。
304
+ // 保留 0.1.x 的 useClarityServerConfig() 兼容;上游 server 路由已恢复为
305
+ // 直接读取 `~~/blog.config`,不再依赖该通道。
139
306
  ;(nuxt.options.runtimeConfig as { clarity?: ClarityServerConfig }).clarity = toServerClarityConfig(config)
140
307
 
141
308
  // ---- SEO / site / robots / llms ----
@@ -177,19 +344,6 @@ export default defineNuxtModule<ModuleOptions>({
177
344
  head.script.push(...integrations.scripts as any[])
178
345
  head.titleTemplate = `%s %separator ${site.title}`
179
346
 
180
- // ---- 构建信息(供 BlogTech 等 Widget 使用) ----
181
- const consumerPkg = await loadJson(resolve(rootDir, 'package.json'), jiti)
182
- const themePkg = await loadJson(resolve(themePkgDir, 'package.json'), jiti)
183
- nuxt.options.runtimeConfig.public.clarity = {
184
- theme: 'Clarity',
185
- themeVersion: String(themePkg?.version ?? ''),
186
- themeHomepage: String(themePkg?.homepage ?? ''),
187
- siteVersion: String(consumerPkg?.version ?? ''),
188
- sitePackageManager: String(consumerPkg?.packageManager ?? ''),
189
- nuxtVersion: nuxtPkg.version,
190
- vueVersion: vuePkg.version,
191
- }
192
-
193
347
  // ---- 路由规则 ----
194
348
  const routeRules = nuxt.options.routeRules ??= {}
195
349
  if (features.stats) {
@@ -214,16 +368,11 @@ export default defineNuxtModule<ModuleOptions>({
214
368
  }
215
369
  })
216
370
 
217
- // ---- 可选功能:anti-mirror ----
218
- // 黑名单完全由站点配置提供;Theme 不携带任何上游默认域名(审计发现 #5)。
371
+ // ---- features.antiMirror 已废弃 ----
372
+ // 上游 modules/anti-mirror 由 Nuxt 自动加载(Layer 的 src/modules 同样生效),
373
+ // 黑名单与跳转目标与上游保持一致;配置项仅为 0.1.x 兼容保留。
219
374
  if (features.antiMirror !== false) {
220
- const blacklist = typeof features.antiMirror === 'boolean' ? [] : features.antiMirror.blacklist
221
- if (blacklist.length === 0) {
222
- logger.warn('features.antiMirror 已启用,但未配置 blacklist,已跳过反镜像脚本注入。')
223
- }
224
- else {
225
- await injectAntiMirror(nuxt, blacklist, site.url)
226
- }
375
+ logger.warn('features.antiMirror 已废弃:上游 anti-mirror 模块始终启用,黑名单与上游保持一致。')
227
376
  }
228
377
  },
229
378
  })
@@ -251,8 +400,8 @@ function parseClarityConfig(raw: unknown, configPath: string): ClarityConfig {
251
400
  return result.data
252
401
  }
253
402
 
254
- /** 消费项目 app/app.config.ts 若覆盖站点级字段,会在 defu 合并中压过 clarity.config.ts 注入,需要提示迁移 */
255
- async function warnNonUiAppConfigOverrides(nuxt: Nuxt, jiti: ReturnType<typeof createJiti>, logger: ReturnType<typeof useLogger>) {
403
+ /** 从消费项目 app/app.config.ts 读取导出对象(失败时交给 Nuxt 自身报错) */
404
+ async function loadConsumerAppConfig(nuxt: Nuxt, jiti: ReturnType<typeof createJiti>) {
256
405
  const candidates = [
257
406
  resolve(nuxt.options.srcDir, 'app.config.ts'),
258
407
  resolve(nuxt.options.rootDir, 'app.config.ts'),
@@ -267,20 +416,10 @@ async function warnNonUiAppConfigOverrides(nuxt: Nuxt, jiti: ReturnType<typeof c
267
416
  globalScope.defineAppConfig ??= (config: unknown) => config
268
417
  try {
269
418
  const mod = await jiti.import(appConfigPath) as unknown
270
- const clarity = ((mod as { default?: unknown })?.default ?? mod) as { clarity?: Record<string, unknown> } | undefined
271
- if (!clarity?.clarity || typeof clarity.clarity !== 'object') {
272
- continue
273
- }
274
- const offenders = Object.keys(clarity.clarity).filter(key => !uiConfigKeys.has(key))
275
- if (offenders.length) {
276
- logger.warn(
277
- `app/app.config.ts 的 clarity 中出现站点级字段 [${offenders.join(', ')}],`
278
- + '它们会覆盖 clarity.config.ts 的注入结果;请将站点配置迁移到 clarity.config.ts。',
279
- )
280
- }
419
+ return ((mod as { default?: unknown })?.default ?? mod) as Record<string, unknown> | undefined
281
420
  }
282
421
  catch {
283
- // 尽力而为的边界提示:app.config 加载失败时交给 Nuxt 自身报错
422
+ return undefined
284
423
  }
285
424
  finally {
286
425
  if (previousDefine === undefined) {
@@ -291,15 +430,134 @@ async function warnNonUiAppConfigOverrides(nuxt: Nuxt, jiti: ReturnType<typeof c
291
430
  }
292
431
  }
293
432
  }
433
+ return undefined
434
+ }
435
+
436
+ /** 站点级字段出现在 app.config.ts 会压过 clarity.config.ts 注入,需要提示迁移 */
437
+ async function warnSiteLevelAppConfigOverrides(appConfig: Record<string, unknown> | undefined, logger: ReturnType<typeof useLogger>) {
438
+ if (!appConfig) {
439
+ return
440
+ }
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
+ }
451
+ if (offenders.length) {
452
+ logger.warn(
453
+ `app/app.config.ts 中出现站点级字段 [${offenders.join(', ')}],`
454
+ + '它们会覆盖 clarity.config.ts 的注入结果;请将站点配置迁移到 clarity.config.ts。',
455
+ )
456
+ }
457
+ }
458
+
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
+ /** clarity.config → 上游 blog.config 扁平形状(与 src/blog.config.ts 适配层保持一致) */
487
+ function toUpstreamBlogConfig(config: ClarityConfig) {
488
+ const { site, article, feed, stats, integrations } = config
489
+ return {
490
+ title: site.title,
491
+ subtitle: site.subtitle ?? '',
492
+ description: site.description,
493
+ author: {
494
+ name: site.author.name,
495
+ avatar: site.author.avatar ?? '',
496
+ email: site.author.email ?? '',
497
+ homepage: site.author.homepage ?? '/',
498
+ },
499
+ copyright: site.copyright ?? { abbr: '', name: '', url: '' },
500
+ favicon: site.favicon,
501
+ language: site.language,
502
+ timeEstablished: site.established ?? '',
503
+ timeZone: site.timezone,
504
+ url: site.url,
505
+ defaultCategory: article.defaultCategory,
506
+ article: {
507
+ categories: article.categories,
508
+ types: article.types,
509
+ order: article.order,
510
+ useRandomPremalat: false,
511
+ hidePostPrefix: article.hidePostPrefix,
512
+ robotsNotIndex: article.robotsNotIndex,
513
+ },
514
+ feed,
515
+ scripts: integrations.scripts,
516
+ stats,
517
+ twikoo: integrations.twikoo ?? { envId: '', preload: '' },
518
+ }
519
+ }
520
+
521
+ function toMyFeedEntry(blogConfig: ReturnType<typeof toUpstreamBlogConfig>) {
522
+ return {
523
+ author: blogConfig.author.name,
524
+ sitenick: blogConfig.title,
525
+ title: blogConfig.title,
526
+ desc: blogConfig.subtitle || blogConfig.description,
527
+ link: blogConfig.url,
528
+ feed: new URL('/atom.xml', blogConfig.url).toString(),
529
+ icon: blogConfig.favicon,
530
+ avatar: blogConfig.author.avatar,
531
+ archs: ['Nuxt'],
532
+ date: blogConfig.timeEstablished,
533
+ comment: '这是我自己',
534
+ }
535
+ }
536
+
537
+ /** 读取消费项目 pnpm-workspace.yaml 的 catalogs;缺失或无 catalogs 时回退到 Theme 内置版本数据 */
538
+ function loadPnpmWorkspace(rootDir: string) {
539
+ const yamlPath = resolve(rootDir, 'pnpm-workspace.yaml')
540
+ if (existsSync(yamlPath)) {
541
+ try {
542
+ const parsed = parseYaml(readFileSync(yamlPath, 'utf8')) as Record<string, unknown>
543
+ if (parsed?.catalogs && typeof parsed.catalogs === 'object') {
544
+ return { catalogs: parsed.catalogs as Record<string, Record<string, string>> }
545
+ }
546
+ }
547
+ catch {
548
+ // 解析失败时回退到 Theme 内置数据
549
+ }
550
+ }
551
+ return { catalogs: fallbackPnpmWorkspace.catalogs as Record<string, Record<string, string>> }
294
552
  }
295
553
 
296
- async function injectAntiMirror(nuxt: Nuxt, blacklist: string[], target: string) {
297
- const source = `(${handleMirror.toString()})(${JSON.stringify(blacklist.map(btoa))},${JSON.stringify(btoa(target))})`
298
- const { code, errors } = await minify('clarity-anti-mirror.iife.js', source)
299
- if (errors.length > 0) {
300
- throw new Error(`[clarity-config] anti-mirror 脚本压缩失败:\n${errors.map(error => error.message ?? String(error)).join('\n')}`)
554
+ async function loadJson(path: string, jiti: ReturnType<typeof createJiti>) {
555
+ try {
556
+ return ((await jiti.import(path)) as { default?: Record<string, unknown> } | undefined)?.default
557
+ }
558
+ catch {
559
+ return undefined
301
560
  }
302
- ;(nuxt.options.app.head.script ??= []).push({ innerHTML: code })
303
561
  }
304
562
 
305
563
  function findConfigFile(rootDir: string) {
@@ -311,20 +569,20 @@ function findConfigFile(rootDir: string) {
311
569
  return 'clarity.config.ts'
312
570
  }
313
571
 
314
- function findFeedsFile(rootDir: string, themeSrcDir: string) {
315
- for (const name of ['feeds.ts', 'feeds.mjs', 'feeds.js']) {
316
- if (existsSync(resolve(rootDir, name))) {
317
- return { found: true, resolved: resolve(rootDir, name) }
572
+ function findExisting(dir: string, names: string[]) {
573
+ for (const name of names) {
574
+ const path = resolve(dir, name)
575
+ if (existsSync(path)) {
576
+ return path
318
577
  }
319
578
  }
320
- return { found: false, resolved: resolve(themeSrcDir, 'config/feeds.empty.ts') }
579
+ return undefined
321
580
  }
322
581
 
323
- async function loadJson(path: string, jiti: ReturnType<typeof createJiti>) {
324
- try {
325
- return ((await jiti.import(path)) as { default?: Record<string, unknown> } | undefined)?.default
326
- }
327
- catch {
328
- return undefined
582
+ function findFeedsFile(rootDir: string, themeSrcDir: string) {
583
+ const found = findExisting(rootDir, ['feeds.ts', 'feeds.mjs', 'feeds.js'])
584
+ if (found) {
585
+ return { found: true, resolved: found }
329
586
  }
587
+ return { found: false, resolved: resolve(themeSrcDir, 'config/feeds.empty.ts') }
330
588
  }
@@ -45,6 +45,19 @@ export default defineNuxtModule({
45
45
  shared: directories.shared,
46
46
  }
47
47
 
48
+ // Nuxt 的 LayerAliasingPlugin 会把 Layer 源码内的 ~ / ~~ 前缀按
49
+ // layer 自身目录改写(node_modules Layer 默认指向 Theme 包根)。
50
+ // 上游组件中的 ~~ 语义是「站点根目录」(~~/package.json、~/feeds 等),
51
+ // 这里把本 Layer 的别名显式指向消费项目目录;消费项目不存在的文件
52
+ // (blog.config、shared/、feeds)由 clarity-config 的精确别名兜底。
53
+ layer.config.alias = {
54
+ ...layer.config.alias,
55
+ '~': nuxt.options.srcDir,
56
+ '@': nuxt.options.srcDir,
57
+ '~~': nuxt.options.rootDir,
58
+ '@@': nuxt.options.rootDir,
59
+ }
60
+
48
61
  // `getLayerDirectories()` caches a plain metadata object per layer before
49
62
  // modules are installed. Nuxt exposes no invalidation API, so update the
50
63
  // cached paths in place as well as `layer.config`.
@@ -4,12 +4,12 @@ import { groupBy } from 'es-toolkit/array'
4
4
  import { sumBy } from 'es-toolkit/math'
5
5
  import { mapValues } from 'es-toolkit/object'
6
6
 
7
- const clarity = useClarityConfig()
7
+ const appConfig = useAppConfig()
8
8
  useSeoMeta({
9
9
  title: '归档',
10
- description: `${clarity.site.title}的所有文章归档。`,
10
+ description: `${appConfig.title}的所有文章归档。`,
11
11
  })
12
- const birthYear = computed(() => clarity.component.stats.birthYear)
12
+ const birthYear = computed(() => appConfig.component.stats.birthYear)
13
13
  const showTuning = ref(false)
14
14
  const spacing = ref(0)
15
15
  const column = ref(1)
@@ -36,7 +36,7 @@ const yearlyWordCount = computed(() =>
36
36
 
37
37
  function getArticleYear(article: ArticleProps) {
38
38
  try {
39
- return toZonedTemporal((article as any)[sortOrder.value] as string, clarity.site.timezone).year.toString()
39
+ return toZonedTemporal(article[sortOrder.value] as string).year.toString()
40
40
  }
41
41
  catch {
42
42
  return ''
@@ -1,10 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { orderBy } from 'es-toolkit/array'
3
3
 
4
- const clarity = useClarityConfig()
4
+ const appConfig = useAppConfig()
5
5
  useSeoMeta({
6
- description: clarity.site.description,
7
- ogImage: clarity.site.author.avatar,
6
+ description: appConfig.description,
7
+ ogImage: appConfig.author.avatar,
8
8
  })
9
9
 
10
10
  const { data: listRaw } = await useAsyncData('posts:index', () => getArticleIndexOptions(), { default: () => [] })
@@ -34,6 +34,7 @@ const { data: previewCount } = useAsyncData(
34
34
  <template #aside>
35
35
  <WidgetBlogStats />
36
36
  <WidgetBlogTech />
37
+ <WidgetCommGroup />
37
38
  </template>
38
39
 
39
40
  <BlogHeader class="mobile-only" to="/" tag="h1" />
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
- import feeds from '#clarity/feeds'
2
+ import { myFeed } from '~~/blog.config'
3
+ import feeds from '~/feeds'
3
4
 
4
- const clarity = useClarityConfig()
5
- const myFeed = useClaritySiteFeedEntry()
5
+ const appConfig = useAppConfig()
6
6
 
7
7
  const { data: postLink } = await useAsyncData(
8
8
  'content:/link',
@@ -12,7 +12,7 @@ const { data: postLink } = await useAsyncData(
12
12
  useSeoMeta({
13
13
  title: '友链',
14
14
  ogType: 'profile',
15
- description: `${clarity.site.title}的友链页面,收集了添加他为友链的网站和他订阅的网站列表。`,
15
+ description: `${appConfig.title}的友链页面,收集了添加他为友链的网站和他订阅的网站列表。`,
16
16
  })
17
17
 
18
18
  const copyFields = {
@@ -33,7 +33,7 @@ const copyFields = {
33
33
  v-for="group in feeds"
34
34
  :key="group.name"
35
35
  v-bind="group"
36
- :shuffle="clarity.link.randomInGroup"
36
+ :shuffle="appConfig.link.randomInGroup"
37
37
  />
38
38
 
39
39
  <Tab :tabs="['我的博客信息', '申请友链']" center>
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
- const clarity = useClarityConfig()
2
+ const appConfig = useAppConfig()
3
3
  useSeoMeta({
4
4
  title: '预览',
5
- description: `${clarity.site.title}的文章预览。`,
5
+ description: `${appConfig.title}的文章预览。`,
6
6
  })
7
7
  const { data: listRaw } = await useAsyncData('previews:index', () => getArticleIndexOptions('previews/%'), { default: () => [] })
8
8
  const { listSorted, isAscending, sortOrder } = useArticleSort(listRaw)