@slidev-react/node 0.1.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.
@@ -0,0 +1,123 @@
1
+ import path from "node:path";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { clampSlideSelection, createRangesFromSlides, createSlideSelectionLabel, expandSlideSelection, toPdfPageRanges } from "@slidev-react/core/presentation/export/selection";
4
+ import { chromium } from "@playwright/test";
5
+ import { createSlideSnapshotFileName, resolveExportSlidesBaseName } from "@slidev-react/core/presentation/export/fileNames";
6
+ //#region src/exportBrowser.ts
7
+ async function readExportViewport(page) {
8
+ return page.locator("[data-export-view=\"print\"]").evaluate((node) => ({
9
+ width: Number.parseInt(node.getAttribute("data-export-viewport-width") ?? "1920", 10),
10
+ height: Number.parseInt(node.getAttribute("data-export-viewport-height") ?? "1080", 10)
11
+ }));
12
+ }
13
+ async function readSnapshotInfos(page) {
14
+ return page.locator("[data-export-snapshot=\"slide\"]").evaluateAll((nodes) => nodes.map((node, index) => ({
15
+ page: index + 1,
16
+ slideNumber: Number.parseInt(node.getAttribute("data-export-slide") ?? "0", 10),
17
+ title: node.getAttribute("data-export-slide-title") ?? "",
18
+ click: node.getAttribute("data-export-click") ?? "all"
19
+ })));
20
+ }
21
+ function resolveSnapshotSelection(snapshotInfos, slideSelection, withClicks) {
22
+ const totalSlides = new Set(snapshotInfos.map((info) => info.slideNumber)).size;
23
+ const selectedRanges = clampSlideSelection(slideSelection, totalSlides);
24
+ const selectedSlides = expandSlideSelection(selectedRanges);
25
+ if (selectedSlides.length === 0) throw new Error(`No slides matched --slides for a slides file with ${totalSlides} slides.`);
26
+ const selectedSlideSet = new Set(selectedSlides);
27
+ const selectedSnapshots = snapshotInfos.filter((info) => selectedSlideSet.has(info.slideNumber));
28
+ if (selectedSnapshots.length === 0) throw new Error("No export snapshots matched the requested slides.");
29
+ return {
30
+ selectedSlides,
31
+ selectedSnapshots,
32
+ variantLabel: [selectedRanges.length === 1 && selectedRanges[0].start === 1 && selectedRanges[0].end === totalSlides ? null : createSlideSelectionLabel(selectedRanges), withClicks ? "with-clicks" : null].filter(Boolean).join("-")
33
+ };
34
+ }
35
+ async function exportPdfArtifacts(page, exportViewport, slidesBaseName, slidesOutputDir, variantLabel, selectedSnapshots) {
36
+ const pdfPath = path.join(slidesOutputDir, variantLabel ? `${slidesBaseName}-${variantLabel}.pdf` : `${slidesBaseName}.pdf`);
37
+ await page.emulateMedia({ media: "print" });
38
+ await page.pdf({
39
+ path: pdfPath,
40
+ landscape: exportViewport.width > exportViewport.height,
41
+ printBackground: true,
42
+ preferCSSPageSize: true,
43
+ pageRanges: toPdfPageRanges(createRangesFromSlides(selectedSnapshots.map((info) => info.page)))
44
+ });
45
+ await page.emulateMedia({ media: "screen" });
46
+ return pdfPath;
47
+ }
48
+ async function applyPngExportStyles(page, exportViewport) {
49
+ await page.addStyleTag({ content: `
50
+ .print-slides-view main {
51
+ max-width: none !important;
52
+ }
53
+ .print-slide-shell {
54
+ width: ${exportViewport.width}px !important;
55
+ margin-left: auto !important;
56
+ margin-right: auto !important;
57
+ }
58
+ .print-slide-frame {
59
+ padding: 0 !important;
60
+ border: none !important;
61
+ background: transparent !important;
62
+ box-shadow: none !important;
63
+ }
64
+ .print-slide-frame [data-export-surface="slide"] {
65
+ width: ${exportViewport.width}px !important;
66
+ height: ${exportViewport.height}px !important;
67
+ max-width: none !important;
68
+ min-height: ${exportViewport.height}px !important;
69
+ aspect-ratio: auto !important;
70
+ box-shadow: none !important;
71
+ }
72
+ ` });
73
+ }
74
+ async function exportPngArtifacts(page, exportViewport, pngOutputDir, withClicks, selectedSnapshots) {
75
+ await mkdir(pngOutputDir, { recursive: true });
76
+ await applyPngExportStyles(page, exportViewport);
77
+ const createdFiles = [];
78
+ for (const snapshot of selectedSnapshots) {
79
+ const shell = page.locator(`[data-export-snapshot="slide"][data-export-slide="${snapshot.slideNumber}"][data-export-click="${snapshot.click}"]`).first();
80
+ const fileName = createSlideSnapshotFileName({
81
+ index: snapshot.slideNumber,
82
+ title: snapshot.title,
83
+ clickStep: withClicks && snapshot.click !== "all" ? Number.parseInt(snapshot.click, 10) : null
84
+ });
85
+ const targetPath = path.join(pngOutputDir, fileName);
86
+ await shell.locator("[data-export-surface=\"slide\"]").screenshot({ path: targetPath });
87
+ createdFiles.push(targetPath);
88
+ }
89
+ return createdFiles;
90
+ }
91
+ async function exportSlidesInBrowser(options) {
92
+ const browser = await chromium.launch({ headless: true });
93
+ try {
94
+ const page = await browser.newPage({
95
+ viewport: {
96
+ width: 2400,
97
+ height: 1600
98
+ },
99
+ deviceScaleFactor: 1
100
+ });
101
+ await page.goto(options.printUrl, { waitUntil: "networkidle" });
102
+ await page.waitForSelector("[data-export-view=\"print\"]");
103
+ await page.waitForFunction(() => Array.from(document.querySelectorAll("[data-export-slide-ready=\"false\"]")).length === 0);
104
+ const exportViewport = await readExportViewport(page);
105
+ const slidesBaseName = resolveExportSlidesBaseName(await page.title());
106
+ const { selectedSlides, selectedSnapshots, variantLabel } = resolveSnapshotSelection(await readSnapshotInfos(page), options.slideSelection, options.withClicks);
107
+ const slidesOutputDir = path.resolve(options.outputDir, slidesBaseName);
108
+ const pngOutputDir = variantLabel ? path.join(slidesOutputDir, "png", variantLabel) : path.join(slidesOutputDir, "png");
109
+ await mkdir(slidesOutputDir, { recursive: true });
110
+ const createdFiles = [];
111
+ if (options.format === "all" || options.format === "pdf") createdFiles.push(await exportPdfArtifacts(page, exportViewport, slidesBaseName, slidesOutputDir, variantLabel, selectedSnapshots));
112
+ if (options.format === "all" || options.format === "png") createdFiles.push(...await exportPngArtifacts(page, exportViewport, pngOutputDir, options.withClicks, selectedSnapshots));
113
+ return {
114
+ createdFiles,
115
+ selectedSlides,
116
+ variantLabel
117
+ };
118
+ } finally {
119
+ await browser.close();
120
+ }
121
+ }
122
+ //#endregion
123
+ export { exportSlidesInBrowser };
@@ -0,0 +1,5 @@
1
+ import { buildSlidesApp, runSlidesBuild } from "./build.mjs";
2
+ import { exportSlidesArtifacts, runSlidesExport } from "./export.mjs";
3
+ import { runSlidesDev, startSlidesDevServer, stopSlidesDevServer } from "./dev.mjs";
4
+ import { lintSlides, runSlidesLint } from "./lint.mjs";
5
+ export { buildSlidesApp, exportSlidesArtifacts, lintSlides, runSlidesBuild, runSlidesDev, runSlidesExport, runSlidesLint, startSlidesDevServer, stopSlidesDevServer };
package/dist/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ import { buildSlidesApp, runSlidesBuild } from "./build.mjs";
2
+ import { runSlidesDev, startSlidesDevServer, stopSlidesDevServer } from "./dev.mjs";
3
+ import { exportSlidesArtifacts, runSlidesExport } from "./export.mjs";
4
+ import { lintSlides, runSlidesLint } from "./lint.mjs";
5
+ export { buildSlidesApp, exportSlidesArtifacts, lintSlides, runSlidesBuild, runSlidesDev, runSlidesExport, runSlidesLint, startSlidesDevServer, stopSlidesDevServer };
@@ -0,0 +1,15 @@
1
+ import { CommandResult, SlidesCommandOptions } from "./context.mjs";
2
+
3
+ //#region src/lint.d.ts
4
+ interface LintSlidesOptions extends SlidesCommandOptions {
5
+ cliArgs?: string[];
6
+ }
7
+ interface LintSlidesResult {
8
+ strict: boolean;
9
+ warnings: string[];
10
+ slidesSourceFile: string;
11
+ }
12
+ declare function lintSlides(options?: LintSlidesOptions): Promise<LintSlidesResult>;
13
+ declare function runSlidesLint(options?: LintSlidesOptions): Promise<CommandResult>;
14
+ //#endregion
15
+ export { lintSlides, runSlidesLint };
package/dist/lint.mjs ADDED
@@ -0,0 +1,36 @@
1
+ import { createFailureResult, createSuccessResult, resolveSlidesCommandContext } from "./context.mjs";
2
+ import { parseSlides } from "./slides/parsing/parseSlides.mjs";
3
+ import { validateSlidesAuthoring } from "./slides/validation/validateSlidesAuthoring.mjs";
4
+ import { parseLintArgs } from "./cli/lintArgs.mjs";
5
+ import path from "node:path";
6
+ import { readFile } from "node:fs/promises";
7
+ //#region src/lint.ts
8
+ async function lintSlides(options = {}) {
9
+ const parsedArgs = parseLintArgs(options.cliArgs ?? []);
10
+ const context = resolveSlidesCommandContext({
11
+ ...options,
12
+ slidesFile: options.slidesFile ?? parsedArgs.slidesFile
13
+ });
14
+ const slides = parseSlides(await readFile(context.slidesSourceFile, "utf8"));
15
+ const warnings = await validateSlidesAuthoring({
16
+ appRoot: context.appRoot,
17
+ slides
18
+ });
19
+ return {
20
+ strict: parsedArgs.strict,
21
+ warnings,
22
+ slidesSourceFile: context.slidesSourceFile
23
+ };
24
+ }
25
+ async function runSlidesLint(options = {}) {
26
+ const result = await lintSlides(options);
27
+ if (result.warnings.length === 0) {
28
+ console.log(`Slides lint passed: no authoring warnings for ${path.relative(process.cwd(), result.slidesSourceFile)}`);
29
+ return createSuccessResult();
30
+ }
31
+ console.warn(`Slides lint found ${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"}:`);
32
+ for (const warning of result.warnings) console.warn(`- ${warning}`);
33
+ return result.strict ? createFailureResult(1) : createSuccessResult();
34
+ }
35
+ //#endregion
36
+ export { lintSlides, runSlidesLint };
@@ -0,0 +1,9 @@
1
+ import { UserConfig } from "vite";
2
+
3
+ //#region src/slides/build/createSlidesViteConfig.d.ts
4
+ declare function createSlidesViteConfig(options: {
5
+ appRoot: string;
6
+ slidesFile?: string;
7
+ }): UserConfig;
8
+ //#endregion
9
+ export { createSlidesViteConfig };
@@ -0,0 +1,25 @@
1
+ import { resolveSlidesSourceFile } from "./slidesSourceFile.mjs";
2
+ import { generatedSlidesAlias, generatedSlidesEntry, pluginCompileTimeSlides } from "./generateCompiledSlides.mjs";
3
+ import path from "node:path";
4
+ import react from "@vitejs/plugin-react";
5
+ //#region src/slides/build/createSlidesViteConfig.ts
6
+ function createSlidesViteConfig(options) {
7
+ const { appRoot, slidesFile } = options;
8
+ return {
9
+ root: appRoot,
10
+ plugins: [pluginCompileTimeSlides({
11
+ appRoot,
12
+ slidesSourceFile: resolveSlidesSourceFile(appRoot, slidesFile)
13
+ }), react()],
14
+ resolve: { alias: {
15
+ "@": path.resolve(appRoot, "./packages/client/src"),
16
+ [generatedSlidesAlias]: path.resolve(appRoot, generatedSlidesEntry),
17
+ react: path.resolve(appRoot, "./node_modules/react"),
18
+ "react-dom": path.resolve(appRoot, "./node_modules/react-dom"),
19
+ "react/jsx-runtime": path.resolve(appRoot, "./node_modules/react/jsx-runtime.js"),
20
+ "react/jsx-dev-runtime": path.resolve(appRoot, "./node_modules/react/jsx-dev-runtime.js")
21
+ } }
22
+ };
23
+ }
24
+ //#endregion
25
+ export { createSlidesViteConfig };
@@ -0,0 +1,264 @@
1
+ import { getMdxCompileOptions } from "../compiling/mdx-options.mjs";
2
+ import { parseImportedSlides, parseSlides } from "../parsing/parseSlides.mjs";
3
+ import { validateSlidesAuthoring } from "../validation/validateSlidesAuthoring.mjs";
4
+ import path from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
7
+ import { compile } from "@mdx-js/mdx";
8
+ //#region src/slides/build/generateCompiledSlides.ts
9
+ const GENERATED_SLIDES_DIR_ROOT = ".generated/slides";
10
+ const GENERATED_SLIDE_MODULES_DIR = `${GENERATED_SLIDES_DIR_ROOT}/slides`;
11
+ const GENERATED_MANIFEST_FILE = `${GENERATED_SLIDES_DIR_ROOT}/index.ts`;
12
+ function hashString(source) {
13
+ return createHash("sha256").update(source).digest("hex").slice(0, 12);
14
+ }
15
+ function toModuleIdentifier(slideId) {
16
+ return slideId.replace(/[^a-zA-Z0-9_$]/g, "_");
17
+ }
18
+ function createSlideModuleFileName(index) {
19
+ return `slide-${index + 1}.tsx`;
20
+ }
21
+ function createSlideModuleCode(compiledSource) {
22
+ return [
23
+ "/* eslint-disable */",
24
+ "/* @generated by compile-time slides pipeline */",
25
+ compiledSource.trim(),
26
+ ""
27
+ ].join("\n");
28
+ }
29
+ function normalizeDiagramCodeProps(compiledSource) {
30
+ return compiledSource.replace(/<(MermaidDiagram|PlantUmlDiagram) code="([\s\S]*?)" \/>/g, (_match, componentName, code) => `<${componentName} code={${JSON.stringify(code)}} />`);
31
+ }
32
+ function createManifestCode({ deckSourceHash, deckMeta, slideEntries }) {
33
+ const imports = slideEntries.map((entry) => `import ${entry.importName} from '${entry.importPath}'`).join("\n");
34
+ const slides = slideEntries.map((entry) => ` {
35
+ id: ${JSON.stringify(entry.id)},
36
+ meta: ${JSON.stringify(entry.meta, null, 6)},
37
+ component: ${entry.importName},
38
+ }`).join(",\n");
39
+ return [
40
+ "/* eslint-disable */",
41
+ "/* @generated by compile-time slides pipeline */",
42
+ "import type { CompiledSlidesManifest } from '@slidev-react/core/slides/compiled-slides'",
43
+ imports,
44
+ "",
45
+ "const compiledSlides: CompiledSlidesManifest = {",
46
+ ` sourceHash: ${JSON.stringify(deckSourceHash)},`,
47
+ ` meta: ${JSON.stringify(deckMeta, null, 2)},`,
48
+ " slides: [",
49
+ slides,
50
+ " ],",
51
+ "}",
52
+ "",
53
+ "export default compiledSlides",
54
+ ""
55
+ ].join("\n");
56
+ }
57
+ async function writeIfChanged(filePath, content) {
58
+ try {
59
+ if (await readFile(filePath, "utf8") === content) return;
60
+ } catch {}
61
+ await mkdir(path.dirname(filePath), { recursive: true });
62
+ await writeFile(filePath, content, "utf8");
63
+ }
64
+ async function removeStaleGeneratedSlides(slidesDir, expectedFiles) {
65
+ try {
66
+ const files = await readdir(slidesDir);
67
+ await Promise.all(files.map(async (fileName) => {
68
+ if (!expectedFiles.has(fileName)) await unlink(path.join(slidesDir, fileName));
69
+ }));
70
+ } catch {}
71
+ }
72
+ async function compileSlideModule(source) {
73
+ const compiled = await compile(source, {
74
+ ...getMdxCompileOptions(),
75
+ development: false,
76
+ jsx: true,
77
+ jsxImportSource: "react",
78
+ providerImportSource: "@mdx-js/react"
79
+ });
80
+ return normalizeDiagramCodeProps(String(compiled));
81
+ }
82
+ function createSlidesSourceHash({ slidesSource, resolvedSlides }) {
83
+ return hashString(JSON.stringify({
84
+ slidesSource,
85
+ slides: resolvedSlides.map((slide) => ({
86
+ id: slide.id,
87
+ source: slide.source,
88
+ externalFilePath: slide.externalFilePath,
89
+ externalFileSource: slide.externalFileSource
90
+ }))
91
+ }));
92
+ }
93
+ async function resolveSlideUnitSource({ slide, slidesSourceFile }) {
94
+ const slideSrc = slide.meta.src?.trim();
95
+ if (!slideSrc) return [{
96
+ ...slide,
97
+ watchedFiles: [],
98
+ externalFilePath: void 0,
99
+ externalFileSource: void 0
100
+ }];
101
+ if (slide.hasInlineSource) throw new Error(`Slide ${slide.index + 1}${slide.meta.title ? ` (${slide.meta.title})` : ""} mixes inline content with src. Use one or the other.`);
102
+ const externalFilePath = path.resolve(path.dirname(slidesSourceFile), slideSrc);
103
+ let externalFileSource;
104
+ try {
105
+ externalFileSource = await readFile(externalFilePath, "utf8");
106
+ } catch (error) {
107
+ throw new Error(`Failed to load slide ${slide.index + 1}${slide.meta.title ? ` (${slide.meta.title})` : ""} src "${slideSrc}": ${error instanceof Error ? error.message : String(error)}`, { cause: error });
108
+ }
109
+ const importedSlides = parseImportedSlides(externalFileSource);
110
+ for (const importedSlide of importedSlides) {
111
+ if (!importedSlide.meta.src) continue;
112
+ throw new Error(`Nested src is not supported in slide ${slide.index + 1}${slide.meta.title ? ` (${slide.meta.title})` : ""}.`);
113
+ }
114
+ return importedSlides.map((importedSlide) => ({
115
+ ...importedSlide,
116
+ meta: {
117
+ ...importedSlide.meta,
118
+ ...slide.meta
119
+ },
120
+ watchedFiles: [externalFilePath],
121
+ externalFilePath,
122
+ externalFileSource
123
+ }));
124
+ }
125
+ async function generateCompiledSlidesArtifacts(options) {
126
+ const { appRoot, slidesSourceFile } = options;
127
+ const slidesSource = await readFile(slidesSourceFile, "utf8");
128
+ const parsedSlides = parseSlides(slidesSource);
129
+ const generatedSlidesRootDir = path.join(appRoot, GENERATED_SLIDES_DIR_ROOT);
130
+ const generatedSlideModulesDir = path.join(appRoot, GENERATED_SLIDE_MODULES_DIR);
131
+ const manifestFile = path.join(appRoot, GENERATED_MANIFEST_FILE);
132
+ const expectedSlideFiles = /* @__PURE__ */ new Set();
133
+ const watchedFiles = new Set([slidesSourceFile]);
134
+ const manifestEntries = [];
135
+ const resolvedSlides = (await Promise.all(parsedSlides.slides.map((slide) => resolveSlideUnitSource({
136
+ slide,
137
+ slidesSourceFile
138
+ })))).flat().map((slide, index) => ({
139
+ ...slide,
140
+ id: `slide-${index + 1}`,
141
+ index
142
+ }));
143
+ const warnings = await validateSlidesAuthoring({
144
+ appRoot,
145
+ slides: {
146
+ meta: parsedSlides.meta,
147
+ slides: resolvedSlides
148
+ }
149
+ });
150
+ const sourceHash = createSlidesSourceHash({
151
+ slidesSource,
152
+ resolvedSlides
153
+ });
154
+ await mkdir(generatedSlideModulesDir, { recursive: true });
155
+ for (const slide of resolvedSlides) {
156
+ const slideFileName = createSlideModuleFileName(slide.index);
157
+ const slideFilePath = path.join(generatedSlideModulesDir, slideFileName);
158
+ const importName = toModuleIdentifier(slide.id);
159
+ for (const watchedFile of slide.watchedFiles) watchedFiles.add(watchedFile);
160
+ try {
161
+ await writeIfChanged(slideFilePath, createSlideModuleCode(await compileSlideModule(slide.source)));
162
+ } catch (error) {
163
+ const slideName = slide.meta.title ? `${slide.index + 1} (${slide.meta.title})` : String(slide.index + 1);
164
+ throw new Error(`Failed to compile slide ${slideName}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
165
+ }
166
+ expectedSlideFiles.add(slideFileName);
167
+ manifestEntries.push({
168
+ id: slide.id,
169
+ importName,
170
+ importPath: `./slides/${path.basename(slideFileName, path.extname(slideFileName))}`,
171
+ meta: slide.meta
172
+ });
173
+ }
174
+ await removeStaleGeneratedSlides(generatedSlideModulesDir, expectedSlideFiles);
175
+ await writeIfChanged(manifestFile, createManifestCode({
176
+ deckSourceHash: sourceHash,
177
+ deckMeta: parsedSlides.meta,
178
+ slideEntries: manifestEntries
179
+ }));
180
+ return {
181
+ generatedSlidesRootDir,
182
+ manifestFile,
183
+ sourceHash,
184
+ watchedFiles: [...watchedFiles],
185
+ warnings
186
+ };
187
+ }
188
+ function createSlidesGenerator(options) {
189
+ let generatePromise = null;
190
+ let rerunRequested = false;
191
+ let watchedFiles = new Set([options.slidesSourceFile]);
192
+ let warnings = [];
193
+ const generate = async () => {
194
+ if (generatePromise) {
195
+ rerunRequested = true;
196
+ await generatePromise;
197
+ return;
198
+ }
199
+ generatePromise = (async () => {
200
+ do {
201
+ rerunRequested = false;
202
+ const result = await generateCompiledSlidesArtifacts(options);
203
+ watchedFiles = new Set(result.watchedFiles);
204
+ warnings = result.warnings;
205
+ } while (rerunRequested);
206
+ })();
207
+ try {
208
+ await generatePromise;
209
+ } finally {
210
+ generatePromise = null;
211
+ }
212
+ };
213
+ return {
214
+ generate,
215
+ getWatchedFiles: () => [...watchedFiles],
216
+ getWarnings: () => warnings
217
+ };
218
+ }
219
+ async function regenerateSlidesForServer(server, generate) {
220
+ try {
221
+ await generate();
222
+ server.ws.send({ type: "full-reload" });
223
+ } catch (error) {
224
+ server.config.logger.error(error instanceof Error ? error.stack ?? error.message : String(error), { timestamp: true });
225
+ }
226
+ }
227
+ function pluginCompileTimeSlides(options) {
228
+ const slidesSourceFile = path.resolve(options.slidesSourceFile);
229
+ const generator = createSlidesGenerator({
230
+ ...options,
231
+ slidesSourceFile
232
+ });
233
+ return {
234
+ name: "slide-react-compile-time-slides",
235
+ async buildStart() {
236
+ await generator.generate();
237
+ for (const warning of generator.getWarnings()) this.warn(warning);
238
+ for (const watchedFile of generator.getWatchedFiles()) this.addWatchFile(watchedFile);
239
+ },
240
+ configureServer(server) {
241
+ server.watcher.add(slidesSourceFile);
242
+ const syncWatchedFiles = () => {
243
+ server.watcher.add(generator.getWatchedFiles());
244
+ };
245
+ const handleSlidesChange = async (filePath) => {
246
+ const resolvedFilePath = path.resolve(filePath);
247
+ if (!generator.getWatchedFiles().includes(resolvedFilePath)) return;
248
+ await regenerateSlidesForServer(server, async () => {
249
+ await generator.generate();
250
+ for (const warning of generator.getWarnings()) server.config.logger.warn(warning, { timestamp: true });
251
+ syncWatchedFiles();
252
+ });
253
+ };
254
+ syncWatchedFiles();
255
+ server.watcher.on("add", handleSlidesChange);
256
+ server.watcher.on("change", handleSlidesChange);
257
+ server.watcher.on("unlink", handleSlidesChange);
258
+ }
259
+ };
260
+ }
261
+ const generatedSlidesAlias = "@generated/slides";
262
+ const generatedSlidesEntry = path.join(GENERATED_SLIDES_DIR_ROOT, "index.ts");
263
+ //#endregion
264
+ export { generatedSlidesAlias, generatedSlidesEntry, pluginCompileTimeSlides };
@@ -0,0 +1,9 @@
1
+ import path from "node:path";
2
+ //#region src/slides/build/slidesSourceFile.ts
3
+ const DEFAULT_SLIDES_SOURCE_FILE = "slides.mdx";
4
+ function resolveSlidesSourceFile(appRoot, slidesFile) {
5
+ const configuredSlidesFile = slidesFile?.trim() || process.env.SLIDES_FILE?.trim() || DEFAULT_SLIDES_SOURCE_FILE;
6
+ return path.resolve(appRoot, configuredSlidesFile);
7
+ }
8
+ //#endregion
9
+ export { resolveSlidesSourceFile };
@@ -0,0 +1,18 @@
1
+ import { remarkDiagramComponents } from "../mdx/remarkDiagramComponents.mjs";
2
+ import { rehypeShikiVitesse } from "./rehypeShikiVitesse.mjs";
3
+ import rehypeKatex from "rehype-katex";
4
+ import remarkGfm from "remark-gfm";
5
+ import remarkMath from "remark-math";
6
+ //#region src/slides/compiling/mdx-options.ts
7
+ function getMdxCompileOptions() {
8
+ return {
9
+ remarkPlugins: [
10
+ remarkGfm,
11
+ remarkMath,
12
+ remarkDiagramComponents
13
+ ],
14
+ rehypePlugins: [rehypeKatex, rehypeShikiVitesse]
15
+ };
16
+ }
17
+ //#endregion
18
+ export { getMdxCompileOptions };
@@ -0,0 +1,93 @@
1
+ import { createHighlighter } from "shiki";
2
+ //#region src/slides/compiling/rehypeShikiVitesse.ts
3
+ const SHIKI_THEME = "vitesse-light";
4
+ const PLAIN_TEXT_LANG = "txt";
5
+ const preloadedLangs = [
6
+ "md",
7
+ "markdown",
8
+ "js",
9
+ "jsx",
10
+ "ts",
11
+ "tsx",
12
+ "json",
13
+ "bash",
14
+ "sh",
15
+ "yaml",
16
+ "yml",
17
+ "html",
18
+ "css",
19
+ "vue",
20
+ "txt"
21
+ ];
22
+ let highlighterPromise = null;
23
+ function getHighlighter() {
24
+ if (!highlighterPromise) highlighterPromise = createHighlighter({
25
+ themes: [SHIKI_THEME],
26
+ langs: preloadedLangs
27
+ });
28
+ return highlighterPromise;
29
+ }
30
+ function asArray(value) {
31
+ if (!value) return [];
32
+ if (Array.isArray(value)) return value.map((item) => {
33
+ if (typeof item === "string") return item;
34
+ try {
35
+ return JSON.stringify(item) ?? "";
36
+ } catch {
37
+ return "";
38
+ }
39
+ });
40
+ if (typeof value === "string") return [value];
41
+ try {
42
+ return [JSON.stringify(value) ?? ""];
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+ function isElement(node, tagName) {
48
+ return node?.type === "element" && (!tagName || node.tagName === tagName);
49
+ }
50
+ function extractText(node) {
51
+ if (!node) return "";
52
+ if (node.type === "text") return node.value ?? "";
53
+ if (!node.children) return "";
54
+ return node.children.map((child) => extractText(child)).join("");
55
+ }
56
+ function detectLanguage(preNode) {
57
+ const codeNode = preNode.children?.find((child) => isElement(child, "code")) ?? null;
58
+ if (!codeNode) return PLAIN_TEXT_LANG;
59
+ const languageClass = asArray(codeNode.properties?.className).find((name) => name.startsWith("language-"));
60
+ if (!languageClass) return PLAIN_TEXT_LANG;
61
+ return languageClass.slice(9).trim() || PLAIN_TEXT_LANG;
62
+ }
63
+ async function highlight(code, lang) {
64
+ const highlighter = await getHighlighter();
65
+ try {
66
+ return highlighter.codeToHast(code, {
67
+ lang,
68
+ theme: SHIKI_THEME
69
+ });
70
+ } catch {
71
+ return highlighter.codeToHast(code, {
72
+ lang: PLAIN_TEXT_LANG,
73
+ theme: SHIKI_THEME
74
+ });
75
+ }
76
+ }
77
+ async function walk(node, parent, index) {
78
+ if (isElement(node, "pre") && parent && index !== null) {
79
+ const rendered = await highlight(extractText(node), detectLanguage(node));
80
+ const replacement = rendered?.type === "root" ? rendered.children?.[0] : rendered;
81
+ if (replacement && parent.children) parent.children[index] = replacement;
82
+ return;
83
+ }
84
+ if (!node.children) return;
85
+ for (let childIndex = 0; childIndex < node.children.length; childIndex += 1) await walk(node.children[childIndex], node, childIndex);
86
+ }
87
+ function rehypeShikiVitesse() {
88
+ return async (tree) => {
89
+ await walk(tree, null, null);
90
+ };
91
+ }
92
+ //#endregion
93
+ export { rehypeShikiVitesse };
@@ -0,0 +1,38 @@
1
+ //#region src/slides/mdx/remarkDiagramComponents.ts
2
+ function createDiagramNode(name, code) {
3
+ return {
4
+ type: "mdxJsxFlowElement",
5
+ name,
6
+ attributes: [],
7
+ children: [{
8
+ type: "text",
9
+ value: code
10
+ }]
11
+ };
12
+ }
13
+ function walk(parent) {
14
+ const children = parent?.children;
15
+ if (!Array.isArray(children)) return;
16
+ for (let index = 0; index < children.length; index += 1) {
17
+ const node = children[index];
18
+ if (node?.type === "code") {
19
+ const lang = String(node.lang || "").trim().toLowerCase();
20
+ if (lang === "mermaid") {
21
+ children[index] = createDiagramNode("MermaidDiagram", node.value || "");
22
+ continue;
23
+ }
24
+ if (lang === "plantuml" || lang === "startuml") {
25
+ children[index] = createDiagramNode("PlantUmlDiagram", node.value || "");
26
+ continue;
27
+ }
28
+ }
29
+ walk(node);
30
+ }
31
+ }
32
+ function remarkDiagramComponents() {
33
+ return (tree) => {
34
+ walk(tree);
35
+ };
36
+ }
37
+ //#endregion
38
+ export { remarkDiagramComponents };
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ import YAML from "yaml";
3
+ //#region src/slides/parsing/frontmatter.ts
4
+ const OPEN = "---";
5
+ const frontmatterDataSchema = z.object({}).catchall(z.unknown());
6
+ function parseFrontmatter(source) {
7
+ const normalized = source.replace(/\r\n/g, "\n");
8
+ if (!normalized.startsWith(`${OPEN}\n`)) return {
9
+ data: {},
10
+ content: normalized
11
+ };
12
+ const closeMarkerWithNewline = `\n${OPEN}\n`;
13
+ const closeMarkerAtEnd = `\n${OPEN}`;
14
+ const withNewlineIndex = normalized.indexOf(closeMarkerWithNewline, 4);
15
+ const atEndIndex = withNewlineIndex < 0 && normalized.endsWith(closeMarkerAtEnd) ? normalized.length - closeMarkerAtEnd.length : -1;
16
+ const endIndex = withNewlineIndex >= 0 ? withNewlineIndex : atEndIndex;
17
+ if (endIndex < 0) throw new Error("Invalid frontmatter: missing closing ---");
18
+ const yamlSource = normalized.slice(4, endIndex);
19
+ const raw = YAML.parse(yamlSource);
20
+ const parsed = frontmatterDataSchema.safeParse(raw ?? {});
21
+ if (!parsed.success) throw new Error("Invalid frontmatter: expected an object");
22
+ const contentStart = endIndex + (withNewlineIndex >= 0 ? closeMarkerWithNewline.length : closeMarkerAtEnd.length);
23
+ return {
24
+ data: parsed.data,
25
+ content: normalized.slice(contentStart)
26
+ };
27
+ }
28
+ //#endregion
29
+ export { parseFrontmatter };