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