@tnotesjs/core 0.2.1 → 0.2.2
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/config/ConfigManager.ts +137 -0
- package/config/constants.ts +121 -0
- package/config/index.ts +25 -0
- package/config/templates.ts +49 -0
- package/core/GitManager.ts +513 -0
- package/core/NoteIndexCache.ts +194 -0
- package/core/NoteManager.ts +407 -0
- package/core/ProcessManager.ts +180 -0
- package/core/ReadmeGenerator.ts +215 -0
- package/core/TocGenerator.ts +212 -0
- package/core/index.ts +11 -0
- package/package.json +5 -2
- package/services/file-watcher/configChangeHandler.ts +64 -0
- package/services/file-watcher/eventScheduler.ts +179 -0
- package/services/file-watcher/folderChangeHandler.ts +325 -0
- package/services/file-watcher/fsWatcherAdapter.ts +128 -0
- package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
- package/services/file-watcher/index.ts +7 -0
- package/services/file-watcher/internal.ts +79 -0
- package/services/file-watcher/readmeChangeHandler.ts +28 -0
- package/services/file-watcher/renameDetector.ts +120 -0
- package/services/file-watcher/service.ts +352 -0
- package/services/file-watcher/watchState.ts +194 -0
- package/services/git/index.ts +7 -0
- package/services/git/service.ts +114 -0
- package/services/index.ts +15 -0
- package/services/init-sub-repo/index.ts +2 -0
- package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
- package/services/init-sub-repo/initSubRepoLogic.ts +304 -0
- package/services/init-sub-repo/service.ts +101 -0
- package/services/note/index.ts +7 -0
- package/services/note/service.ts +362 -0
- package/services/readme/index.ts +7 -0
- package/services/readme/service.ts +761 -0
- package/services/timestamp/index.ts +7 -0
- package/services/timestamp/service.ts +465 -0
- package/services/toc/index.ts +5 -0
- package/services/toc/moveTocInside.test.ts +73 -0
- package/services/toc/service.ts +759 -0
- package/services/vitepress/index.ts +7 -0
- package/services/vitepress/service.ts +339 -0
- package/utils/errorHandler.ts +174 -0
- package/utils/file.ts +17 -0
- package/utils/genHierarchicalSidebar.ts +69 -0
- package/utils/generateAnchor.ts +24 -0
- package/utils/getChangedIds.ts +35 -0
- package/utils/index.ts +71 -0
- package/utils/logger.ts +231 -0
- package/utils/markdown.ts +75 -0
- package/utils/migrateReadmeToToc.test.ts +111 -0
- package/utils/migrateReadmeToToc.ts +135 -0
- package/utils/parseArgs.ts +90 -0
- package/utils/parseReadmeCompletedNotes.test.ts +90 -0
- package/utils/parseReadmeCompletedNotes.ts +108 -0
- package/utils/portUtils.ts +113 -0
- package/utils/readmeHelpers.ts +190 -0
- package/utils/runCommand.ts +29 -0
- package/utils/tocHelpers.test.ts +278 -0
- package/utils/tocHelpers.ts +855 -0
- package/utils/tocNodeId.test.ts +60 -0
- package/utils/tocNodeId.ts +97 -0
- package/utils/validators.ts +102 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services/git-service/service.ts
|
|
3
|
+
*
|
|
4
|
+
* Git 服务 - 封装 Git 操作相关的业务逻辑
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ROOT_DIR_PATH } from '../../config/constants'
|
|
8
|
+
import { GitManager } from '../../core'
|
|
9
|
+
import { logger } from '../../utils'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Git 推送选项
|
|
13
|
+
*/
|
|
14
|
+
interface PushOptions {
|
|
15
|
+
message?: string
|
|
16
|
+
branch?: string
|
|
17
|
+
force?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Git 拉取选项
|
|
22
|
+
*/
|
|
23
|
+
interface PullOptions {
|
|
24
|
+
branch?: string
|
|
25
|
+
rebase?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Git 服务类
|
|
30
|
+
*/
|
|
31
|
+
export class GitService {
|
|
32
|
+
private gitManager: GitManager
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
this.gitManager = new GitManager(ROOT_DIR_PATH)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 推送到远程仓库
|
|
40
|
+
* @param options - 推送选项
|
|
41
|
+
*/
|
|
42
|
+
async push(options: PushOptions = {}): Promise<void> {
|
|
43
|
+
const { message, branch, force = false } = options
|
|
44
|
+
|
|
45
|
+
logger.info('Pushing to remote repository...')
|
|
46
|
+
|
|
47
|
+
if (message) {
|
|
48
|
+
await this.gitManager.pushWithCommit(message, { force })
|
|
49
|
+
} else {
|
|
50
|
+
await this.gitManager.push({ setUpstream: !!branch, force })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
logger.info('Push completed successfully')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 从远程仓库拉取
|
|
58
|
+
* @param options - 拉取选项
|
|
59
|
+
*/
|
|
60
|
+
async pull(options: PullOptions = {}): Promise<void> {
|
|
61
|
+
const { rebase = false } = options
|
|
62
|
+
|
|
63
|
+
logger.info('Pulling from remote repository...')
|
|
64
|
+
|
|
65
|
+
await this.gitManager.pull({ rebase })
|
|
66
|
+
|
|
67
|
+
logger.info('Pull completed successfully')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 同步本地和远程仓库(先拉取后推送)
|
|
72
|
+
* @param commitMessage - 可选的提交信息
|
|
73
|
+
*/
|
|
74
|
+
async sync(commitMessage?: string): Promise<void> {
|
|
75
|
+
logger.info('Syncing with remote repository...')
|
|
76
|
+
|
|
77
|
+
await this.gitManager.sync({ commitMessage })
|
|
78
|
+
|
|
79
|
+
logger.info('Sync completed successfully')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 检查是否有未提交的更改
|
|
84
|
+
* @returns 是否有未提交的更改
|
|
85
|
+
*/
|
|
86
|
+
async hasChanges(): Promise<boolean> {
|
|
87
|
+
const status = await this.gitManager.getStatus()
|
|
88
|
+
return status.hasChanges
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 生成自动提交信息
|
|
93
|
+
* @returns 自动生成的提交信息
|
|
94
|
+
*/
|
|
95
|
+
generateCommitMessage(): string {
|
|
96
|
+
const date = new Date().toISOString().split('T')[0]
|
|
97
|
+
const time = new Date().toTimeString().split(' ')[0]
|
|
98
|
+
return `📝 Update notes - ${date} ${time}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 快速提交并推送(使用自动生成的提交信息)
|
|
103
|
+
* @param options - 推送选项
|
|
104
|
+
*/
|
|
105
|
+
async quickPush(options: { force?: boolean; skipCheck?: boolean } = {}): Promise<void> {
|
|
106
|
+
if (!options.skipCheck && !(await this.hasChanges())) {
|
|
107
|
+
logger.info('No changes to commit')
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const message = this.generateCommitMessage()
|
|
112
|
+
await this.push({ message, force: options.force })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services/index.ts
|
|
3
|
+
*
|
|
4
|
+
* Services 层统一 IO 层
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { FileWatcherService } from './file-watcher'
|
|
8
|
+
export { GitService } from './git'
|
|
9
|
+
export { NoteService } from './note'
|
|
10
|
+
export { ReadmeService } from './readme'
|
|
11
|
+
export { TocService } from './toc'
|
|
12
|
+
export { TimestampService } from './timestamp'
|
|
13
|
+
export { VitepressService } from './vitepress'
|
|
14
|
+
export { InitSubRepoService } from './init-sub-repo'
|
|
15
|
+
export * from './init-sub-repo/initSubRepoLogic'
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
2
|
+
import { tmpdir } from 'os'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { describe, expect, it, afterEach } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
buildInitContext,
|
|
8
|
+
buildRepoName,
|
|
9
|
+
buildPackageJsonTemplate,
|
|
10
|
+
defaultDisplayName,
|
|
11
|
+
isAlreadyInitialized,
|
|
12
|
+
mergePackageJson,
|
|
13
|
+
renderTocMd,
|
|
14
|
+
resolvePackageJsonMerge,
|
|
15
|
+
validatePort,
|
|
16
|
+
validateTopic,
|
|
17
|
+
} from './initSubRepoLogic'
|
|
18
|
+
import { InitSubRepoService } from './service'
|
|
19
|
+
|
|
20
|
+
describe('initSubRepoLogic', () => {
|
|
21
|
+
const baseInput = {
|
|
22
|
+
topic: 'react',
|
|
23
|
+
displayName: 'React 笔记',
|
|
24
|
+
port: 8000,
|
|
25
|
+
description: '这是一个基于 tnotesjs/core 搭建的 TNotes 知识库。',
|
|
26
|
+
repoUuid: 'repo-uuid',
|
|
27
|
+
noteUuid: 'note-uuid',
|
|
28
|
+
coreVersion: '0.1.28',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
it('buildRepoName 前缀 TNotes.', () => {
|
|
32
|
+
expect(buildRepoName('react')).toBe('TNotes.react')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('defaultDisplayName 首字母大写', () => {
|
|
36
|
+
expect(defaultDisplayName('react')).toBe('React 笔记')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('validateTopic 校验 slug', () => {
|
|
40
|
+
expect(validateTopic('react')).toBeNull()
|
|
41
|
+
expect(validateTopic('React')).not.toBeNull()
|
|
42
|
+
expect(validateTopic('')).not.toBeNull()
|
|
43
|
+
expect(validateTopic('1bad')).not.toBeNull()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('validatePort 默认值与范围', () => {
|
|
47
|
+
expect(validatePort('')).toEqual({ port: 8000 })
|
|
48
|
+
expect(validatePort('9000')).toEqual({ port: 9000 })
|
|
49
|
+
expect(validatePort('abc').error).toBeTruthy()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('renderTocMd 单行格式', () => {
|
|
53
|
+
const ctx = buildInitContext(baseInput)
|
|
54
|
+
expect(renderTocMd(ctx)).toBe('- [ ] 0001. TNotes.react\n')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('mergePackageJson 保留额外字段', () => {
|
|
58
|
+
const template = buildPackageJsonTemplate('0.1.28')
|
|
59
|
+
const merged = mergePackageJson(
|
|
60
|
+
{ name: 'my-repo', license: 'MIT', scripts: { old: 'x' } },
|
|
61
|
+
template,
|
|
62
|
+
)
|
|
63
|
+
expect(merged.name).toBe('my-repo')
|
|
64
|
+
expect(merged.license).toBe('MIT')
|
|
65
|
+
expect(merged.scripts).toEqual(template.scripts)
|
|
66
|
+
expect(merged.dependencies).toEqual(template.dependencies)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('resolvePackageJsonMerge 新建与合并', () => {
|
|
70
|
+
const dir = mkdtempSync(join(tmpdir(), 'init-sub-repo-'))
|
|
71
|
+
const pkgPath = join(dir, 'package.json')
|
|
72
|
+
const template = buildPackageJsonTemplate('0.1.28')
|
|
73
|
+
|
|
74
|
+
const created = resolvePackageJsonMerge(pkgPath, template)
|
|
75
|
+
expect(created.action).toBe('created')
|
|
76
|
+
|
|
77
|
+
writeFileSync(pkgPath, JSON.stringify({ name: 'keep-me' }), 'utf-8')
|
|
78
|
+
const updated = resolvePackageJsonMerge(pkgPath, template)
|
|
79
|
+
expect(updated.action).toBe('updated')
|
|
80
|
+
const parsed = JSON.parse(updated.content) as { name: string }
|
|
81
|
+
expect(parsed.name).toBe('keep-me')
|
|
82
|
+
|
|
83
|
+
rmSync(dir, { recursive: true, force: true })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('isAlreadyInitialized 检测 .tnotes.json', () => {
|
|
87
|
+
const dir = mkdtempSync(join(tmpdir(), 'init-sub-repo-'))
|
|
88
|
+
expect(isAlreadyInitialized(dir)).toBe(false)
|
|
89
|
+
|
|
90
|
+
writeFileSync(join(dir, '.tnotes.json'), '{}', 'utf-8')
|
|
91
|
+
expect(isAlreadyInitialized(dir)).toBe(true)
|
|
92
|
+
|
|
93
|
+
rmSync(dir, { recursive: true, force: true })
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('InitSubRepoService', () => {
|
|
98
|
+
let tempRoot = ''
|
|
99
|
+
const templateRoot = join(process.cwd(), 'templates', 'sub-repo')
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
if (tempRoot) {
|
|
103
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
104
|
+
tempRoot = ''
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('writeScaffold 创建文件并跳过已存在项', () => {
|
|
109
|
+
tempRoot = mkdtempSync(join(tmpdir(), 'init-sub-repo-svc-'))
|
|
110
|
+
writeFileSync(join(tempRoot, 'README.md'), '# existing', 'utf-8')
|
|
111
|
+
|
|
112
|
+
const service = new InitSubRepoService(tempRoot, templateRoot)
|
|
113
|
+
const result = service.writeScaffold({
|
|
114
|
+
topic: 'demo',
|
|
115
|
+
displayName: 'Demo 笔记',
|
|
116
|
+
port: 8000,
|
|
117
|
+
description: 'desc',
|
|
118
|
+
repoUuid: 'repo-id',
|
|
119
|
+
noteUuid: 'note-id',
|
|
120
|
+
coreVersion: '0.1.28',
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(result.created).toContain('.tnotes.json')
|
|
124
|
+
expect(result.created).toContain('TOC.md')
|
|
125
|
+
expect(result.skipped).toContain('README.md')
|
|
126
|
+
expect(existsSync(join(tempRoot, '.tnotes.json'))).toBe(true)
|
|
127
|
+
|
|
128
|
+
const config = JSON.parse(
|
|
129
|
+
readFileSync(join(tempRoot, '.tnotes.json'), 'utf-8'),
|
|
130
|
+
) as { repoName: string; port: number }
|
|
131
|
+
expect(config.repoName).toBe('TNotes.demo')
|
|
132
|
+
expect(config.port).toBe(8000)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('writeScaffold 合并已有 package.json', () => {
|
|
136
|
+
tempRoot = mkdtempSync(join(tmpdir(), 'init-sub-repo-svc-'))
|
|
137
|
+
writeFileSync(
|
|
138
|
+
join(tempRoot, 'package.json'),
|
|
139
|
+
JSON.stringify({ name: 'custom-name', private: true }),
|
|
140
|
+
'utf-8',
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const service = new InitSubRepoService(tempRoot, templateRoot)
|
|
144
|
+
const result = service.writeScaffold({
|
|
145
|
+
topic: 'vue',
|
|
146
|
+
displayName: 'Vue 笔记',
|
|
147
|
+
port: 8100,
|
|
148
|
+
description: 'desc',
|
|
149
|
+
repoUuid: 'repo-id',
|
|
150
|
+
noteUuid: 'note-id',
|
|
151
|
+
coreVersion: '0.1.28',
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
expect(result.updated).toContain('package.json')
|
|
155
|
+
const pkg = JSON.parse(
|
|
156
|
+
readFileSync(join(tempRoot, 'package.json'), 'utf-8'),
|
|
157
|
+
) as { name: string; private: boolean; scripts: Record<string, string> }
|
|
158
|
+
expect(pkg.name).toBe('custom-name')
|
|
159
|
+
expect(pkg.private).toBe(true)
|
|
160
|
+
expect(pkg.scripts['tn:dev']).toBe('tnotes --dev')
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services/init-sub-repo/initSubRepoLogic.ts
|
|
3
|
+
*
|
|
4
|
+
* 子知识库初始化纯函数(便于测试)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from 'fs'
|
|
8
|
+
import { join, resolve } from 'path'
|
|
9
|
+
|
|
10
|
+
import { getDefaultConfig } from '../../config/defaultConfig'
|
|
11
|
+
import {
|
|
12
|
+
generateNoteTitle,
|
|
13
|
+
getNewNoteReadmeBody,
|
|
14
|
+
} from '../../config/templates'
|
|
15
|
+
|
|
16
|
+
import type { NoteConfig, TNotesConfig } from '../../types'
|
|
17
|
+
|
|
18
|
+
export const TOPIC_PATTERN = /^[a-z][a-z0-9-]*$/
|
|
19
|
+
export const DEFAULT_PORT = 8000
|
|
20
|
+
export const DEFAULT_DESCRIPTION =
|
|
21
|
+
'这是一个基于 tnotesjs/core 搭建的 TNotes 知识库。'
|
|
22
|
+
export const GITHUB_ORG = 'tnotesjs'
|
|
23
|
+
export const TNOTES_ROOT_URL = 'https://tnotesjs.github.io/TNotes'
|
|
24
|
+
|
|
25
|
+
/** 静态模板文件(相对 templates/sub-repo/) */
|
|
26
|
+
export const STATIC_TEMPLATE_FILES = [
|
|
27
|
+
'.github/workflows/deploy.yml',
|
|
28
|
+
'.vitepress/theme/index.ts',
|
|
29
|
+
'.vitepress/config.mts',
|
|
30
|
+
'.vitepress/env.d.ts',
|
|
31
|
+
'.vscode/settings.json',
|
|
32
|
+
'.vscode/tnotes.code-snippets',
|
|
33
|
+
'public/logo.png',
|
|
34
|
+
'public/favicon.ico',
|
|
35
|
+
'.gitattributes',
|
|
36
|
+
'.gitignore',
|
|
37
|
+
'tsconfig.json',
|
|
38
|
+
] as const
|
|
39
|
+
|
|
40
|
+
export interface InitSubRepoInput {
|
|
41
|
+
topic: string
|
|
42
|
+
displayName: string
|
|
43
|
+
port: number
|
|
44
|
+
description: string
|
|
45
|
+
repoUuid: string
|
|
46
|
+
noteUuid: string
|
|
47
|
+
coreVersion: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface InitSubRepoContext extends InitSubRepoInput {
|
|
51
|
+
repoName: string
|
|
52
|
+
noteTitle: string
|
|
53
|
+
noteDirName: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PackageJsonTemplate {
|
|
57
|
+
type: 'module'
|
|
58
|
+
scripts: Record<string, string>
|
|
59
|
+
dependencies: Record<string, string>
|
|
60
|
+
devDependencies: Record<string, string>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface InitSubRepoWriteResult {
|
|
64
|
+
created: string[]
|
|
65
|
+
skipped: string[]
|
|
66
|
+
updated: string[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildRepoName(topic: string): string {
|
|
70
|
+
return `TNotes.${topic}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function defaultDisplayName(topic: string): string {
|
|
74
|
+
const label = topic.charAt(0).toUpperCase() + topic.slice(1)
|
|
75
|
+
return `${label} 笔记`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function validateTopic(topic: string): string | null {
|
|
79
|
+
const trimmed = topic.trim()
|
|
80
|
+
if (!trimmed) return 'topic 不能为空'
|
|
81
|
+
if (!TOPIC_PATTERN.test(trimmed)) {
|
|
82
|
+
return 'topic 须以小写字母开头,仅含小写字母、数字、连字符'
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validatePort(raw: string): { port: number; error?: string } {
|
|
88
|
+
const trimmed = raw.trim()
|
|
89
|
+
if (!trimmed) return { port: DEFAULT_PORT }
|
|
90
|
+
|
|
91
|
+
const port = Number.parseInt(trimmed, 10)
|
|
92
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
93
|
+
return { port: DEFAULT_PORT, error: '端口须为 1024–65535 之间的整数,已使用默认值 8000' }
|
|
94
|
+
}
|
|
95
|
+
return { port }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildInitContext(input: InitSubRepoInput): InitSubRepoContext {
|
|
99
|
+
const repoName = buildRepoName(input.topic)
|
|
100
|
+
const noteTitle = repoName
|
|
101
|
+
const noteDirName = `0001. ${noteTitle}`
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...input,
|
|
105
|
+
repoName,
|
|
106
|
+
noteTitle,
|
|
107
|
+
noteDirName,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildTNotesConfig(ctx: InitSubRepoContext): TNotesConfig {
|
|
112
|
+
const config = getDefaultConfig(ctx.repoName)
|
|
113
|
+
|
|
114
|
+
config.id = ctx.repoUuid
|
|
115
|
+
config.port = ctx.port
|
|
116
|
+
config.root_item = {
|
|
117
|
+
...config.root_item,
|
|
118
|
+
title: ctx.topic,
|
|
119
|
+
details: ctx.displayName,
|
|
120
|
+
link: `https://tnotesjs.github.io/${ctx.repoName}/`,
|
|
121
|
+
}
|
|
122
|
+
config.menuItems = [
|
|
123
|
+
{ text: '🏠 Home', link: '/' },
|
|
124
|
+
{ text: '⚙️ Settings', link: '/Settings' },
|
|
125
|
+
{ text: '📒 TNotes', link: TNOTES_ROOT_URL },
|
|
126
|
+
{
|
|
127
|
+
text: '📂 TNotes.yuque',
|
|
128
|
+
link: 'https://www.yuque.com/tdahuyou/tnotes.yuque',
|
|
129
|
+
},
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
return config
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function buildNoteConfig(noteUuid: string): NoteConfig {
|
|
136
|
+
return {
|
|
137
|
+
id: noteUuid,
|
|
138
|
+
bilibili: [],
|
|
139
|
+
tnotes: [],
|
|
140
|
+
yuque: [],
|
|
141
|
+
done: false,
|
|
142
|
+
enableDiscussions: false,
|
|
143
|
+
description: '',
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function buildRepoNotesUrl(repoName: string): string {
|
|
148
|
+
return `https://github.com/${GITHUB_ORG}/${repoName}/tree/main/notes`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function renderFirstNoteReadme(ctx: InitSubRepoContext): string {
|
|
152
|
+
const noteTitle = generateNoteTitle(
|
|
153
|
+
'0001',
|
|
154
|
+
ctx.noteTitle,
|
|
155
|
+
buildRepoNotesUrl(ctx.repoName),
|
|
156
|
+
)
|
|
157
|
+
return `${noteTitle}\n${getNewNoteReadmeBody()}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function renderIndexMd(ctx: InitSubRepoContext): string {
|
|
161
|
+
return `---
|
|
162
|
+
layout: home
|
|
163
|
+
|
|
164
|
+
hero:
|
|
165
|
+
name: '${ctx.displayName}'
|
|
166
|
+
image:
|
|
167
|
+
src: /logo.png
|
|
168
|
+
alt: TNotes logo
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
<SidebarCard pending />
|
|
172
|
+
`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function renderTocMd(ctx: InitSubRepoContext): string {
|
|
176
|
+
return `- [ ] 0001. ${ctx.noteTitle}\n`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function renderRootReadme(ctx: InitSubRepoContext): string {
|
|
180
|
+
return `# ${ctx.repoName}\n\n${ctx.description}\n`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function buildPackageJsonTemplate(
|
|
184
|
+
coreVersion: string,
|
|
185
|
+
): PackageJsonTemplate {
|
|
186
|
+
return {
|
|
187
|
+
type: 'module',
|
|
188
|
+
scripts: {
|
|
189
|
+
'tn:build': 'tnotes --build',
|
|
190
|
+
'tn:create-notes': 'tnotes --create-notes',
|
|
191
|
+
'tn:dev': 'tnotes --dev',
|
|
192
|
+
'tn:fix-timestamps': 'tnotes --fix-timestamps',
|
|
193
|
+
'tn:help': 'tnotes --help',
|
|
194
|
+
'tn:init-sub-repo': 'tnotes --init-sub-repo',
|
|
195
|
+
'tn:preview': 'tnotes --preview',
|
|
196
|
+
'tn:pull': 'tnotes --pull',
|
|
197
|
+
'tn:push': 'tnotes --push',
|
|
198
|
+
'tn:update': 'tnotes --update',
|
|
199
|
+
'tn:update-completed-count': 'tnotes --update-completed-count',
|
|
200
|
+
},
|
|
201
|
+
dependencies: {
|
|
202
|
+
'@tnotesjs/core': `^${coreVersion}`,
|
|
203
|
+
},
|
|
204
|
+
devDependencies: {
|
|
205
|
+
vite: '^7.3.1',
|
|
206
|
+
vitepress: '^1.6.3',
|
|
207
|
+
vue: '^3.5.27',
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function mergePackageJson(
|
|
213
|
+
existing: Record<string, unknown>,
|
|
214
|
+
template: PackageJsonTemplate,
|
|
215
|
+
): Record<string, unknown> {
|
|
216
|
+
return {
|
|
217
|
+
...existing,
|
|
218
|
+
type: template.type,
|
|
219
|
+
scripts: template.scripts,
|
|
220
|
+
dependencies: template.dependencies,
|
|
221
|
+
devDependencies: template.devDependencies,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface DynamicFileSpec {
|
|
226
|
+
relativePath: string
|
|
227
|
+
content: string
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function buildDynamicFiles(ctx: InitSubRepoContext): DynamicFileSpec[] {
|
|
231
|
+
return [
|
|
232
|
+
{
|
|
233
|
+
relativePath: '.tnotes.json',
|
|
234
|
+
content: `${JSON.stringify(buildTNotesConfig(ctx), null, 2)}\n`,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
relativePath: 'package.json',
|
|
238
|
+
content: `${JSON.stringify(buildPackageJsonTemplate(ctx.coreVersion), null, 2)}\n`,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
relativePath: 'index.md',
|
|
242
|
+
content: renderIndexMd(ctx),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
relativePath: 'TOC.md',
|
|
246
|
+
content: renderTocMd(ctx),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
relativePath: 'README.md',
|
|
250
|
+
content: renderRootReadme(ctx),
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
relativePath: join('notes', ctx.noteDirName, 'README.md'),
|
|
254
|
+
content: renderFirstNoteReadme(ctx),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
relativePath: join('notes', ctx.noteDirName, '.tnotes.json'),
|
|
258
|
+
content: `${JSON.stringify(buildNoteConfig(ctx.noteUuid), null, 2)}\n`,
|
|
259
|
+
},
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function isAlreadyInitialized(rootPath: string): boolean {
|
|
264
|
+
return existsSync(resolve(rootPath, '.tnotes.json'))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function getInitializedConfigPath(rootPath: string): string {
|
|
268
|
+
return resolve(rootPath, '.tnotes.json')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function resolvePackageJsonMerge(
|
|
272
|
+
targetPath: string,
|
|
273
|
+
template: PackageJsonTemplate,
|
|
274
|
+
): { content: string; action: 'created' | 'updated' } {
|
|
275
|
+
if (!existsSync(targetPath)) {
|
|
276
|
+
return {
|
|
277
|
+
content: `${JSON.stringify(template, null, 2)}\n`,
|
|
278
|
+
action: 'created',
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const existing = JSON.parse(readFileSync(targetPath, 'utf-8')) as Record<
|
|
283
|
+
string,
|
|
284
|
+
unknown
|
|
285
|
+
>
|
|
286
|
+
const merged = mergePackageJson(existing, template)
|
|
287
|
+
return {
|
|
288
|
+
content: `${JSON.stringify(merged, null, 2)}\n`,
|
|
289
|
+
action: 'updated',
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function buildManualSteps(ctx: InitSubRepoContext): string[] {
|
|
294
|
+
return [
|
|
295
|
+
'pnpm install — package.json 已更新,需重装依赖',
|
|
296
|
+
`创建 GitHub 仓库 https://github.com/${GITHUB_ORG}/${ctx.repoName}(若尚未创建)`,
|
|
297
|
+
'git init / git remote add origin(若尚未关联远程)',
|
|
298
|
+
'GitHub Pages:Settings → Build and deployment → Source 选 GitHub Actions',
|
|
299
|
+
'Repository secret:添加 TNOTES_DISPATCH_TOKEN(供 deploy.yml notify job 回调根库)',
|
|
300
|
+
`主题图标(可选):确认 CDN 存在 icon--${ctx.topic}.svg,否则替换 .tnotes.json 中 root_item.icon.src`,
|
|
301
|
+
'本地验证:pnpm tn:dev',
|
|
302
|
+
'首次发布:push 到 main 触发 deploy workflow',
|
|
303
|
+
]
|
|
304
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services/init-sub-repo/service.ts
|
|
3
|
+
*
|
|
4
|
+
* 子知识库初始化服务
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
8
|
+
import { dirname, join, resolve } from 'path'
|
|
9
|
+
import { fileURLToPath } from 'url'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
buildDynamicFiles,
|
|
13
|
+
buildInitContext,
|
|
14
|
+
buildPackageJsonTemplate,
|
|
15
|
+
resolvePackageJsonMerge,
|
|
16
|
+
STATIC_TEMPLATE_FILES,
|
|
17
|
+
} from './initSubRepoLogic'
|
|
18
|
+
|
|
19
|
+
import type { InitSubRepoInput, InitSubRepoWriteResult } from './initSubRepoLogic'
|
|
20
|
+
|
|
21
|
+
export function getCorePackageRoot(fromModuleUrl: string): string {
|
|
22
|
+
let dir = dirname(fileURLToPath(fromModuleUrl))
|
|
23
|
+
|
|
24
|
+
while (dir !== dirname(dir)) {
|
|
25
|
+
const pkgPath = join(dir, 'package.json')
|
|
26
|
+
if (existsSync(pkgPath)) {
|
|
27
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { name?: string }
|
|
28
|
+
if (pkg.name === '@tnotesjs/core') return dir
|
|
29
|
+
}
|
|
30
|
+
dir = dirname(dir)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error('无法定位 @tnotesjs/core 包根目录')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getTemplateRoot(fromModuleUrl: string): string {
|
|
37
|
+
return join(getCorePackageRoot(fromModuleUrl), 'templates', 'sub-repo')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class InitSubRepoService {
|
|
41
|
+
private rootPath: string
|
|
42
|
+
private templateRoot: string
|
|
43
|
+
|
|
44
|
+
constructor(rootPath: string, templateRoot: string) {
|
|
45
|
+
this.rootPath = rootPath
|
|
46
|
+
this.templateRoot = templateRoot
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static fromModuleUrl(moduleUrl: string, rootPath = process.cwd()): InitSubRepoService {
|
|
50
|
+
return new InitSubRepoService(rootPath, getTemplateRoot(moduleUrl))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
writeScaffold(input: InitSubRepoInput): InitSubRepoWriteResult {
|
|
54
|
+
const ctx = buildInitContext(input)
|
|
55
|
+
const created: string[] = []
|
|
56
|
+
const skipped: string[] = []
|
|
57
|
+
const updated: string[] = []
|
|
58
|
+
|
|
59
|
+
for (const relativePath of STATIC_TEMPLATE_FILES) {
|
|
60
|
+
const targetPath = resolve(this.rootPath, relativePath)
|
|
61
|
+
if (existsSync(targetPath)) {
|
|
62
|
+
skipped.push(relativePath)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sourcePath = join(this.templateRoot, relativePath)
|
|
67
|
+
mkdirSync(dirname(targetPath), { recursive: true })
|
|
68
|
+
copyFileSync(sourcePath, targetPath)
|
|
69
|
+
created.push(relativePath)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const packageTemplate = buildPackageJsonTemplate(input.coreVersion)
|
|
73
|
+
|
|
74
|
+
for (const file of buildDynamicFiles(ctx)) {
|
|
75
|
+
const targetPath = resolve(this.rootPath, file.relativePath)
|
|
76
|
+
|
|
77
|
+
if (file.relativePath === 'package.json') {
|
|
78
|
+
const { content, action } = resolvePackageJsonMerge(
|
|
79
|
+
targetPath,
|
|
80
|
+
packageTemplate,
|
|
81
|
+
)
|
|
82
|
+
mkdirSync(dirname(targetPath), { recursive: true })
|
|
83
|
+
writeFileSync(targetPath, content, 'utf-8')
|
|
84
|
+
if (action === 'created') created.push(file.relativePath)
|
|
85
|
+
else updated.push(file.relativePath)
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (existsSync(targetPath)) {
|
|
90
|
+
skipped.push(file.relativePath)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
mkdirSync(dirname(targetPath), { recursive: true })
|
|
95
|
+
writeFileSync(targetPath, file.content, 'utf-8')
|
|
96
|
+
created.push(file.relativePath)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { created, skipped, updated }
|
|
100
|
+
}
|
|
101
|
+
}
|