返回 oh-my-ppt
style-initializer.ts
根目录 / src / main / styles / style-initializer.ts
1 import fs from 'node:fs'
2 import path from 'node:path'
3 import {
4 atomicCopyDirectory,
5 listStylePackageDirectories,
6 readStylePackage,
7 type StylePackageJson
8 } from './style-package'
9 import { ensureInstalledStylesPath } from './style-paths'
10
11 export interface StyleInitializerLogger {
12 info?: (message: string, meta?: Record<string, unknown>) => void
13 warn?: (message: string, meta?: Record<string, unknown>) => void
14 error?: (message: string, meta?: Record<string, unknown>) => void
15 }
16
17 export interface InitializeStylesResult {
18 bundledCount: number
19 copiedCount: number
20 skippedCount: number
21 failedCount: number
22 }
23
24 interface SystemReleaseManifest {
25 version: string
26 time: string
27 author: string
28 }
29
30 export async function initializeStyles(options: {
31 bundledSourcePath: string
32 installedRootPath: string
33 logger?: StyleInitializerLogger
34 }): Promise<InitializeStylesResult> {
35 const logger = options.logger
36 await ensureInstalledStylesPath(options.installedRootPath)
37 const systemPath = path.join(options.installedRootPath, 'system')
38 const bundledManifest = await readSystemReleaseManifest(options.bundledSourcePath, logger)
39 const installedManifest = await readSystemReleaseManifest(systemPath, logger)
40
41 if (
42 bundledManifest &&
43 installedManifest &&
44 bundledManifest.version === installedManifest.version &&
45 fs.existsSync(systemPath)
46 ) {
47 logger?.info?.('[styles] system styles are up to date', {
48 version: bundledManifest.version
49 })
50 return {
51 bundledCount: 0,
52 copiedCount: 0,
53 skippedCount: 1,
54 failedCount: 0
55 }
56 }
57
58 const bundledStyles = await readBundledStyles(options.bundledSourcePath, logger)
59 let copiedCount = 0
60 let failedCount = 0
61
62 for (const style of bundledStyles) {
63 try {
64 const destinationPath = path.join(systemPath, style.json.style)
65 await atomicCopyDirectory(style.path, destinationPath)
66 copiedCount += 1
67 logger?.info?.('[styles] installed bundled style', {
68 style: style.json.style,
69 version: style.json.version
70 })
71 } catch (error) {
72 failedCount += 1
73 logger?.error?.('[styles] failed to sync bundled style', {
74 style: style.json.style,
75 message: error instanceof Error ? error.message : String(error)
76 })
77 }
78 }
79
80 if (bundledManifest && failedCount === 0) {
81 await writeSystemReleaseManifest(systemPath, bundledManifest)
82 }
83
84 return {
85 bundledCount: bundledStyles.length,
86 copiedCount,
87 skippedCount: 0,
88 failedCount
89 }
90 }
91
92 async function readBundledStyles(
93 bundledSourcePath: string,
94 logger?: StyleInitializerLogger
95 ): Promise<Array<{ path: string; json: StylePackageJson }>> {
96 const entryNames = await listStylePackageDirectories(bundledSourcePath).catch((error) => {
97 logger?.warn?.('[styles] bundled styles source missing or unreadable', {
98 path: bundledSourcePath,
99 message: error instanceof Error ? error.message : String(error)
100 })
101 return []
102 })
103
104 const styles: Array<{ path: string; json: StylePackageJson }> = []
105 for (const styleName of entryNames) {
106 const stylePath = path.join(bundledSourcePath, styleName)
107 try {
108 const pkg = await readStylePackage(stylePath)
109 if (pkg.json.style !== styleName) {
110 logger?.warn?.('[styles] style key does not match directory', {
111 directory: styleName,
112 style: pkg.json.style
113 })
114 continue
115 }
116 if (pkg.json.source !== 'builtin') {
117 logger?.warn?.('[styles] bundled style source must be builtin', { path: stylePath })
118 continue
119 }
120 styles.push({ path: stylePath, json: pkg.json })
121 } catch (error) {
122 logger?.warn?.('[styles] invalid bundled style package', {
123 path: stylePath,
124 message: error instanceof Error ? error.message : String(error)
125 })
126 }
127 }
128 return styles
129 }
130
131 async function readSystemReleaseManifest(
132 rootPath: string,
133 logger?: StyleInitializerLogger
134 ): Promise<SystemReleaseManifest | null> {
135 const manifestPath = path.join(rootPath, 'manifest.json')
136 try {
137 const raw = await fs.promises.readFile(manifestPath, 'utf8')
138 const parsed = JSON.parse(raw) as Partial<SystemReleaseManifest>
139 const version = String(parsed.version || '').trim()
140 const time = String(parsed.time || '').trim()
141 const author = String(parsed.author || '').trim()
142 if (!/^\d+\.\d+\.\d+$/.test(version)) {
143 throw new Error('Invalid system styles manifest version')
144 }
145 return { version, time, author }
146 } catch (error) {
147 const code = (error as NodeJS.ErrnoException).code
148 if (code !== 'ENOENT') {
149 logger?.warn?.('[styles] system styles manifest invalid', {
150 path: manifestPath,
151 message: error instanceof Error ? error.message : String(error)
152 })
153 }
154 return null
155 }
156 }
157
158 async function writeSystemReleaseManifest(
159 systemPath: string,
160 manifest: SystemReleaseManifest
161 ): Promise<void> {
162 await fs.promises.mkdir(systemPath, { recursive: true })
163 const manifestPath = path.join(systemPath, 'manifest.json')
164 const tmpPath = path.join(systemPath, `.manifest.json.tmp-${process.pid}-${Date.now()}`)
165 await fs.promises.writeFile(tmpPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8')
166 await fs.promises.rename(tmpPath, manifestPath)
167 }
168
168 lines TYPESCRIPT