@tnotesjs/core 0.2.0 → 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/dist/chunk-5QN44ES5.js +6050 -0
- package/dist/chunk-5Y5IYS6C.js +200 -0
- package/dist/cli/index.js +986 -0
- package/dist/index.js +8 -0
- package/dist/vitepress/config/index.js +1686 -0
- package/package.json +6 -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,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
|
+
}
|