@lincyaw/render-slides 1.0.0-rc.0 → 1.0.0-rc.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/README.md CHANGED
@@ -1,24 +1,37 @@
1
1
  # @lincyaw/render-slides
2
2
 
3
- Presmium's slides renderer: writes the IR as a Slidev deck and builds it, with Presmium's default theme Book (书卷). Narration becomes speaker notes, and plugin components render through the slides host adapter.
3
+ Presmium's slides renderer: the slides are the viewer's slides page, built and served as the web form is (`@lincyaw/web`). Each section is a slide on a canvas of a fixed size, drawn in the Book (书卷) layout and scaled as a whole to the screen; plugin components render in it as on the web page. It also gives the document's exports of the slides: PDF, pictures and PowerPoint.
4
4
 
5
- Part of [Presmium](https://github.com/Lincyaw/presmium), which turns lectures written in MyST Markdown into web pages, slides, Jupyter notebooks and narrated lectures.
5
+ Part of [Presmium](https://github.com/Lincyaw/presmium), which compiles canonical lecture.json teaching documents into web pages, slides, Jupyter notebooks and narrated playback.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```sh
10
- pnpm add -D @lincyaw/cli @lincyaw/render-slides @lincyaw/slidev-addon-core
10
+ pnpm add -D @lincyaw/cli @lincyaw/web @lincyaw/render-slides
11
11
  ```
12
12
 
13
- List `@lincyaw/slidev-addon-core` in the document's `plugins`: it is the host adapter of the slides.
13
+ `presmium build` builds the slides into `dist/slides/` when it is installed (or `--to slides`); `presmium dev --to slides` serves the slides page (`?form=slides`).
14
+
15
+ The exports print the slides page in Chromium, from `playwright-chromium`, an optional peer dependency. Install it in the document's project to export:
16
+
17
+ ```sh
18
+ pnpm add -D playwright-chromium
19
+ pnpm exec playwright install chromium
20
+ ```
14
21
 
15
22
  ## Configuration
16
23
 
17
- In the headmatter, `renderers.slides` takes Slidev's headmatter options (`theme`, `routerMode`, `exportFilename` and others), plus `base`, `port` and `addons`. Without `theme`, the slides use the default theme; `theme: default` is Slidev's own.
24
+ In `lecture.json`, `metadata.renderers.slides`: `base` (the site path), `port` (the dev server), `aspectRatio` (`16/9` by default; `4:3` or a number) and `canvasWidth` (980 by default), which give the canvas the slides are drawn on.
25
+
26
+ A section's `{hints}` gives its slide, under `slides`: `layout` (`default`, `cover`, `center` or `full`; the first section is `cover` unless it says otherwise), `class` (classes added to the slide) and `zoom` (a scale of its content).
27
+
28
+ ## Exports
29
+
30
+ The exporters `pdf`, `png` (a picture of each slide, or of each step) and `pptx` (a picture of each slide, its narration as the speaker notes) run as the workspace service's jobs and from `presmium-export` (`@lincyaw/addon-publish`).
18
31
 
19
32
  ## Documentation
20
33
 
21
- - [Author guide](https://github.com/Lincyaw/presmium/blob/main/site/guide/lecture.en.md)
34
+ - [Author guide](https://github.com/Lincyaw/presmium/blob/main/site/guide/locales/en/lecture.json)
22
35
 
23
36
  ## License
24
37
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lincyaw/render-slides",
3
- "version": "1.0.0-rc.0",
4
- "description": "Presmium's slides renderer: writes the IR as a Slidev deck and builds it, with the default theme",
3
+ "version": "1.0.0-rc.2",
4
+ "description": "Presmium's slides renderer: the viewer's slides page, built as the web form is, and its exports as PDF, pictures and PowerPoint",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -12,7 +12,6 @@
12
12
  "bugs": "https://github.com/Lincyaw/presmium/issues",
13
13
  "keywords": [
14
14
  "presmium",
15
- "slidev",
16
15
  "slides",
17
16
  "renderer"
18
17
  ],
@@ -28,34 +27,24 @@
28
27
  "./package.json": "./package.json"
29
28
  },
30
29
  "files": [
31
- "src",
32
- "theme"
30
+ "src"
33
31
  ],
34
32
  "dependencies": {
35
- "@slidev/cli": "53.0.0",
36
- "@slidev/theme-default": "^0.25.0",
37
- "dompurify": "3.4.17",
38
- "jsdom": "29.1.1",
39
- "mdast-util-from-markdown": "2.1.0",
40
- "mdast-util-gfm": "^3.1.0",
41
- "mdast-util-to-markdown": "^2.2.0",
42
- "micromark-extension-gfm": "3.0.0",
43
- "shiki": "^4.5.0",
44
- "vue": "^3.5.42",
45
- "yaml": "^2.9.1"
33
+ "pptxgenjs": "4.0.1"
46
34
  },
47
35
  "peerDependencies": {
48
- "@lincyaw/host": "^1.0.0-rc.0",
49
- "@lincyaw/viewer": "^1.0.0-rc.0",
50
- "@lincyaw/kernel": "^1.0.0-rc.0"
36
+ "playwright-chromium": "^1.40.0",
37
+ "@lincyaw/kernel": "^1.0.0-rc.2",
38
+ "@lincyaw/web": "^1.0.0-rc.2"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "playwright-chromium": {
42
+ "optional": true
43
+ }
51
44
  },
52
45
  "devDependencies": {
53
- "pdf-lib": "^1.17.1",
54
- "pdfjs-dist": "5.7.284",
55
46
  "playwright-chromium": "1.64.1",
56
- "@lincyaw/cli": "1.0.0-rc.0",
57
- "@lincyaw/kernel": "1.0.0-rc.0",
58
- "@lincyaw/host": "1.0.0-rc.0",
59
- "@lincyaw/viewer": "1.0.0-rc.0"
47
+ "@lincyaw/kernel": "1.0.0-rc.2",
48
+ "@lincyaw/web": "1.0.0-rc.2"
60
49
  }
61
50
  }
@@ -1,276 +1,94 @@
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'
1
+ import { mkdir, readFile, rm } from 'node:fs/promises'
5
2
  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'
3
+ import { join } from 'node:path'
4
+ import { blockText, sectionNarration } from '@lincyaw/kernel'
5
+ import type { ExportContext, Exporter, IRDocument, PresmiumNodePlugin } from '@lincyaw/kernel'
6
+ import { playwrightChromium } from '@lincyaw/kernel/node'
7
+ import { printSlides, slidesRenderer } from '@lincyaw/web/slides'
8
+ import type { PrintingChromium, PrintOptions } from '@lincyaw/web/slides'
9
+ import PptxGenJS from 'pptxgenjs'
10
+
11
+ const { version } = createRequire(import.meta.url)('../../package.json') as { version: string }
12
+
13
+ // PowerPoint measures a slide in inches, at 96 pixels each.
14
+ const PIXELS_PER_INCH = 96
15
+ const PICTURES = '.pictures'
16
+
17
+ /** Whether the document has the slides form: its configuration names it, or names no renderer. */
18
+ function hasSlides(doc: IRDocument): boolean {
19
+ const named = Object.keys(doc.config.renderers)
20
+ return named.length === 0 || named.includes('slides')
21
+ }
22
+
23
+ /**
24
+ * What the one who asked chose (`presmium-export`'s flags): `steps`, a page for every step; `slides`, the slides by number
25
+ * from 1; `dark`; `timeout`, how long the page may take, in milliseconds.
26
+ */
27
+ function printChoices(options: Record<string, unknown>): Pick<PrintOptions, 'steps' | 'slides' | 'dark' | 'timeout'> {
28
+ const slides = Array.isArray(options.slides) ? options.slides.filter((n): n is number => Number.isInteger(n) && n > 0) : undefined
29
+ return {
30
+ steps: options.steps === true,
31
+ ...slides ? { slides } : {},
32
+ ...options.dark === true ? { dark: true } : {},
33
+ ...typeof options.timeout === 'number' && options.timeout > 0 ? { timeout: options.timeout } : {},
34
+ }
35
+ }
15
36
 
16
- const ADDON_CORE = '@lincyaw/slidev-addon-core'
17
- const PREVIEW_FILE = 'preview.json'
37
+ /** Builds the slides and prints them. */
38
+ async function printed(ctx: ExportContext, format: 'pdf' | 'png', output: string) {
39
+ ctx.progress('build')
40
+ const dir = await ctx.build('slides')
41
+ ctx.progress('print')
42
+ const chromium = await playwrightChromium<PrintingChromium>(ctx.deckDir, import.meta.url)
43
+ const result = await printSlides({ dir, chromium, format, output, signal: ctx.signal, ...printChoices(ctx.options) })
44
+ return { ...result, doc: JSON.parse(await readFile(join(dir, 'ir.json'), 'utf8')) as IRDocument }
45
+ }
18
46
 
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 }
47
+ function slidesExporter(id: string, label: Record<string, string>, run: Exporter['run']): Exporter {
48
+ return { id, label, available: hasSlides, run }
49
+ }
23
50
 
24
51
  const plugin: PresmiumNodePlugin = {
25
52
  id: 'presmium.slides',
26
53
  version,
27
54
  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,
55
+ renderer: slidesRenderer,
56
+ exporters: [
57
+ slidesExporter('pdf', { 'zh-CN': '幻灯片 PDF', 'en': 'Slides as PDF' }, async (ctx) => {
58
+ await printed(ctx, 'pdf', join(ctx.outDir, 'slides.pdf'))
59
+ return { files: ['slides.pdf'] }
60
+ }),
61
+ slidesExporter('png', { 'zh-CN': '幻灯片图片', 'en': 'Slides as pictures' }, async (ctx) => {
62
+ const { pages } = await printed(ctx, 'png', ctx.outDir)
63
+ return { files: pages.map(page => page.file!) }
64
+ }),
65
+ // A picture of each slide, its narration as the speaker notes.
66
+ slidesExporter('pptx', { 'zh-CN': '幻灯片 PowerPoint', 'en': 'Slides as PowerPoint' }, async (ctx) => {
67
+ const pictures = join(ctx.outDir, PICTURES)
68
+ await mkdir(pictures, { recursive: true })
69
+ try {
70
+ const { shape, pages, doc } = await printed(ctx, 'png', pictures)
71
+ ctx.progress('pptx')
72
+ const pptx = new PptxGenJS()
73
+ const [w, h] = [shape.width / PIXELS_PER_INCH, shape.height / PIXELS_PER_INCH]
74
+ pptx.defineLayout({ name: 'PRESMIUM', width: w, height: h })
75
+ pptx.layout = 'PRESMIUM'
76
+ pptx.title = doc.meta.title
77
+ const sections = new Map(doc.sections.map(section => [section.id, section]))
78
+ for (const page of pages) {
79
+ const slide = pptx.addSlide()
80
+ slide.addImage({ path: join(pictures, page.file!), x: 0, y: 0, w, h })
81
+ const section = sections.get(page.section)
82
+ const notes = section ? sectionNarration(section).map(blockText).join('\n\n') : ''
83
+ if (notes) slide.addNotes(notes)
84
+ }
85
+ await pptx.writeFile({ fileName: join(ctx.outDir, 'slides.pptx') })
86
+ return { files: ['slides.pptx'] }
87
+ } finally {
88
+ await rm(pictures, { recursive: true, force: true })
67
89
  }
68
- },
69
- },
90
+ }),
91
+ ],
70
92
  }
71
93
 
72
94
  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
- }