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.
- package/README.md +15 -5
- package/README.zh-CN.md +15 -5
- package/nuxt.config.ts +19 -1
- package/package.json +14 -2
- package/src/app.config.ts +8 -104
- package/src/assets/icons/zhilu.svg +8 -0
- package/src/blog.config.ts +71 -0
- package/src/components/blog/BlogFooter.vue +3 -3
- package/src/components/blog/BlogHeader.global.vue +9 -9
- package/src/components/blog/BlogSidebar.vue +3 -3
- package/src/components/blog/ThemeToggle.vue +2 -2
- package/src/components/content/Alert.vue +2 -2
- package/src/components/content/FeedCard.vue +2 -2
- package/src/components/content/Mermaid.vue +1 -1
- package/src/components/content/ProseCode.vue +4 -32
- package/src/components/content/ProsePre.vue +2 -2
- package/src/components/content/ProseTable.vue +1 -1
- package/src/components/popover/Search.vue +3 -3
- package/src/components/post/Comment.vue +3 -15
- package/src/components/post/Excerpt.vue +3 -3
- package/src/components/post/OrderToggle.vue +3 -3
- package/src/components/post/PostFooter.vue +3 -4
- package/src/components/post/PostHeader.vue +4 -4
- package/src/components/post/PostSurround.vue +2 -2
- package/src/components/post/Slide.vue +2 -2
- package/src/components/util/Img.vue +1 -1
- package/src/components/widget/BlogLog.vue +9 -19
- package/src/components/widget/BlogStats.vue +4 -4
- package/src/components/widget/BlogTech.vue +16 -17
- package/src/components/widget/CommGroup.vue +43 -0
- package/src/composables/useArticle.ts +9 -9
- package/src/composables/usePagination.ts +2 -2
- package/src/composables/useWidgets.ts +2 -0
- package/src/config/app.ts +40 -11
- package/src/config/content.ts +6 -0
- package/src/config/feed.ts +2 -42
- package/src/config/index.ts +2 -2
- package/src/config/pnpm-workspace.ts +19 -0
- package/src/config/public.ts +3 -61
- package/src/config/ui.ts +111 -0
- package/src/modules/anti-mirror/index.ts +38 -0
- package/src/modules/{clarity-config/anti-mirror-client.ts → anti-mirror/runtime/client.ts} +3 -3
- package/src/modules/clarity-config/index.ts +354 -93
- package/src/modules/clarity-source-layout/index.ts +13 -0
- package/src/pages/archive.vue +4 -4
- package/src/pages/index.vue +4 -3
- package/src/pages/link.vue +5 -5
- package/src/pages/preview.vue +2 -2
- package/src/server/api/stats.get.ts +9 -19
- package/src/server/routes/atom.xml.get.ts +48 -50
- package/src/server/routes/subscriptions.opml.get.ts +14 -31
- package/src/server/utils/clarity.ts +1 -1
- package/src/shared/utils/time.ts +13 -18
- package/src/types/article.ts +2 -2
- package/src/types/feed.ts +35 -1
- package/src/types/upstream-compat.d.ts +17 -0
- package/src/types/yaml-modules.d.ts +26 -0
- package/src/shared/types/content.ts +0 -30
- package/src/shared/utils/clarity.ts +0 -34
- package/src/types/modal.ts +0 -16
- /package/src/assets/icons/{solar-rewind-back-bold-duotone.svg → SolarRewindBackBoldDuotone.svg} +0 -0
- /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
|
|
16
|
-
import {
|
|
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
|
|
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
|
-
/**
|
|
27
|
-
const
|
|
25
|
+
/** Windows 路径比较需要大小写不敏感(vite 解析产物盘符大小写不定) */
|
|
26
|
+
const IS_WINDOWS = platform === 'win32'
|
|
28
27
|
|
|
29
|
-
/** app/app.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
|
-
|
|
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
|
|
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 {
|
|
91
|
+
`import type { ClarityAppConfigArticle, ClarityUiConfig } from '${appConfigTypePath}'`,
|
|
72
92
|
'',
|
|
73
93
|
'declare module \'@nuxt/schema\' {',
|
|
74
|
-
' // 读取侧:
|
|
94
|
+
' // 读取侧:UI 扁平键补充完整类型(0.2.0 起不再提供 clarity 嵌套键),',
|
|
95
|
+
' // 使上游组件在 JSON 字面量(空数组 / 宽化字符串)下仍获得正确类型。',
|
|
96
|
+
' // article 只声明 categories(Record 索引),order / types 保留 Resolved 字面量。',
|
|
75
97
|
' interface CustomAppConfig {',
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
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
|
-
// ----
|
|
114
|
-
|
|
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
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
//
|
|
137
|
-
//
|
|
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
|
-
// ----
|
|
218
|
-
//
|
|
410
|
+
// ---- features.antiMirror 已废弃 ----
|
|
411
|
+
// 上游 modules/anti-mirror 由 Nuxt 自动加载(Layer 的 src/modules 同样生效),
|
|
412
|
+
// 黑名单与跳转目标与上游保持一致;配置项仅为 0.1.x 兼容保留。
|
|
219
413
|
if (features.antiMirror !== false) {
|
|
220
|
-
|
|
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
|
-
/**
|
|
255
|
-
async function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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 {
|
|
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`.
|
package/src/pages/archive.vue
CHANGED
|
@@ -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
|
|
7
|
+
const appConfig = useAppConfig()
|
|
8
8
|
useSeoMeta({
|
|
9
9
|
title: '归档',
|
|
10
|
-
description: `${
|
|
10
|
+
description: `${appConfig.title}的所有文章归档。`,
|
|
11
11
|
})
|
|
12
|
-
const birthYear = computed(() =>
|
|
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(
|
|
39
|
+
return toZonedTemporal(article[sortOrder.value] as string).year.toString()
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
42
|
return ''
|
package/src/pages/index.vue
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { orderBy } from 'es-toolkit/array'
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const appConfig = useAppConfig()
|
|
5
5
|
useSeoMeta({
|
|
6
|
-
description:
|
|
7
|
-
ogImage:
|
|
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" />
|
package/src/pages/link.vue
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import { myFeed } from '~~/blog.config'
|
|
3
|
+
import feeds from '~/feeds'
|
|
3
4
|
|
|
4
|
-
const
|
|
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: `${
|
|
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="
|
|
36
|
+
:shuffle="appConfig.link.randomInGroup"
|
|
37
37
|
/>
|
|
38
38
|
|
|
39
39
|
<Tab :tabs="['我的博客信息', '申请友链']" center>
|