clarity-theme 0.1.3 → 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.
Files changed (62) hide show
  1. package/README.md +15 -5
  2. package/README.zh-CN.md +15 -5
  3. package/nuxt.config.ts +19 -1
  4. package/package.json +14 -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 +16 -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 +40 -11
  35. package/src/config/content.ts +6 -0
  36. package/src/config/feed.ts +2 -42
  37. package/src/config/index.ts +2 -2
  38. package/src/config/pnpm-workspace.ts +19 -0
  39. package/src/config/public.ts +3 -61
  40. package/src/config/ui.ts +111 -0
  41. package/src/modules/anti-mirror/index.ts +38 -0
  42. package/src/modules/{clarity-config/anti-mirror-client.ts → anti-mirror/runtime/client.ts} +3 -3
  43. package/src/modules/clarity-config/index.ts +354 -93
  44. package/src/modules/clarity-source-layout/index.ts +13 -0
  45. package/src/pages/archive.vue +4 -4
  46. package/src/pages/index.vue +4 -3
  47. package/src/pages/link.vue +5 -5
  48. package/src/pages/preview.vue +2 -2
  49. package/src/server/api/stats.get.ts +9 -19
  50. package/src/server/routes/atom.xml.get.ts +48 -50
  51. package/src/server/routes/subscriptions.opml.get.ts +14 -31
  52. package/src/server/utils/clarity.ts +1 -1
  53. package/src/shared/utils/time.ts +13 -18
  54. package/src/types/article.ts +2 -2
  55. package/src/types/feed.ts +35 -1
  56. package/src/types/upstream-compat.d.ts +17 -0
  57. package/src/types/yaml-modules.d.ts +26 -0
  58. package/src/shared/types/content.ts +0 -30
  59. package/src/shared/utils/clarity.ts +0 -34
  60. package/src/types/modal.ts +0 -16
  61. /package/src/assets/icons/{solar-rewind-back-bold-duotone.svg → SolarRewindBackBoldDuotone.svg} +0 -0
  62. /package/src/assets/icons/{solar-rewind-forward-bold-duotone.svg → SolarRewindForwardBoldDuotone.svg} +0 -0
@@ -1,8 +1,9 @@
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
+ import { platform } from 'node:process'
6
7
  import { fileURLToPath } from 'node:url'
7
8
  import { createJiti } from 'jiti'
8
9
  import {
@@ -12,21 +13,19 @@ import {
12
13
  updateAppConfig,
13
14
  useLogger,
14
15
  } from 'nuxt/kit'
15
- import nuxtPkg from 'nuxt/package.json'
16
- import { minify } from 'oxc-minify'
17
- import vuePkg from 'vue/package.json'
18
- import { toPublicClarityConfig } from '../../config/public'
16
+ import { parse as parseYaml } from 'yaml'
17
+ import { fallbackPnpmWorkspace } from '../../config/pnpm-workspace'
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, '../../..')
25
+ /** Windows 路径比较需要大小写不敏感(vite 解析产物盘符大小写不定) */
26
+ const IS_WINDOWS = platform === 'win32'
28
27
 
29
- /** app/app.config.ts 的 clarity 键只允许 UI 覆盖(站点级字段属于 clarity.config.ts) */
28
+ /** app/app.config.ts 中允许作为 UI 覆盖的顶层键(站点级字段属于 clarity.config.ts) */
30
29
  const uiConfigKeys = new Set(['component', 'footer', 'header', 'link', 'nav', 'pagination', 'themes'])
31
30
 
