@lincyaw/render-slides 0.0.0-stage → 1.0.0-rc.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/LICENSE +202 -0
- package/README.md +24 -2
- package/package.json +58 -3
- package/src/deck/style.css +215 -0
- package/src/node/html.ts +15 -0
- package/src/node/markdown.ts +20 -0
- package/src/node/presmium.ts +276 -0
- package/src/node/slidev.ts +550 -0
- package/theme/layouts/cover.vue +12 -0
- package/theme/layouts/default.vue +20 -0
- package/theme/package.json +24 -0
- package/theme/parts/SlideFooter.vue +28 -0
- package/theme/setup/mermaid.ts +9 -0
- package/theme/setup/shiki.ts +36 -0
- package/theme/setup/vite-plugins.ts +25 -0
- package/theme/styles/book.css +282 -0
- package/theme/styles/index.ts +2 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import type { ChildProcess } from 'node:child_process'
|
|
3
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
4
|
+
import { copyFile, mkdir, readdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
8
|
+
import { errorMessage, irBlocks, isRecord, nodeMdast, previewFrameAncestors, PREVIEW_REVISION } from '@lincyaw/kernel'
|
|
9
|
+
import type { IRDocument, LocalAsset, MdastNode, PresmiumNodePlugin, RenderContext } from '@lincyaw/kernel'
|
|
10
|
+
import { copyPublished, FONT_LICENSE, fontLicense, optimizeDeps, TYPESCRIPT_IMPORT } from '@lincyaw/kernel/node'
|
|
11
|
+
import { addonDirs, hasExtension, previewDocument, slidevMarkdown } from './slidev.ts'
|
|
12
|
+
import type { SlidevDeck } from './slidev.ts'
|
|
13
|
+
|
|
14
|
+
export const SLIDEV_DIR = '.presmium/slides'
|
|
15
|
+
|
|
16
|
+
const ADDON_CORE = '@lincyaw/slidev-addon-core'
|
|
17
|
+
const PREVIEW_FILE = 'preview.json'
|
|
18
|
+
|
|
19
|
+
const STYLE = fileURLToPath(new URL('../deck/style.css', import.meta.url))
|
|
20
|
+
const PACKAGE_DIR = fileURLToPath(new URL('../..', import.meta.url))
|
|
21
|
+
const require = createRequire(import.meta.url)
|
|
22
|
+
const { version } = require('../../package.json') as { version: string }
|
|
23
|
+
|
|
24
|
+
const plugin: PresmiumNodePlugin = {
|
|
25
|
+
id: 'presmium.slides',
|
|
26
|
+
version,
|
|
27
|
+
kernel: '^1.0.0',
|
|
28
|
+
renderer: {
|
|
29
|
+
id: 'slides',
|
|
30
|
+
site: true,
|
|
31
|
+
support: (node, extensions) => node.type === 'component' && !node.plugin && !hasExtension(node, extensions) ? 'degraded' : 'full',
|
|
32
|
+
async prepare(doc, ctx) {
|
|
33
|
+
await writeDeck(ctx.workDir, ctx, slidevMarkdown(doc, ctx, ctx.workDir), doc)
|
|
34
|
+
},
|
|
35
|
+
async build(doc, ctx) {
|
|
36
|
+
const dir = ctx.workDir
|
|
37
|
+
await writeDeck(dir, ctx, slidevMarkdown(doc, ctx, dir), doc)
|
|
38
|
+
await rm(ctx.outDir, { recursive: true, force: true })
|
|
39
|
+
await slidev(['build', 'slides.md', '--out', ctx.outDir, ...base(ctx)], dir).catch((error: unknown) => {
|
|
40
|
+
throw located(error, doc, ctx)
|
|
41
|
+
})
|
|
42
|
+
ctx.report({
|
|
43
|
+
checkId: 'presmium.slides',
|
|
44
|
+
severity: 'info',
|
|
45
|
+
message: ctx.base === '/'
|
|
46
|
+
? `Slidev deck in ${SLIDEV_DIR}/slides.md; export it with \`slidev export ${SLIDEV_DIR}/slides.md\``
|
|
47
|
+
: `Slidev deck in ${SLIDEV_DIR}/slides.md, written for the base ${ctx.base}; \`slidev export\` serves it at the root, where the local files it links are not found: export with \`presmium-export\` (@lincyaw/slidev-addon-publish)`,
|
|
48
|
+
})
|
|
49
|
+
return { outDir: ctx.outDir, skipped: [] }
|
|
50
|
+
},
|
|
51
|
+
async dev(doc, ctx) {
|
|
52
|
+
const dir = ctx.workDir
|
|
53
|
+
await writeDeck(dir, ctx, slidevMarkdown(doc, ctx, dir), doc)
|
|
54
|
+
await writeRevision(dir, ctx, doc)
|
|
55
|
+
const port = Number(ctx.config.port)
|
|
56
|
+
const server = await slidevDev(dir, [...Number.isInteger(port) && port > 0 ? ['--port', String(port)] : [], ...base(ctx)])
|
|
57
|
+
return {
|
|
58
|
+
url: server.url,
|
|
59
|
+
async update(next) {
|
|
60
|
+
const deck = slidevMarkdown(next, ctx, dir)
|
|
61
|
+
await publish(dir, deck.assets)
|
|
62
|
+
const file = join(dir, 'slides.md')
|
|
63
|
+
if (await readFile(file, 'utf8').catch(() => '') !== deck.markdown) await writeFile(file, deck.markdown)
|
|
64
|
+
await writeRevision(dir, ctx, next)
|
|
65
|
+
},
|
|
66
|
+
close: server.close,
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default plugin
|
|
73
|
+
|
|
74
|
+
const hasHtml = (node: MdastNode): boolean => node.type === 'html' || (node.children ?? []).some(hasHtml)
|
|
75
|
+
|
|
76
|
+
function located(error: unknown, doc: IRDocument, ctx: RenderContext): unknown {
|
|
77
|
+
const output = error instanceof SlidevError ? error.output : errorMessage(error)
|
|
78
|
+
const at = /slides\.md__slidev_(\d+)\.md(?::\d+(?::\d+)?)?\s*\n(?:\w*Error: )?([^\n]+)/.exec(output)
|
|
79
|
+
const section = at && doc.sections[Number(at[1]) - 1]
|
|
80
|
+
if (!at || !section) return error
|
|
81
|
+
const reason = at[2]!.replace(/^\w*Error: /, '').trim()
|
|
82
|
+
const raw = [...irBlocks(section.blocks)].filter(block => block.role === 'visual' && nodeMdast(block.node).some(hasHtml))
|
|
83
|
+
const source = raw.length === 1 ? raw[0]!.source : section.source
|
|
84
|
+
ctx.report({
|
|
85
|
+
checkId: 'presmium.slides',
|
|
86
|
+
severity: 'error',
|
|
87
|
+
message: `slide ${at[1]} (${section.title || section.id}) does not build: ${reason}`,
|
|
88
|
+
source: { file: source.file, line: source.line },
|
|
89
|
+
})
|
|
90
|
+
return new Error(`slidev build failed at slide ${at[1]}: ${reason}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function base(ctx: RenderContext): string[] {
|
|
94
|
+
return ctx.base !== '/' ? ['--base', ctx.base] : []
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function writeDeck(dir: string, ctx: RenderContext, deck: SlidevDeck, doc: IRDocument): Promise<void> {
|
|
98
|
+
await mkdir(dir, { recursive: true })
|
|
99
|
+
await writeFile(join(dir, '.gitignore'), '*\n')
|
|
100
|
+
const packages = [PACKAGE_DIR, ...addonDirs(doc, ctx)]
|
|
101
|
+
const optimize = optimizeDeps(dir, packages)
|
|
102
|
+
await writeFile(join(dir, 'vite.config.ts'), viteConfig(ctx, await vitePluginEntries(ctx, doc.config.presmium), optimize, allowed(ctx, packages)))
|
|
103
|
+
await copyFile(STYLE, join(dir, 'style.css'))
|
|
104
|
+
await rm(join(dir, 'public'), { recursive: true, force: true })
|
|
105
|
+
await mkdir(join(dir, 'public'))
|
|
106
|
+
const shared = join(ctx.deckDir, 'public')
|
|
107
|
+
for (const entry of await readdir(shared).catch(() => [])) await symlink(join(shared, entry), join(dir, 'public', entry), 'junction')
|
|
108
|
+
await publish(dir, deck.assets)
|
|
109
|
+
const license = join(dir, 'public', FONT_LICENSE)
|
|
110
|
+
if (!existsSync(join(dir, 'public', FONT_LICENSE.split('/')[0]!))) {
|
|
111
|
+
await mkdir(dirname(license), { recursive: true })
|
|
112
|
+
await writeFile(license, fontLicense(import.meta.url))
|
|
113
|
+
}
|
|
114
|
+
await rm(join(dir, 'components'), { recursive: true, force: true })
|
|
115
|
+
if (existsSync(join(ctx.deckDir, 'components'))) await symlink(join(ctx.deckDir, 'components'), join(dir, 'components'), 'junction')
|
|
116
|
+
await writePreviewSetup(dir, ctx)
|
|
117
|
+
await writeFile(join(dir, 'slides.md'), deck.markdown)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function writePreviewSetup(dir: string, ctx: RenderContext): Promise<void> {
|
|
121
|
+
await rm(join(dir, 'setup'), { recursive: true, force: true })
|
|
122
|
+
await rm(join(dir, PREVIEW_FILE), { force: true })
|
|
123
|
+
const core = ctx.pluginDirs[ADDON_CORE]
|
|
124
|
+
if (!ctx.preview || !core) return
|
|
125
|
+
const agent = join(core, 'src/client/preview.ts')
|
|
126
|
+
const options = { origins: ctx.preview.origins, document: previewDocument(ctx) }
|
|
127
|
+
await mkdir(join(dir, 'setup'))
|
|
128
|
+
await writeFile(join(dir, 'setup/root.ts'), `// Generated by @lincyaw/render-slides for a preview of the workspace service; do not edit.
|
|
129
|
+
import { startSlidesPreview } from ${JSON.stringify(agent)}
|
|
130
|
+
|
|
131
|
+
export default function () {
|
|
132
|
+
startSlidesPreview(${JSON.stringify(options)})
|
|
133
|
+
}
|
|
134
|
+
`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function writeRevision(dir: string, ctx: RenderContext, doc: IRDocument): Promise<void> {
|
|
138
|
+
if (!ctx.preview) return
|
|
139
|
+
const revision = doc.meta[PREVIEW_REVISION]
|
|
140
|
+
const json = `${JSON.stringify(typeof revision === 'string' ? { revision } : {})}\n`
|
|
141
|
+
const file = join(dir, PREVIEW_FILE)
|
|
142
|
+
if (await readFile(file, 'utf8').catch(() => '') !== json) await writeFile(file, json)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface VitePluginsEntry {
|
|
146
|
+
file: string
|
|
147
|
+
config: Record<string, unknown>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function nodeEntry(dir: string): Promise<string | undefined> {
|
|
151
|
+
const manifest: unknown = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8').catch(() => '{}'))
|
|
152
|
+
const exports = isRecord(manifest) ? manifest.exports : undefined
|
|
153
|
+
const entry = isRecord(exports) ? exports['./presmium'] : undefined
|
|
154
|
+
return typeof entry === 'string' ? resolve(dir, entry) : undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function vitePluginEntries(ctx: RenderContext, presmium: Record<string, unknown>): Promise<VitePluginsEntry[]> {
|
|
158
|
+
if (ctx.vitePlugins.length === 0) return []
|
|
159
|
+
const entries: VitePluginsEntry[] = []
|
|
160
|
+
for (const name of ctx.plugins) {
|
|
161
|
+
const dir = ctx.pluginDirs[name]
|
|
162
|
+
const file = dir && await nodeEntry(dir)
|
|
163
|
+
if (!file) continue
|
|
164
|
+
const plugin = (await import(pathToFileURL(file).href).catch(() => ({}))).default as PresmiumNodePlugin | undefined
|
|
165
|
+
if (typeof plugin?.vitePlugins !== 'function') continue
|
|
166
|
+
const config = presmium[plugin.id]
|
|
167
|
+
entries.push({ file, config: isRecord(config) ? config : {} })
|
|
168
|
+
}
|
|
169
|
+
return entries
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function allowed(ctx: RenderContext, packages: string[]): string[] {
|
|
173
|
+
const installed = packages.flatMap((dir) => {
|
|
174
|
+
const parts = realpathSync(dir).split(sep)
|
|
175
|
+
const at = parts.indexOf('node_modules')
|
|
176
|
+
return at < 0 ? [] : [parts.slice(0, at + 1).join(sep)]
|
|
177
|
+
})
|
|
178
|
+
return [...new Set([ctx.deckDir, ...installed])]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function viteConfig(ctx: RenderContext, entries: VitePluginsEntry[], optimize: { include: string[], exclude: string[] }, allow: string[]): string {
|
|
182
|
+
const imports = entries.map(({ file }, i) => `import plugin${i} from ${JSON.stringify(file)}\n`).join('')
|
|
183
|
+
const deck = JSON.stringify({ deckDir: ctx.deckDir, workspaceRoot: ctx.workspaceRoot })
|
|
184
|
+
const plugins = entries.map(({ config }, i) => ` ...(plugin${i}.vitePlugins?.({ ...deck, config: ${JSON.stringify(config)} }) ?? []),\n`).join('')
|
|
185
|
+
const server = {
|
|
186
|
+
...ctx.server ? { proxy: { '^/__presmium/': { target: ctx.server.url, ws: true } }, fs: { allow } } : {},
|
|
187
|
+
...ctx.preview ? { headers: { 'Content-Security-Policy': previewFrameAncestors(ctx.preview.origins) } } : {},
|
|
188
|
+
}
|
|
189
|
+
const served = Object.keys(server).length > 0 ? ` server: ${JSON.stringify(server)},\n` : ''
|
|
190
|
+
const optimized = optimize.exclude.length > 0 ? ` optimizeDeps: ${JSON.stringify(optimize)},\n` : ''
|
|
191
|
+
const preview = ctx.preview ? ` presmiumPreview(),\n` : ''
|
|
192
|
+
return `${imports}${ctx.preview ? previewPlugin() : ''}\nconst deck = ${deck}\n\nexport default {\n cacheDir: 'node_modules/.vite',\n plugins: [\n${preview}${plugins} ],\n${served}${optimized}}\n`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function previewPlugin(): string {
|
|
196
|
+
return `import { readFileSync } from 'node:fs'
|
|
197
|
+
import { fileURLToPath } from 'node:url'
|
|
198
|
+
|
|
199
|
+
function presmiumPreview() {
|
|
200
|
+
const file = fileURLToPath(new URL(${JSON.stringify(`./${PREVIEW_FILE}`)}, import.meta.url))
|
|
201
|
+
const read = () => {
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(readFileSync(file, 'utf8'))
|
|
204
|
+
} catch {
|
|
205
|
+
return {}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
name: 'presmium:preview',
|
|
210
|
+
configureServer(server) {
|
|
211
|
+
server.ws.on('presmium:preview', (_data, client) => client.send('presmium:revision', read()))
|
|
212
|
+
server.watcher.add(file)
|
|
213
|
+
let timer
|
|
214
|
+
server.watcher.on('change', (changed) => {
|
|
215
|
+
if (changed !== file) return
|
|
216
|
+
clearTimeout(timer)
|
|
217
|
+
timer = setTimeout(() => server.ws.send('presmium:revision', read()), 150)
|
|
218
|
+
})
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
`
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function publish(dir: string, assets: Map<string, LocalAsset>): Promise<void> {
|
|
226
|
+
for (const found of assets.values()) copyPublished(found, join(dir, 'public'))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function slidevBin(): string {
|
|
230
|
+
const pkg = require.resolve('@slidev/cli/package.json')
|
|
231
|
+
return join(dirname(pkg), (require(pkg) as { bin: { slidev: string } }).bin.slidev)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class SlidevError extends Error {
|
|
235
|
+
readonly output: string
|
|
236
|
+
constructor(message: string, output: string) {
|
|
237
|
+
super(`${message}\n${output.slice(-4000)}`)
|
|
238
|
+
this.output = output
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function slidev(args: string[], cwd: string): Promise<void> {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const child = spawn(process.execPath, [...TYPESCRIPT_IMPORT, slidevBin(), ...args], { cwd, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
245
|
+
let output = ''
|
|
246
|
+
child.stdout.on('data', (chunk: Buffer) => output += chunk)
|
|
247
|
+
child.stderr.on('data', (chunk: Buffer) => output += chunk)
|
|
248
|
+
child.on('error', reject)
|
|
249
|
+
child.on('close', code => code === 0 ? resolve() : reject(new SlidevError(`slidev ${args[0]} exited with code ${code}`, output)))
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function slidevDev(dir: string, args: string[]): Promise<{ url: string, close(): Promise<void> }> {
|
|
254
|
+
const child: ChildProcess = spawn(process.execPath, [...TYPESCRIPT_IMPORT, slidevBin(), 'slides.md', ...args], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
255
|
+
const exited = new Promise<void>(resolve => child.once('exit', () => resolve()))
|
|
256
|
+
const close = async () => {
|
|
257
|
+
if (child.exitCode === null) child.kill('SIGTERM')
|
|
258
|
+
await exited
|
|
259
|
+
}
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
let output: string | undefined = ''
|
|
262
|
+
child.stderr!.on('data', (chunk: Buffer) => {
|
|
263
|
+
if (output !== undefined) output += chunk
|
|
264
|
+
})
|
|
265
|
+
child.stdout!.on('data', (chunk: Buffer) => {
|
|
266
|
+
if (output === undefined) return
|
|
267
|
+
output += chunk
|
|
268
|
+
const url = /(http:\/\/localhost:\d+\/\S*)/.exec(output.replace(/\x1B\[[\d;]*m/g, ''))?.[1]
|
|
269
|
+
if (!url) return
|
|
270
|
+
output = undefined
|
|
271
|
+
resolve({ url, close })
|
|
272
|
+
})
|
|
273
|
+
child.once('error', reject)
|
|
274
|
+
child.once('exit', code => reject(new Error(`slidev exited with code ${code}\n${output?.slice(-4000) ?? ''}`)))
|
|
275
|
+
})
|
|
276
|
+
}
|