32
31
  export interface ModuleOptions {
@@ -34,6 +33,15 @@ export interface ModuleOptions {
34
33
  configFile?: string
35
34
  }
36
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
+
37
45
  export default defineNuxtModule<ModuleOptions>({
38
46
  meta: {
39
47
  name: 'clarity-config',
@@ -46,17 +54,29 @@ export default defineNuxtModule<ModuleOptions>({
46
54
 
47
55
  const configPath = resolve(rootDir, options.configFile ?? findConfigFile(rootDir))
48
56
  const feedsPath = findFeedsFile(rootDir, themeSrcDir)
57
+ const consumerBlogConfigPath = findExisting(rootDir, ['blog.config.ts', 'blog.config.mjs', 'blog.config.js'])
58
+ const consumerSrcFeedsPath = findExisting(nuxt.options.srcDir, ['feeds.ts', 'feeds.mjs', 'feeds.js'])
49
59
 
50
60
  // ---- 依赖注入层:Theme 内部不感知消费项目的文件布局 ----
51
- Object.assign(nuxt.options.alias, {
61
+ // 上游文件通过 `~~/blog.config` / `~/feeds` 读取站点数据;消费项目使用
62
+ // clarity.config.ts(或根目录 feeds.ts),这里以前缀更长的别名承接,
63
+ // 使上游同步文件无需改写即可在 Layer 中解析。
64
+ // 新键必须置于 '~' / '~~' 之前,保证 Vite / Nitro 前缀匹配优先命中。
65
+ nuxt.options.alias = {
66
+ ...(consumerBlogConfigPath ? {} : { '~~/blog.config': resolve(themeSrcDir, 'blog.config.ts') }),
67
+ // 上游 server 路由通过 ~~/shared/utils/time 引用工具;消费项目没有
68
+ // shared 目录时指向 Theme 内置实现。
69
+ ...(existsSync(resolve(rootDir, 'shared')) ? {} : { '~~/shared': resolve(themeSrcDir, 'shared') }),
70
+ ...(consumerSrcFeedsPath ? {} : { '~/feeds': feedsPath.resolved }),
52
71
  '#clarity/config': configPath,
53
72
  '#clarity/feeds': feedsPath.resolved,
54
- })
73
+ ...nuxt.options.alias,
74
+ }
55
75
 
56
76
  // @pinia/nuxt 不会自动扫描 Layer 的 stores 目录,需显式注册
57
77
  addImportsDir(resolve(themeSrcDir, 'stores'))
58
78
 
59
- // ---- AppConfig 类型化:消费者 defineAppConfig({ clarity: ... }) 获得完整类型提示 ----
79
+ // ---- AppConfig 类型化:上游扁平键获得类型提示 ----
60
80
  // 注意:类型文件生成于 buildDir/types/ 下,相对路径必须以其为基准计算,
61
81
  // 否则导入解析失败会因 skipLibCheck 静默退化为 any(审计发现 #9)。
62
82
  const appConfigTypeDir = resolve(nuxt.options.buildDir, 'types')
@@ -68,16 +88,20 @@ export default defineNuxtModule<ModuleOptions>({
68
88
  // app/types/index.ts 的 Window.twikoo 等环境声明在 node_modules Layer 中
69
89
  // 不会被 tsconfig include 命中(被 node_modules exclude 过滤),需显式导入
70
90
  `import '${globalsTypePath}'`,
71
- `import type { ClarityAppConfig, ClarityUiConfigInput } from '${appConfigTypePath}'`,
91
+ `import type { ClarityAppConfigArticle, ClarityUiConfig } from '${appConfigTypePath}'`,
72
92
  '',
73
93
  'declare module \'@nuxt/schema\' {',
74
- ' // 读取侧:unknown 使 MergedAppConfig 回落到 Theme 注入的完整 Resolved 类型',
94
+ ' // 读取侧:UI 扁平键补充完整类型(0.2.0 起不再提供 clarity 嵌套键),',
95
+ ' // 使上游组件在 JSON 字面量(空数组 / 宽化字符串)下仍获得正确类型。',
96
+ ' // article 只声明 categories(Record 索引),order / types 保留 Resolved 字面量。',
75
97
  ' interface CustomAppConfig {',
76
- ' clarity?: unknown',
77
- ' }',
78
- ' // 输入侧:完整配置(Theme 内部)或 UI 部分覆盖(消费项目 app/app.config.ts)',
79
- ' interface AppConfigInput {',
80
- ' clarity?: ClarityAppConfig | ClarityUiConfigInput',
98
+ ' article?: ClarityAppConfigArticle',
99
+ ' component?: ClarityUiConfig[\'component\']',
100
+ ' footer?: ClarityUiConfig[\'footer\']',
101
+ ' header?: ClarityUiConfig[\'header\']',
102
+ ' link?: ClarityUiConfig[\'link\']',
103
+ ' nav?: ClarityUiConfig[\'nav\']',
104
+ ' themes?: ClarityUiConfig[\'themes\']',
81
105
  ' }',
82
106
  '}',
83
107
  '',
@@ -110,32 +134,214 @@ export default defineNuxtModule<ModuleOptions>({
110
134
  const config = parseClarityConfig((configModule as { default?: unknown })?.default ?? configModule, configPath)
111
135
  const { site, article, integrations, features } = config
112
136
 
113
- // ---- 边界审计:app/app.config.ts 中不应出现站点级字段(其优先级更高,会覆盖站点配置注入) ----
114
- await warnNonUiAppConfigOverrides(nuxt, jiti, logger)
137
+ // ---- 上游构建期数据模块:~~/package.json 与 ~~/pnpm-workspace.yaml ----
138
+ // 上游 BlogTech / atom.xml 通过这两个说明符读取消费项目数据;直接把别名
139
+ // 指到真实 JSON/YAML 文件时,Nitro prerenderer 会将其外部化(Node 无法
140
+ // 以 ESM 导入 JSON/YAML),导致含 BlogTech 的页面(首页)在预渲染时崩溃。
141
+ // 因此在 buildDir 生成等价 ES 模块,运行时别名与 TS 声明都指向生成物。
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
+ }
150
+ const pnpmWorkspace = loadPnpmWorkspace(rootDir)
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')
159
+ const packageJsonModulePath = join(buildInfoDir, 'package-json.mjs').replaceAll('\\', '/')
160
+ const pnpmWorkspaceModulePath = join(buildInfoDir, 'pnpm-workspace.mjs').replaceAll('\\', '/')
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)
186
+ nuxt.options.alias = {
187
+ '~~/package.json': packageJsonModulePath,
188
+ '~~/pnpm-workspace.yaml': pnpmWorkspaceModulePath,
189
+ ...nuxt.options.alias,
190
+ }
191
+
192
+ // 上游形状的 blog.config 数据模块(<buildDir>/clarity/blog.config.mjs):
193
+ // anti-mirror 等 jiti 加载的 Layer 模块无法解析 #clarity/* 别名
194
+ // (src/blog.config.ts 运行时适配层仅适用于 App / Nitro 构建上下文),
195
+ // 因此把映射结果内联成真实模块,供其以 file URL 动态导入。
115
196
 
116
- // ---- Site Config → App Config 桥梁 ----
117
- // 注意 Nuxt appConfig 合并优先级:消费项目 app.config > Theme app.config > 模块注入,
118
- // 因此站点派生默认值(header.logo 等)不放入 Theme 的 app.config.ts,
119
- // 由这里注入后仍可被消费项目覆盖。
120
- updateAppConfig({
121
- clarity: {
122
- ...toPublicClarityConfig(config),
123
- header: {
124
- logo: site.author.avatar ?? '',
125
- subtitle: site.subtitle ?? '',
126
- },
127
- footer: {
128
- copyright: site.copyright?.name
129
- ? `© ${new Date().getFullYear()} ${site.author.name} · ${site.copyright.name}`
130
- : `© ${new Date().getFullYear()} ${site.author.name}`,
197
+ // 两个说明符的 TS 侧声明(app / server 类型工程的路径重定向目标)
198
+ const yamlDecl = addTypeTemplate({
199
+ filename: 'types/clarity-yaml-workspace.d.ts',
200
+ getContents: () => [
201
+ 'declare const value: Record<string, unknown>',
202
+ 'export default value',
203
+ '',
204
+ ].join('\n'),
205
+ })
206
+ const packageJsonDecl = addTypeTemplate({
207
+ filename: 'types/clarity-package-json.d.ts',
208
+ getContents: () => [
209
+ 'declare const value: { name: string, version: string, packageManager: string }',
210
+ 'export default value',
211
+ 'export const packageManager: string',
212
+ 'export const version: string',
213
+ '',
214
+ ].join('\n'),
215
+ })
216
+
217
+ // 上游类型兼容垫片(post.author 可选字段、*.yaml 导入声明)
218
+ // 需要在 app / shared / server 三个类型工程中同样生效。
219
+ const compatTypesPath = resolve(themeSrcDir, 'types/upstream-compat.d.ts')
220
+ const withLeadingDot = (path: string) => path.startsWith('.') ? path : `./${path}`
221
+ nuxt.hook('prepare:types', ({ references, sharedReferences, nodeReferences, tsConfig }) => {
222
+ references.push({ path: compatTypesPath })
223
+ sharedReferences.push({ path: compatTypesPath })
224
+ nodeReferences.push({ path: compatTypesPath })
225
+ // TS 的别名路径替换会去掉 .json/.yaml 扩展名导致解析失败,
226
+ // 重定向到上方声明模块。
227
+ if (tsConfig.compilerOptions?.paths) {
228
+ const packageJsonDeclPath = relative(nuxt.options.buildDir, resolve(nuxt.options.buildDir, packageJsonDecl.filename)).replaceAll('\\', '/')
229
+ tsConfig.compilerOptions.paths['~~/package.json'] = [withLeadingDot(packageJsonDeclPath)]
230
+ const yamlDeclPath = relative(nuxt.options.buildDir, resolve(nuxt.options.buildDir, yamlDecl.filename)).replaceAll('\\', '/')
231
+ tsConfig.compilerOptions.paths['~~/pnpm-workspace.yaml'] = [withLeadingDot(yamlDeclPath)]
232
+ }
233
+ })
234
+ nuxt.hook('nitro:prepare:types', ({ declarations }) => {
235
+ declarations.push(`/// <reference types="${compatTypesPath.replaceAll('\\', '/')}" />`)
236
+ })
237
+
238
+ // prerenderer 是独立的 Nitro 子构建(nitro:config 不会对其生效),
239
+ // 生成模块在其中必须内联:外部化为绝对 Windows 路径后 Node 无法
240
+ // 以 ESM 导入,含 BlogTech 的首页会在预渲染时挂起。
241
+ ;(nuxt.hook as unknown as (event: string, cb: (config: Record<string, any>) => void) => void)('prerender:config', (prerendererConfig) => {
242
+ prerendererConfig.externals ??= {}
243
+ prerendererConfig.externals.inline ??= []
244
+ prerendererConfig.externals.inline.push(packageJsonModulePath, pnpmWorkspaceModulePath)
245
+ })
246
+ nuxt.hook('nitro:config', (nitroConfig) => {
247
+ nitroConfig.typescript ??= {}
248
+ nitroConfig.typescript.tsConfig ??= {}
249
+ nitroConfig.typescript.tsConfig.compilerOptions ??= {}
250
+ nitroConfig.typescript.tsConfig.compilerOptions.paths ??= {}
251
+ nitroConfig.typescript.tsConfig.compilerOptions.paths['~~/package.json'] = [withLeadingDot(packageJsonDecl.filename)]
252
+ nitroConfig.typescript.tsConfig.compilerOptions.paths['~~/pnpm-workspace.yaml'] = [withLeadingDot(yamlDecl.filename)]
253
+ })
254
+
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
+ },
131
286
  },
287
+ })
288
+ })
289
+
290
+ // ---- 消费项目 app/app.config.ts:站点级字段的迁移警告 ----
291
+ const consumerAppConfig = await loadConsumerAppConfig(nuxt, jiti)
292
+ await warnSiteLevelAppConfigOverrides(consumerAppConfig, logger)
293
+
294
+ // ---- 上游形状的扁平 AppConfig 注入(upstream parity 的关键)----
295
+ // blog-v3 组件一律通过 useAppConfig() 读取扁平键(title / nav / component.*)。
296
+ // 这里将 clarity.config.ts + UI 默认值注入为同一形状;
297
+ // 消费项目仍可在自己的 app.config.ts 中按上游习惯直接覆盖任意扁平键。
298
+ updateAppConfig({
299
+ // 站点数据(上游来自 blog.config.ts 展开进 app.config.ts)
300
+ title: site.title,
301
+ subtitle: site.subtitle ?? '',
302
+ description: site.description,
303
+ author: {
304
+ name: site.author.name,
305
+ avatar: site.author.avatar ?? '',
306
+ email: site.author.email ?? '',
307
+ homepage: site.author.homepage ?? '/',
308
+ },
309
+ copyright: site.copyright ?? { abbr: '', name: '', url: '' },
310
+ favicon: site.favicon,
311
+ language: site.language,
312
+ timeEstablished: site.established ?? '',
313
+ timeZone: site.timezone,
314
+ url: site.url,
315
+ defaultCategory: article.defaultCategory,
316
+ article: {
317
+ categories: article.categories,
318
+ types: article.types,
319
+ order: article.order,
320
+ useRandomPremalat: false,
321
+ hidePostPrefix: article.hidePostPrefix,
322
+ robotsNotIndex: article.robotsNotIndex,
323
+ },
324
+ feed: config.feed,
325
+ scripts: integrations.scripts,
326
+ stats: config.stats,
327
+ twikoo: integrations.twikoo ?? { envId: '', preload: '' },
328
+
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}`,
132
339
  },
133
340
  })
134
341
 
135
342
  // ---- 服务端专用配置:注入 Nitro 私有 runtimeConfig(不进入客户端 bundle)----
136
- // Atom/OPML/stats 处理器与 feature 路由守卫从这里读取完整配置。
137
- // 注意:Nuxt 会按消费者实际值推导 runtimeConfig 类型(可选字段可能被推断为必填),
138
- // 此处以 Theme 声明的 ClarityServerConfig 为准,避免与消费者无关的推断形状冲突。
343
+ // 保留 0.1.x 的 useClarityServerConfig() 兼容;上游 server 路由已恢复为
344
+ // 直接读取 `~~/blog.config`,不再依赖该通道。
139
345
  ;(nuxt.options.runtimeConfig as { clarity?: ClarityServerConfig }).clarity = toServerClarityConfig(config)
140
346
 
141
347
  // ---- SEO / site / robots / llms ----
@@ -177,19 +383,6 @@ export default defineNuxtModule<ModuleOptions>({
177
383
  head.script.push(...integrations.scripts as any[])
178
384
  head.titleTemplate = `%s %separator ${site.title}`
179
385
 
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
386
  // ---- 路由规则 ----
194
387
  const routeRules = nuxt.options.routeRules ??= {}
195
388
  if (features.stats) {
@@ -214,16 +407,11 @@ export default defineNuxtModule<ModuleOptions>({
214
407
  }
215
408
  })
216
409
 
217
- // ---- 可选功能:anti-mirror ----
218
- // 黑名单完全由站点配置提供;Theme 不携带任何上游默认域名(审计发现 #5)。
410
+ // ---- features.antiMirror 已废弃 ----
411
+ // 上游 modules/anti-mirror 由 Nuxt 自动加载(Layer 的 src/modules 同样生效),
412
+ // 黑名单与跳转目标与上游保持一致;配置项仅为 0.1.x 兼容保留。
219
413
  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
- }
414
+ logger.warn('features.antiMirror 已废弃:上游 anti-mirror 模块始终启用,黑名单与上游保持一致。')
227
415
  }
228
416
  },
229
417
  })
@@ -251,8 +439,8 @@ function parseClarityConfig(raw: unknown, configPath: string): ClarityConfig {
251
439
  return result.data
252
440
  }
253
441
 
254
- /** 消费项目 app/app.config.ts 若覆盖站点级字段,会在 defu 合并中压过 clarity.config.ts 注入,需要提示迁移 */
255
- async function warnNonUiAppConfigOverrides(nuxt: Nuxt, jiti: ReturnType<typeof createJiti>, logger: ReturnType<typeof useLogger>) {
442
+ /** 从消费项目 app/app.config.ts 读取导出对象(失败时交给 Nuxt 自身报错) */
443
+ async function loadConsumerAppConfig(nuxt: Nuxt, jiti: ReturnType<typeof createJiti>) {
256
444
  const candidates = [
257
445
  resolve(nuxt.options.srcDir, 'app.config.ts'),
258
446
  resolve(nuxt.options.rootDir, 'app.config.ts'),
@@ -267,20 +455,10 @@ async function warnNonUiAppConfigOverrides(nuxt: Nuxt, jiti: ReturnType<typeof c
267
455
  globalScope.defineAppConfig ??= (config: unknown) => config
268
456
  try {
269
457
  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
- }
458
+ return ((mod as { default?: unknown })?.default ?? mod) as Record<string, unknown> | undefined
281
459
  }
282
460
  catch {
283
- // 尽力而为的边界提示:app.config 加载失败时交给 Nuxt 自身报错
461
+ return undefined
284
462
  }
285
463
  finally {
286
464
  if (previousDefine === undefined) {
@@ -291,33 +469,89 @@ async function warnNonUiAppConfigOverrides(nuxt: Nuxt, jiti: ReturnType<typeof c
291
469
  }
292
470
  }
293
471
  }
472
+ return undefined
294
473
  }
295
474
 
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')}`)
475
+ /** 站点级字段出现在 app.config.ts 会压过 clarity.config.ts 注入,需要提示迁移 */
476
+ async function warnSiteLevelAppConfigOverrides(appConfig: Record<string, unknown> | undefined, logger: ReturnType<typeof useLogger>) {
477
+ if (!appConfig) {
478
+ return
479
+ }
480
+ const offenders = Object.keys(appConfig).filter(key => !uiConfigKeys.has(key))
481
+ if (offenders.length) {
482
+ logger.warn(
483
+ `app/app.config.ts 中出现站点级字段 [${offenders.join(', ')}],`
484
+ + '它们会覆盖 clarity.config.ts 的注入结果;请将站点配置迁移到 clarity.config.ts。',
485
+ )
301
486
  }
302
- ;(nuxt.options.app.head.script ??= []).push({ innerHTML: code })
303
487
  }
304
488
 
305
- function findConfigFile(rootDir: string) {
306
- for (const name of ['clarity.config.ts', 'clarity.config.mjs', 'clarity.config.js']) {
307
- if (existsSync(resolve(rootDir, name))) {
308
- return name
309
- }
489
+ /** clarity.config → 上游 blog.config 扁平形状(与 src/blog.config.ts 适配层保持一致) */
490
+ function toUpstreamBlogConfig(config: ClarityConfig) {
491
+ const { site, article, feed, stats, integrations } = config
492
+ return {
493
+ title: site.title,
494
+ subtitle: site.subtitle ?? '',
495
+ description: site.description,
496
+ author: {
497
+ name: site.author.name,
498
+ avatar: site.author.avatar ?? '',
499
+ email: site.author.email ?? '',
500
+ homepage: site.author.homepage ?? '/',
501
+ },
502
+ copyright: site.copyright ?? { abbr: '', name: '', url: '' },
503
+ favicon: site.favicon,
504
+ language: site.language,
505
+ timeEstablished: site.established ?? '',
506
+ timeZone: site.timezone,
507
+ url: site.url,
508
+ defaultCategory: article.defaultCategory,
509
+ article: {
510
+ categories: article.categories,
511
+ types: article.types,
512
+ order: article.order,
513
+ useRandomPremalat: false,
514
+ hidePostPrefix: article.hidePostPrefix,
515
+ robotsNotIndex: article.robotsNotIndex,
516
+ },
517
+ feed,
518
+ scripts: integrations.scripts,
519
+ stats,
520
+ twikoo: integrations.twikoo ?? { envId: '', preload: '' },
310
521
  }
311
- return 'clarity.config.ts'
312
522
  }
313
523
 
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) }
524
+ function toMyFeedEntry(blogConfig: ReturnType<typeof toUpstreamBlogConfig>) {
525
+ return {
526
+ author: blogConfig.author.name,
527
+ sitenick: blogConfig.title,
528
+ title: blogConfig.title,
529
+ desc: blogConfig.subtitle || blogConfig.description,
530
+ link: blogConfig.url,
531
+ feed: new URL('/atom.xml', blogConfig.url).toString(),
532
+ icon: blogConfig.favicon,
533
+ avatar: blogConfig.author.avatar,
534
+ archs: ['Nuxt'],
535
+ date: blogConfig.timeEstablished,
536
+ comment: '这是我自己',
537
+ }
538
+ }
539
+
540
+ /** 读取消费项目 pnpm-workspace.yaml 的 catalogs;缺失或无 catalogs 时回退到 Theme 内置版本数据 */
541
+ function loadPnpmWorkspace(rootDir: string) {
542
+ const yamlPath = resolve(rootDir, 'pnpm-workspace.yaml')
543
+ if (existsSync(yamlPath)) {
544
+ try {
545
+ const parsed = parseYaml(readFileSync(yamlPath, 'utf8')) as Record<string, unknown>
546
+ if (parsed?.catalogs && typeof parsed.catalogs === 'object') {
547
+ return { catalogs: parsed.catalogs as Record<string, Record<string, string>> }
548
+ }
549
+ }
550
+ catch {
551
+ // 解析失败时回退到 Theme 内置数据
318
552
  }
319
553
  }
320
- return { found: false, resolved: resolve(themeSrcDir, 'config/feeds.empty.ts') }
554
+ return { catalogs: fallbackPnpmWorkspace.catalogs as Record<string, Record<string, string>> }
321
555
  }
322
556
 
323
557
  async function loadJson(path: string, jiti: ReturnType<typeof createJiti>) {
@@ -328,3 +562,30 @@ async function loadJson(path: string, jiti: ReturnType<typeof createJiti>) {
328
562
  return undefined
329
563
  }
330
564
  }
565
+
566
+ function findConfigFile(rootDir: string) {
567
+ for (const name of ['clarity.config.ts', 'clarity.config.mjs', 'clarity.config.js']) {
568
+ if (existsSync(resolve(rootDir, name))) {
569
+ return name
570
+ }
571
+ }
572
+ return 'clarity.config.ts'
573
+ }
574
+
575
+ function findExisting(dir: string, names: string[]) {
576
+ for (const name of names) {
577
+ const path = resolve(dir, name)
578
+ if (existsSync(path)) {
579
+ return path
580
+ }
581
+ }
582
+ return undefined
583
+ }
584
+
585
+ function findFeedsFile(rootDir: string, themeSrcDir: string) {
586
+ const found = findExisting(rootDir, ['feeds.ts', 'feeds.mjs', 'feeds.js'])
587
+ if (found) {
588
+ return { found: true, resolved: found }
589
+ }
590
+ return { found: false, resolved: resolve(themeSrcDir, 'config/feeds.empty.ts') }
591
+ }
@@ -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>