@slidev/cli 52.16.0 → 52.17.1

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.
@@ -1,6 +1,6 @@
1
- import { a as resolveImportPath, c as toAtFS, i as resolveEntry, n as getRoots, o as resolveImportUrl, r as isInstalledGlobally, s as resolveSourceFiles, t as createResolver } from "./resolver-CZx9LFrt.mjs";
1
+ import { a as resolveImportPath, c as toAtFS, i as resolveEntry, n as getRoots, o as resolveImportUrl, r as isInstalledGlobally, s as resolveSourceFiles, t as createResolver } from "./resolver-BBT539iz.mjs";
2
2
  import { builtinModules } from "node:module";
3
- import path, { basename, dirname, join, relative, resolve, win32 } from "node:path";
3
+ import path, { basename, dirname, isAbsolute, join, relative, resolve, win32 } from "node:path";
4
4
  import process from "node:process";
5
5
  import { createServer, loadConfigFromFile, mergeConfig, parseSync } from "vite";
6
6
  import MarkdownExit from "markdown-exit";
@@ -40,7 +40,11 @@ import lz from "lz-string";
40
40
  import { encode } from "plantuml-encoder";
41
41
  import { SourceMapConsumer } from "source-map-js";
42
42
  import { fromAsyncCodeToHtml } from "@shikijs/markdown-it/async";
43
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
44
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
45
+ import { z } from "zod";
43
46
  import ServerRef from "vite-plugin-vue-server-ref";
47
+ import { VitePWA } from "vite-plugin-pwa";
44
48
  import UnoCSS from "unocss/vite";
45
49
  import { mergeConfigs, presetIcons } from "unocss";
46
50
  import Vue from "@vitejs/plugin-vue";
@@ -66,7 +70,172 @@ function MarkdownItLink(md) {
66
70
  };
67
71
  }
68
72
  //#endregion
73
+ //#region node/virtual/deprecated.ts
74
+ /**
75
+ * Kept for backward compatibility, use #slidev/slides instead
76
+ *
77
+ * @deprecated
78
+ */
79
+ const templateLegacyRoutes = {
80
+ id: "/@slidev/routes",
81
+ getContent() {
82
+ return [`export { slides } from '#slidev/slides'`, `console.warn('[slidev] #slidev/routes is deprecated, use #slidev/slides instead')`].join("\n");
83
+ }
84
+ };
85
+ /**
86
+ * Kept for backward compatibility, use #slidev/title-renderer instead
87
+ *
88
+ * @deprecated
89
+ */
90
+ const templateLegacyTitles = {
91
+ id: "/@slidev/titles.md",
92
+ getContent() {
93
+ return `
94
+ <script setup lang="ts">
95
+ import TitleRenderer from '#slidev/title-renderer'
96
+ defineProps<{ no: number | string }>()
97
+ console.warn('/@slidev/titles.md is deprecated, import from #slidev/title-renderer instead')
98
+ <\/script>
99
+
100
+ <TitleRenderer :no="no" />
101
+ `;
102
+ }
103
+ };
104
+ //#endregion
105
+ //#region node/virtual/titles.ts
106
+ const templateTitleRendererMd = {
107
+ id: "/@slidev/title-renderer.md",
108
+ getContent({ data }) {
109
+ const lines = data.slides.map(({ title }, i) => `<template ${i === 0 ? "v-if" : "v-else-if"}="no === ${i + 1}">\n\n${title}\n\n</template>`);
110
+ lines.push(`<script setup lang="ts">`, `import { useSlideContext } from '@slidev/client/context.ts'`, `import { computed } from 'vue'`, `const props = defineProps<{ no?: number | string }>()`, `const { $page } = useSlideContext()`, `const no = computed(() => +(props.no ?? $page.value))`, `<\/script>`);
111
+ return lines.join("\n");
112
+ }
113
+ };
114
+ const templateTitleRenderer = {
115
+ id: "/@slidev/title-renderer",
116
+ async getContent() {
117
+ return "export { default } from \"/@slidev/title-renderer.md\"";
118
+ }
119
+ };
120
+ //#endregion
121
+ //#region node/vite/common.ts
122
+ const regexSlideReqPath = /^\/__slidev\/slides\/(\d+)\.json$/;
123
+ const regexSlideFacadeId = /^\/@slidev\/slides\/(\d+)\/(md|frontmatter)($|\?)/;
124
+ const regexSlideSourceId = /__slidev_(\d+)\.(md|frontmatter)$/;
125
+ const templateInjectionMarker = "/* @slidev-injection */";
126
+ const templateImportContextUtils = `import { useSlideContext as _useSlideContext, frontmatterToProps as _frontmatterToProps } from "@slidev/client/context.ts"`;
127
+ const templateInitContext = `const { $slidev, $nav, $clicksContext, $clicks, $page, $renderContext, $frontmatter } = _useSlideContext()`;
128
+ //#endregion
129
+ //#region node/vite/importGuard.ts
130
+ const virtualSlideMarkdownIds = /* @__PURE__ */ new Set([templateTitleRendererMd.id, templateLegacyTitles.id]);
131
+ function createSlideImportGuardPlugin() {
132
+ let config;
133
+ let allowRoots = [];
134
+ return {
135
+ name: "slidev:slide-import-guard",
136
+ configResolved(resolved) {
137
+ config = resolved;
138
+ allowRoots = config.server.fs.allow.map(normalizeFsPath);
139
+ },
140
+ async transform(code, id) {
141
+ if (!isSlideMarkdownId(id) || !config?.server.fs.strict) return null;
142
+ const importer = filePathFromId(id) ?? id;
143
+ await Promise.all(extractImportSources(code, id).map(async ({ value, start }) => {
144
+ const resolved = await this.resolve(value, importer, { skipSelf: true });
145
+ if (!resolved || resolved.external) return;
146
+ const filePath = filePathFromId(resolved.id);
147
+ if (!filePath) return;
148
+ const normalized = normalizeFsPath(filePath);
149
+ if (isAllowedFile(normalized, allowRoots)) return;
150
+ if (isBareImport(value) && isDependencyFile(normalized)) return;
151
+ this.error(`[slidev] Import "${value}" from slide Markdown resolves outside of Vite server.fs.allow: ${normalized}`, start);
152
+ }));
153
+ return null;
154
+ }
155
+ };
156
+ }
157
+ function isSlideMarkdownId(id) {
158
+ const clean = cleanUrl$1(id);
159
+ return regexSlideSourceId.test(clean) || virtualSlideMarkdownIds.has(clean);
160
+ }
161
+ function filePathFromId(id) {
162
+ const clean = cleanUrl$1(id);
163
+ if (clean.startsWith("file://")) return fileURLToPath(clean);
164
+ if (clean.startsWith("/@fs/")) return clean.slice(4);
165
+ if (clean.startsWith("/@")) return null;
166
+ if (path.isAbsolute(clean)) return clean;
167
+ return null;
168
+ }
169
+ function isAllowedFile(filePath, allowRoots) {
170
+ return allowRoots.some((root) => isFileInRoot(root, filePath));
171
+ }
172
+ function extractImportSources(code, id) {
173
+ const result = parseSync(id, code);
174
+ const sources = [];
175
+ for (const item of result.module.staticImports) sources.push({
176
+ value: item.moduleRequest.value,
177
+ start: item.moduleRequest.start
178
+ });
179
+ for (const item of result.module.staticExports) for (const entry of item.entries) if (entry.moduleRequest) sources.push({
180
+ value: entry.moduleRequest.value,
181
+ start: entry.moduleRequest.start
182
+ });
183
+ for (const item of result.module.dynamicImports) {
184
+ const source = parseStringLiteral(code.slice(item.moduleRequest.start, item.moduleRequest.end));
185
+ if (source) sources.push({
186
+ value: source,
187
+ start: item.moduleRequest.start
188
+ });
189
+ }
190
+ return sources;
191
+ }
192
+ function cleanUrl$1(id) {
193
+ return id.replace(/[?#].*$/, "");
194
+ }
195
+ function normalizeFsPath(filePath) {
196
+ const absolute = path.resolve(filePath);
197
+ if (!existsSync(absolute)) return normalizeMissingFsPath(absolute);
198
+ return realpathSync.native(absolute);
199
+ }
200
+ function normalizeMissingFsPath(filePath) {
201
+ const dir = path.dirname(filePath);
202
+ if (dir === filePath) return filePath;
203
+ if (existsSync(dir)) return path.join(realpathSync.native(dir), path.basename(filePath));
204
+ return path.join(normalizeMissingFsPath(dir), path.basename(filePath));
205
+ }
206
+ function isFileInRoot(root, filePath) {
207
+ const relative = path.relative(root, filePath);
208
+ return relative === "" || !!relative && !relative.startsWith("..") && !path.isAbsolute(relative);
209
+ }
210
+ function isDependencyFile(filePath) {
211
+ return filePath.split(/[\\/]/).includes("node_modules");
212
+ }
213
+ function isBareImport(source) {
214
+ return /^(?![a-z]:)[\w@](?!.*:\/\/)/i.test(source);
215
+ }
216
+ function parseStringLiteral(raw) {
217
+ const trimmed = raw.trim();
218
+ const quote = trimmed[0];
219
+ if (quote !== "\"" && quote !== "'" && quote !== "`") return null;
220
+ if (trimmed.at(-1) !== quote) return null;
221
+ if (quote === "`" && trimmed.includes("${")) return null;
222
+ try {
223
+ if (quote === "\"") return JSON.parse(trimmed);
224
+ return JSON.parse(`"${trimmed.slice(1, -1).replace(/"/g, "\\\"")}"`);
225
+ } catch {
226
+ return trimmed.slice(1, -1);
227
+ }
228
+ }
229
+ //#endregion
69
230
  //#region node/utils.ts
231
+ /**
232
+ * Whether `filePath` resolves inside any of `roots` (no `..` escape). Shared
233
+ * containment predicate reused by the snippet (`<<<`) and `src:` deck-file
234
+ * reads, and by the Vite slide-import guard (`isAllowedFile`).
235
+ */
236
+ function isPathInsideRoots(filePath, roots) {
237
+ return isAllowedFile(filePath, roots);
238
+ }
70
239
  const RE_WHITESPACE_ONLY = /^\s*$/;
71
240
  const RE_QUOTED_STRING = /^(['"])(.*)\1$/;
72
241
  const RE_WHITESPACE = /\s+/g;
@@ -239,14 +408,6 @@ function createComponentsPlugin({ clientRoot, roots }, pluginOptions) {
239
408
  });
240
409
  }
241
410
  //#endregion
242
- //#region node/vite/common.ts
243
- const regexSlideReqPath = /^\/__slidev\/slides\/(\d+)\.json$/;
244
- const regexSlideFacadeId = /^\/@slidev\/slides\/(\d+)\/(md|frontmatter)($|\?)/;
245
- const regexSlideSourceId = /__slidev_(\d+)\.(md|frontmatter)$/;
246
- const templateInjectionMarker = "/* @slidev-injection */";
247
- const templateImportContextUtils = `import { useSlideContext as _useSlideContext, frontmatterToProps as _frontmatterToProps } from "@slidev/client/context.ts"`;
248
- const templateInitContext = `const { $slidev, $nav, $clicksContext, $clicks, $page, $renderContext, $frontmatter } = _useSlideContext()`;
249
- //#endregion
250
411
  //#region node/vite/contextInjection.ts
251
412
  const RE_EXPORT_DEFAULT_OBJECT = /export\s+default\s+\{/;
252
413
  const RE_INJECT_OPTION = /.*inject\s*:\s*([[{])/;
@@ -307,6 +468,7 @@ const RE_SLIDEV_CLIENT_SUBPATH = /^@slidev\/client\/(.*)/;
307
468
  const RE_SLIDEV_VIRTUAL = /^#slidev\/(.*)/;
308
469
  const RE_MONACO_EDITOR = /\/monaco-editor(?:-core)?\//;
309
470
  const INCLUDE_GLOBAL = [
471
+ "@fix-webm-duration/fix",
310
472
  "@typescript/ata",
311
473
  "file-saver",
312
474
  "lz-string",
@@ -462,170 +624,21 @@ function createHmrPatchPlugin() {
462
624
  name: "slidev:hmr-patch",
463
625
  transform: {
464
626
  filter: { id: { include: regexSlideSourceId } },
465
- handler(code, id) {
466
- if (!id.match(regexSlideSourceId)) return;
467
- return code.replace("if (_rerender_only)", "if (false)");
468
- }
469
- }
470
- };
471
- }
472
- //#endregion
473
- //#region node/vite/icons.ts
474
- function createIconsPlugin(options, pluginOptions) {
475
- return Icons({
476
- defaultClass: "slidev-icon",
477
- collectionsNodeResolvePath: options.utils.iconsResolvePath,
478
- ...pluginOptions.icons
479
- });
480
- }
481
- //#endregion
482
- //#region node/virtual/deprecated.ts
483
- /**
484
- * Kept for backward compatibility, use #slidev/slides instead
485
- *
486
- * @deprecated
487
- */
488
- const templateLegacyRoutes = {
489
- id: "/@slidev/routes",
490
- getContent() {
491
- return [`export { slides } from '#slidev/slides'`, `console.warn('[slidev] #slidev/routes is deprecated, use #slidev/slides instead')`].join("\n");
492
- }
493
- };
494
- /**
495
- * Kept for backward compatibility, use #slidev/title-renderer instead
496
- *
497
- * @deprecated
498
- */
499
- const templateLegacyTitles = {
500
- id: "/@slidev/titles.md",
501
- getContent() {
502
- return `
503
- <script setup lang="ts">
504
- import TitleRenderer from '#slidev/title-renderer'
505
- defineProps<{ no: number | string }>()
506
- console.warn('/@slidev/titles.md is deprecated, import from #slidev/title-renderer instead')
507
- <\/script>
508
-
509
- <TitleRenderer :no="no" />
510
- `;
511
- }
512
- };
513
- //#endregion
514
- //#region node/virtual/titles.ts
515
- const templateTitleRendererMd = {
516
- id: "/@slidev/title-renderer.md",
517
- getContent({ data }) {
518
- const lines = data.slides.map(({ title }, i) => `<template ${i === 0 ? "v-if" : "v-else-if"}="no === ${i + 1}">\n\n${title}\n\n</template>`);
519
- lines.push(`<script setup lang="ts">`, `import { useSlideContext } from '@slidev/client/context.ts'`, `import { computed } from 'vue'`, `const props = defineProps<{ no?: number | string }>()`, `const { $page } = useSlideContext()`, `const no = computed(() => +(props.no ?? $page.value))`, `<\/script>`);
520
- return lines.join("\n");
521
- }
522
- };
523
- const templateTitleRenderer = {
524
- id: "/@slidev/title-renderer",
525
- async getContent() {
526
- return "export { default } from \"/@slidev/title-renderer.md\"";
527
- }
528
- };
529
- //#endregion
530
- //#region node/vite/importGuard.ts
531
- const virtualSlideMarkdownIds = new Set([templateTitleRendererMd.id, templateLegacyTitles.id]);
532
- function createSlideImportGuardPlugin() {
533
- let config;
534
- let allowRoots = [];
535
- return {
536
- name: "slidev:slide-import-guard",
537
- configResolved(resolved) {
538
- config = resolved;
539
- allowRoots = config.server.fs.allow.map(normalizeFsPath);
540
- },
541
- async transform(code, id) {
542
- if (!isSlideMarkdownId(id) || !config?.server.fs.strict) return null;
543
- const importer = filePathFromId(id) ?? id;
544
- await Promise.all(extractImportSources(code, id).map(async ({ value, start }) => {
545
- const resolved = await this.resolve(value, importer, { skipSelf: true });
546
- if (!resolved || resolved.external) return;
547
- const filePath = filePathFromId(resolved.id);
548
- if (!filePath) return;
549
- const normalized = normalizeFsPath(filePath);
550
- if (isAllowedFile(normalized, allowRoots)) return;
551
- if (isBareImport(value) && isDependencyFile(normalized)) return;
552
- this.error(`[slidev] Import "${value}" from slide Markdown resolves outside of Vite server.fs.allow: ${normalized}`, start);
553
- }));
554
- return null;
555
- }
556
- };
557
- }
558
- function isSlideMarkdownId(id) {
559
- const clean = cleanUrl$1(id);
560
- return regexSlideSourceId.test(clean) || virtualSlideMarkdownIds.has(clean);
561
- }
562
- function filePathFromId(id) {
563
- const clean = cleanUrl$1(id);
564
- if (clean.startsWith("file://")) return fileURLToPath(clean);
565
- if (clean.startsWith("/@fs/")) return clean.slice(4);
566
- if (clean.startsWith("/@")) return null;
567
- if (path.isAbsolute(clean)) return clean;
568
- return null;
569
- }
570
- function isAllowedFile(filePath, allowRoots) {
571
- return allowRoots.some((root) => isFileInRoot(root, filePath));
572
- }
573
- function extractImportSources(code, id) {
574
- const result = parseSync(id, code);
575
- const sources = [];
576
- for (const item of result.module.staticImports) sources.push({
577
- value: item.moduleRequest.value,
578
- start: item.moduleRequest.start
579
- });
580
- for (const item of result.module.staticExports) for (const entry of item.entries) if (entry.moduleRequest) sources.push({
581
- value: entry.moduleRequest.value,
582
- start: entry.moduleRequest.start
583
- });
584
- for (const item of result.module.dynamicImports) {
585
- const source = parseStringLiteral(code.slice(item.moduleRequest.start, item.moduleRequest.end));
586
- if (source) sources.push({
587
- value: source,
588
- start: item.moduleRequest.start
589
- });
590
- }
591
- return sources;
592
- }
593
- function cleanUrl$1(id) {
594
- return id.replace(/[?#].*$/, "");
595
- }
596
- function normalizeFsPath(filePath) {
597
- const absolute = path.resolve(filePath);
598
- if (!existsSync(absolute)) return normalizeMissingFsPath(absolute);
599
- return realpathSync.native(absolute);
600
- }
601
- function normalizeMissingFsPath(filePath) {
602
- const dir = path.dirname(filePath);
603
- if (dir === filePath) return filePath;
604
- if (existsSync(dir)) return path.join(realpathSync.native(dir), path.basename(filePath));
605
- return path.join(normalizeMissingFsPath(dir), path.basename(filePath));
606
- }
607
- function isFileInRoot(root, filePath) {
608
- const relative = path.relative(root, filePath);
609
- return relative === "" || !!relative && !relative.startsWith("..") && !path.isAbsolute(relative);
610
- }
611
- function isDependencyFile(filePath) {
612
- return filePath.split(/[\\/]/).includes("node_modules");
613
- }
614
- function isBareImport(source) {
615
- return /^(?![a-z]:)[\w@](?!.*:\/\/)/i.test(source);
616
- }
617
- function parseStringLiteral(raw) {
618
- const trimmed = raw.trim();
619
- const quote = trimmed[0];
620
- if (quote !== "\"" && quote !== "'" && quote !== "`") return null;
621
- if (trimmed.at(-1) !== quote) return null;
622
- if (quote === "`" && trimmed.includes("${")) return null;
623
- try {
624
- if (quote === "\"") return JSON.parse(trimmed);
625
- return JSON.parse(`"${trimmed.slice(1, -1).replace(/"/g, "\\\"")}"`);
626
- } catch {
627
- return trimmed.slice(1, -1);
628
- }
627
+ handler(code, id) {
628
+ if (!id.match(regexSlideSourceId)) return;
629
+ return code.replace("if (_rerender_only)", "if (false)");
630
+ }
631
+ }
632
+ };
633
+ }
634
+ //#endregion
635
+ //#region node/vite/icons.ts
636
+ function createIconsPlugin(options, pluginOptions) {
637
+ return Icons({
638
+ defaultClass: "slidev-icon",
639
+ collectionsNodeResolvePath: options.utils.iconsResolvePath,
640
+ ...pluginOptions.icons
641
+ });
629
642
  }
630
643
  //#endregion
631
644
  //#region node/vite/inspect.ts
@@ -685,7 +698,7 @@ function createLayoutWrapperPlugin({ data, utils }) {
685
698
  }
686
699
  //#endregion
687
700
  //#region package.json
688
- var version = "52.16.0";
701
+ var version = "52.17.1";
689
702
  //#endregion
690
703
  //#region node/integrations/addons.ts
691
704
  async function resolveAddons(addonsInConfig) {
@@ -901,7 +914,7 @@ function resolveShikiOptions(options) {
901
914
  if (mergedOptions.themes) {
902
915
  for (const theme of Object.values(mergedOptions.themes)) if (typeof theme === "object" && theme?.name) themeInput[theme.name] = theme;
903
916
  }
904
- const languageNames = new Set([
917
+ const languageNames = /* @__PURE__ */ new Set([
905
918
  "markdown",
906
919
  "vue",
907
920
  "javascript",
@@ -981,22 +994,31 @@ const debug = createDebug("slidev:options");
981
994
  async function resolveOptions(entryOptions, mode) {
982
995
  const entry = await resolveEntry(entryOptions.entry);
983
996
  const rootsInfo = await getRoots(entry);
984
- const loaded = await parser.load({
997
+ const initialLoaded = await parser.load({
985
998
  userRoot: rootsInfo.userRoot,
986
- roots: [rootsInfo.userRoot]
999
+ roots: [rootsInfo.userRoot],
1000
+ allowedRoots: uniq([rootsInfo.userWorkspaceRoot, rootsInfo.userRoot])
987
1001
  }, entry, void 0, mode);
988
- let themeRaw = entryOptions.theme || loaded.headmatter.theme;
1002
+ let themeRaw = entryOptions.theme || initialLoaded.headmatter.theme;
989
1003
  themeRaw = themeRaw === null ? "none" : themeRaw || "default";
990
1004
  const [theme, themeRoot] = await resolveTheme(themeRaw, entry);
991
1005
  const themeRoots = themeRoot ? [themeRoot] : [];
992
1006
  const themeMeta = themeRoot ? await getThemeMeta(theme, themeRoot) : void 0;
993
- const config = parser.resolveConfig(loaded.headmatter, themeMeta, entryOptions.entry);
994
- const addonRoots = await resolveAddons(config.addons);
1007
+ const initialConfig = parser.resolveConfig(initialLoaded.headmatter, themeMeta, entryOptions.entry);
1008
+ const addonRoots = await resolveAddons(initialConfig.addons);
995
1009
  const roots = uniq([
996
1010
  ...themeRoots,
997
1011
  ...addonRoots,
998
1012
  rootsInfo.userRoot
999
1013
  ]);
1014
+ const loaded = await parser.load({
1015
+ userRoot: rootsInfo.userRoot,
1016
+ roots,
1017
+ allowedRoots: uniq([rootsInfo.userWorkspaceRoot, ...roots])
1018
+ }, entry, void 0, mode);
1019
+ const config = parser.resolveConfig(loaded.headmatter, themeMeta, entryOptions.entry);
1020
+ config.theme = themeRaw;
1021
+ config.addons = initialConfig.addons;
1000
1022
  if (entryOptions.download) config.download ||= entryOptions.download;
1001
1023
  if (entryOptions.routerMode) config.routerMode = entryOptions.routerMode;
1002
1024
  debug({
@@ -1071,6 +1093,7 @@ function getDefine(options) {
1071
1093
  __DEV__: options.mode === "dev",
1072
1094
  __SLIDEV_CLIENT_ROOT__: toAtFS(options.clientRoot),
1073
1095
  __SLIDEV_HASH_ROUTE__: options.data.config.routerMode === "hash",
1096
+ __SLIDEV_MEMORY_ROUTE__: options.data.config.routerMode === "memory" && options.mode !== "export",
1074
1097
  __SLIDEV_FEATURE_DRAWINGS__: matchMode(options.data.config.drawings.enabled),
1075
1098
  __SLIDEV_FEATURE_EDITOR__: options.mode === "dev" && options.data.config.editor !== false,
1076
1099
  __SLIDEV_FEATURE_DRAWINGS_PERSIST__: !!options.data.config.drawings.persist,
@@ -1083,6 +1106,7 @@ function getDefine(options) {
1083
1106
  ].includes(options.data.config.download),
1084
1107
  __SLIDEV_FEATURE_BROWSER_EXPORTER__: matchMode(options.data.config.browserExporter),
1085
1108
  __SLIDEV_FEATURE_WAKE_LOCK__: matchMode(options.data.config.wakeLock),
1109
+ __SLIDEV_FEATURE_PWA__: matchMode(options.data.config.pwa),
1086
1110
  __SLIDEV_HAS_SERVER__: options.mode !== "build"
1087
1111
  }, (v, k) => [v, JSON.stringify(k)]);
1088
1112
  }
@@ -1658,6 +1682,7 @@ function createSlidesLoader(options, serverOptions) {
1658
1682
  frontmatter: void 0,
1659
1683
  source: void 0,
1660
1684
  importChain: void 0,
1685
+ images: slide.images ?? slide.source?.images,
1661
1686
  ...mode === "build" ? {
1662
1687
  raw: "",
1663
1688
  content: "",
@@ -2035,7 +2060,12 @@ function createMonacoWriterPlugin({ userRoot }) {
2035
2060
  console.error(`[Slidev] Unauthorized file write: ${file}`);
2036
2061
  return;
2037
2062
  }
2038
- const filepath = path.join(userRoot, file);
2063
+ const filepath = path.resolve(userRoot, file);
2064
+ const rel = path.relative(userRoot, filepath);
2065
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
2066
+ console.error(`[slidev] Refusing monaco write outside project root: ${file}`);
2067
+ return;
2068
+ }
2039
2069
  console.log("[Slidev] Writing file:", filepath);
2040
2070
  await fs$1.writeFile(filepath, content, "utf-8");
2041
2071
  }
@@ -2120,7 +2150,7 @@ function findRegion(lines, regionName) {
2120
2150
  return null;
2121
2151
  }
2122
2152
  const RE_SNIPPET_IMPORT = /^<<<[ \t]*(\S.*?)(#[\w-]+)?[ \t]*(?:[ \t](\S+?))?[ \t]*(\{.*)?$/;
2123
- function resolveSnippetImport(lineText, userRoot, slide) {
2153
+ function resolveSnippetImport(lineText, userRoot, slide, allowedRoots = [userRoot]) {
2124
2154
  const match = lineText.trimStart().match(RE_SNIPPET_IMPORT);
2125
2155
  if (!match) return null;
2126
2156
  let [, filepath = "", regionName = "", lang = "", meta = ""] = match;
@@ -2128,6 +2158,7 @@ function resolveSnippetImport(lineText, userRoot, slide) {
2128
2158
  const src = slash(filepath.startsWith("@/") ? path.resolve(userRoot, filepath.slice(2)) : path.resolve(dir, filepath));
2129
2159
  lang = lang.trim() || path.extname(filepath).slice(1);
2130
2160
  meta = meta.trim();
2161
+ if (!isPathInsideRoots(src, allowedRoots)) throw new Error(`Code snippet path escapes the project root: ${src}`);
2131
2162
  if (!(fs.existsSync(src) && fs.statSync(src).isFile())) throw new Error(`Code snippet path not found: ${src}`);
2132
2163
  let content = fs.readFileSync(src, "utf8");
2133
2164
  if (regionName) {
@@ -2143,7 +2174,12 @@ function resolveSnippetImport(lineText, userRoot, slide) {
2143
2174
  src
2144
2175
  };
2145
2176
  }
2146
- function MarkdownItSnippet(md, { userRoot, data: { watchFiles, slides } }) {
2177
+ function MarkdownItSnippet(md, { userRoot, userWorkspaceRoot, roots, data: { watchFiles, slides } }) {
2178
+ const allowedRoots = [...new Set([
2179
+ userWorkspaceRoot,
2180
+ userRoot,
2181
+ ...roots ?? []
2182
+ ].filter(Boolean))];
2147
2183
  md.block.ruler.before("fence", "snippet_import", (state, startLine, _endLine, silent) => {
2148
2184
  const pos = state.bMarks[startLine] + state.tShift[startLine];
2149
2185
  const max = state.eMarks[startLine];
@@ -2156,7 +2192,7 @@ function MarkdownItSnippet(md, { userRoot, data: { watchFiles, slides } }) {
2156
2192
  console.warn(yellow(`[markdown-it-snippet] Snippet syntax is not supported in ${state.env.id || "unknown source"}. Skipped.`));
2157
2193
  return false;
2158
2194
  }
2159
- const snippet = resolveSnippetImport(lineText, userRoot, slide);
2195
+ const snippet = resolveSnippetImport(lineText, userRoot, slide, allowedRoots);
2160
2196
  if (!snippet) return false;
2161
2197
  const { content, filepath, src } = snippet;
2162
2198
  let { lang, meta } = snippet;
@@ -2579,6 +2615,505 @@ async function createMarkdownPlugin(options, { markdown: mdOptions }) {
2579
2615
  });
2580
2616
  }
2581
2617
  //#endregion
2618
+ //#region node/mcp/operations.ts
2619
+ /**
2620
+ * Resolve a rendered slide (1-based, as displayed in the presentation) or
2621
+ * throw a descriptive error.
2622
+ */
2623
+ function resolveSlide(data, no) {
2624
+ const slide = data.slides[no - 1];
2625
+ if (!slide) throw new Error(`Slide ${no} does not exist. The deck has ${data.slides.length} slides (1-${data.slides.length}).`);
2626
+ return slide;
2627
+ }
2628
+ function getMarkdown(data, source) {
2629
+ const md = data.markdownFiles[source.filepath];
2630
+ if (!md) throw new Error(`Markdown file not loaded: ${source.filepath}`);
2631
+ return md;
2632
+ }
2633
+ function assertNotEntryHeadmatter(data, source, action) {
2634
+ if (source.filepath === data.entry.filepath && data.entry.slides.indexOf(source) === 0) throw new Error(`Cannot ${action} the first slide of the entry file: its frontmatter is the deck headmatter (global configuration). Edit its content with the update tool instead, or operate on the following slides.`);
2635
+ }
2636
+ /**
2637
+ * Apply a `SlidePatch` to the slide source and save the markdown file.
2638
+ *
2639
+ * Note this only mutates the *source* slide (`slide.source`), not the
2640
+ * rendered `SlideInfo`, so a running dev server will pick up the change from
2641
+ * disk like an external edit and push HMR updates to connected clients.
2642
+ */
2643
+ async function applySlidePatch(data, no, patch) {
2644
+ const slide = resolveSlide(data, no);
2645
+ const source = slide.source;
2646
+ if (patch.content != null) source.content = patch.content;
2647
+ if (patch.frontmatterRaw != null) if (patch.frontmatterRaw.trim() === "") source.frontmatterDoc = source.frontmatterStyle = void 0;
2648
+ else {
2649
+ const parsed = YAML.parseDocument(patch.frontmatterRaw);
2650
+ if (parsed.errors.length) throw new Error(`Invalid YAML frontmatter: ${parsed.errors.map((e) => e.message).join("; ")}`);
2651
+ source.frontmatterDoc = parsed;
2652
+ }
2653
+ if (patch.note != null) source.note = patch.note;
2654
+ if (patch.frontmatter) updateFrontmatterPatch(source, patch.frontmatter);
2655
+ parser.prettifySlide(source);
2656
+ return {
2657
+ slide,
2658
+ fileContent: await parser.save(getMarkdown(data, source))
2659
+ };
2660
+ }
2661
+ /**
2662
+ * Insert a new slide after an existing one, in the same markdown file.
2663
+ */
2664
+ async function insertSlide(data, options) {
2665
+ const anchor = resolveSlide(data, options.after);
2666
+ const md = getMarkdown(data, anchor.source);
2667
+ const frontmatter = options.frontmatter ?? {};
2668
+ const doc = Object.keys(frontmatter).length > 0 ? new YAML.Document(frontmatter) : void 0;
2669
+ const source = {
2670
+ filepath: anchor.source.filepath,
2671
+ index: 0,
2672
+ start: 0,
2673
+ contentStart: 0,
2674
+ end: 0,
2675
+ raw: "",
2676
+ revision: "",
2677
+ content: options.content,
2678
+ contentRaw: options.content,
2679
+ frontmatter,
2680
+ frontmatterDoc: doc,
2681
+ frontmatterStyle: doc ? "frontmatter" : void 0,
2682
+ frontmatterRaw: doc?.toString(),
2683
+ note: options.note?.trim() || void 0
2684
+ };
2685
+ parser.prettifySlide(source);
2686
+ const anchorIdx = md.slides.indexOf(anchor.source);
2687
+ md.slides.splice(anchorIdx + 1, 0, source);
2688
+ const fileContent = await parser.save(md);
2689
+ return {
2690
+ source,
2691
+ filepath: md.filepath,
2692
+ fileContent
2693
+ };
2694
+ }
2695
+ /**
2696
+ * Remove a slide from its source markdown file.
2697
+ */
2698
+ async function removeSlide(data, no) {
2699
+ const slide = resolveSlide(data, no);
2700
+ assertNotEntryHeadmatter(data, slide.source, "remove");
2701
+ const md = getMarkdown(data, slide.source);
2702
+ const idx = md.slides.indexOf(slide.source);
2703
+ if (idx < 0) throw new Error(`Slide ${no} is out of sync with its source file. Try again.`);
2704
+ md.slides.splice(idx, 1);
2705
+ const fileContent = await parser.save(md);
2706
+ return {
2707
+ removed: slide,
2708
+ filepath: md.filepath,
2709
+ fileContent
2710
+ };
2711
+ }
2712
+ /**
2713
+ * Move a slide before or after another slide within the same markdown file.
2714
+ */
2715
+ async function moveSlide(data, options) {
2716
+ const { from, before, after } = options;
2717
+ if (before == null === (after == null)) throw new Error("Specify exactly one of `before` or `after`.");
2718
+ const anchorNo = before ?? after;
2719
+ if (anchorNo === from) throw new Error("The `before`/`after` anchor must be a different slide than `from`.");
2720
+ const slide = resolveSlide(data, from);
2721
+ const anchor = resolveSlide(data, anchorNo);
2722
+ if (slide.source.filepath !== anchor.source.filepath) throw new Error(`Cannot move a slide across markdown files: slide ${from} is in "${slide.source.filepath}" but slide ${anchorNo} is in "${anchor.source.filepath}" (imported with \`src:\`). Move it within its own file, or edit the \`src:\` imports in the entry file manually.`);
2723
+ assertNotEntryHeadmatter(data, slide.source, "move");
2724
+ if (before != null) assertNotEntryHeadmatter(data, anchor.source, "insert a slide before");
2725
+ const md = getMarkdown(data, slide.source);
2726
+ const fromIdx = md.slides.indexOf(slide.source);
2727
+ if (fromIdx < 0) throw new Error(`Slide ${from} is out of sync with its source file. Try again.`);
2728
+ md.slides.splice(fromIdx, 1);
2729
+ const anchorIdx = md.slides.indexOf(anchor.source);
2730
+ if (anchorIdx < 0) throw new Error(`Slide ${anchorNo} is out of sync with its source file. Try again.`);
2731
+ md.slides.splice(before != null ? anchorIdx : anchorIdx + 1, 0, slide.source);
2732
+ const fileContent = await parser.save(md);
2733
+ return {
2734
+ moved: slide,
2735
+ anchor,
2736
+ filepath: md.filepath,
2737
+ fileContent
2738
+ };
2739
+ }
2740
+ //#endregion
2741
+ //#region node/mcp/server.ts
2742
+ function result(data) {
2743
+ return { content: [{
2744
+ type: "text",
2745
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
2746
+ }] };
2747
+ }
2748
+ function slideSummary(slide) {
2749
+ return {
2750
+ no: slide.index + 1,
2751
+ title: slide.title ?? null,
2752
+ ...slide.frontmatter.layout ? { layout: slide.frontmatter.layout } : {},
2753
+ file: slide.source.filepath,
2754
+ hasNote: !!slide.source.note,
2755
+ ...slide.importChain?.length ? { importedBySrcDirective: true } : {}
2756
+ };
2757
+ }
2758
+ const noSchema = z.number().int().min(1).describe("Slide number (1-based, as displayed in the presentation)");
2759
+ const frontmatterSchema = z.record(z.any()).optional().describe("Slide frontmatter (YAML headmatter of the slide) as an object, e.g. { \"layout\": \"two-cols\" }");
2760
+ /**
2761
+ * Create the Slidev MCP server exposing tools for agents to inspect, edit,
2762
+ * and (with a running dev server) navigate a slides deck.
2763
+ *
2764
+ * The same tool set is served over the dev server HTTP endpoint (`/__mcp`)
2765
+ * and the `slidev mcp` stdio command.
2766
+ */
2767
+ function createSlidevMcpServer(ctx) {
2768
+ const server = new McpServer({
2769
+ name: "slidev",
2770
+ version: ctx.version
2771
+ }, { instructions: [
2772
+ "Tools for working with a Slidev (https://sli.dev) slides deck.",
2773
+ "A deck is a Markdown file where slides are separated by `---`; each slide can have YAML frontmatter, Markdown/Vue content, and a speaker note (trailing HTML comment).",
2774
+ "Slides are addressed by their rendered 1-based number, matching the slide numbers shown in the presentation.",
2775
+ "After editing tools run, a running dev server hot-reloads the presentation automatically."
2776
+ ].join("\n") });
2777
+ server.registerTool("slidev-get-info", {
2778
+ title: "Get deck info",
2779
+ description: "Get an overview of the Slidev deck: entry file, title, slide count, markdown files, and (when a dev server is running) the server URL and current position of the live presentation.",
2780
+ annotations: { readOnlyHint: true }
2781
+ }, async () => {
2782
+ const data = await ctx.getData();
2783
+ const nav = ctx.nav?.getState();
2784
+ return result({
2785
+ slidevVersion: ctx.version,
2786
+ entry: ctx.entry,
2787
+ title: data.headmatter.title ?? data.slides[0]?.title ?? null,
2788
+ theme: data.config?.theme ?? data.headmatter.theme ?? null,
2789
+ totalSlides: data.slides.length,
2790
+ markdownFiles: Object.keys(data.markdownFiles),
2791
+ ...ctx.getServerUrl?.() ? { server: {
2792
+ url: ctx.getServerUrl(),
2793
+ currentPage: nav?.page || null,
2794
+ currentClicks: nav?.page ? nav.clicks : null
2795
+ } } : {}
2796
+ });
2797
+ });
2798
+ server.registerTool("slidev-list-slides", {
2799
+ title: "List slides",
2800
+ description: "List all slides of the deck with their number, title, layout, and source file. Slides hidden with `hide`/`disabled` frontmatter are not included.",
2801
+ annotations: { readOnlyHint: true }
2802
+ }, async () => {
2803
+ return result((await ctx.getData()).slides.map(slideSummary));
2804
+ });
2805
+ server.registerTool("slidev-get-slide", {
2806
+ title: "Get slide",
2807
+ description: "Get the full source of one slide: frontmatter, Markdown content, and speaker note.",
2808
+ inputSchema: { no: noSchema },
2809
+ annotations: { readOnlyHint: true }
2810
+ }, async ({ no }) => {
2811
+ const slide = resolveSlide(await ctx.getData(), no);
2812
+ return result({
2813
+ ...slideSummary(slide),
2814
+ frontmatter: slide.source.frontmatter,
2815
+ content: slide.source.content.trim(),
2816
+ note: slide.source.note ?? null,
2817
+ ...slide.importChain?.length ? { importedBy: slide.importChain.map((s) => `${s.filepath}#${s.index + 1}`) } : {}
2818
+ });
2819
+ });
2820
+ server.registerTool("slidev-update-slide", {
2821
+ title: "Update slide",
2822
+ description: "Update the content, speaker note, and/or frontmatter of a slide. Only the provided fields are changed. Pass an empty string to clear the content or note. In `frontmatter`, only the given keys are patched; pass `null` as a value to delete that key.",
2823
+ inputSchema: {
2824
+ no: noSchema,
2825
+ content: z.string().optional().describe("New Markdown content of the slide (without frontmatter and note)"),
2826
+ note: z.string().optional().describe("New speaker note (Markdown, stored as a trailing HTML comment)"),
2827
+ frontmatter: frontmatterSchema
2828
+ }
2829
+ }, async ({ no, content, note, frontmatter }) => {
2830
+ if (content == null && note == null && frontmatter == null) throw new Error("Nothing to update: provide at least one of `content`, `note`, or `frontmatter`.");
2831
+ const { slide } = await applySlidePatch(await ctx.getData(), no, {
2832
+ content,
2833
+ note,
2834
+ frontmatter
2835
+ });
2836
+ return result(`Updated slide ${no} in ${slide.source.filepath}.`);
2837
+ });
2838
+ server.registerTool("slidev-insert-slide", {
2839
+ title: "Insert slide",
2840
+ description: "Insert a new slide after an existing slide (into the same markdown file). To add a slide at the very end, pass the last slide number.",
2841
+ inputSchema: {
2842
+ after: z.number().int().min(1).describe("Slide number (1-based) after which the new slide is inserted"),
2843
+ content: z.string().describe("Markdown content of the new slide"),
2844
+ frontmatter: frontmatterSchema,
2845
+ note: z.string().optional().describe("Speaker note of the new slide")
2846
+ }
2847
+ }, async ({ after, content, frontmatter, note }) => {
2848
+ const { filepath } = await insertSlide(await ctx.getData(), {
2849
+ after,
2850
+ content,
2851
+ frontmatter,
2852
+ note
2853
+ });
2854
+ return result(`Inserted a new slide after slide ${after} in ${filepath}. Slide numbers after it have shifted; list the slides again if needed.`);
2855
+ });
2856
+ server.registerTool("slidev-remove-slide", {
2857
+ title: "Remove slide",
2858
+ description: "Remove a slide from the deck (deletes it from its source markdown file).",
2859
+ inputSchema: { no: noSchema },
2860
+ annotations: { destructiveHint: true }
2861
+ }, async ({ no }) => {
2862
+ const { removed, filepath } = await removeSlide(await ctx.getData(), no);
2863
+ return result(`Removed slide ${no}${removed.title ? ` ("${removed.title}")` : ""} from ${filepath}. Slide numbers after it have shifted; list the slides again if needed.`);
2864
+ });
2865
+ server.registerTool("slidev-move-slide", {
2866
+ title: "Move slide",
2867
+ description: "Move a slide before or after another slide to reorder the deck. Both slides must be in the same markdown file. To swap two adjacent slides, move one after the other.",
2868
+ inputSchema: {
2869
+ from: z.number().int().min(1).describe("Slide number (1-based) of the slide to move"),
2870
+ before: z.number().int().min(1).optional().describe("Move the slide right before this slide number"),
2871
+ after: z.number().int().min(1).optional().describe("Move the slide right after this slide number")
2872
+ }
2873
+ }, async ({ from, before, after }) => {
2874
+ const { filepath } = await moveSlide(await ctx.getData(), {
2875
+ from,
2876
+ before,
2877
+ after
2878
+ });
2879
+ return result(`Moved slide ${from} ${before != null ? `before slide ${before}` : `after slide ${after}`} in ${filepath}. Slide numbers have shifted; list the slides again if needed.`);
2880
+ });
2881
+ if (ctx.nav) {
2882
+ const nav = ctx.nav;
2883
+ server.registerTool("slidev-goto-slide", {
2884
+ title: "Go to slide",
2885
+ description: "Navigate the live presentation (all connected browsers) to a given slide, e.g. to visually verify a slide after editing it.",
2886
+ inputSchema: {
2887
+ no: noSchema,
2888
+ clicks: z.number().int().min(0).optional().describe("Click animation step to reveal (defaults to 0)")
2889
+ },
2890
+ annotations: { idempotentHint: true }
2891
+ }, async ({ no, clicks }) => {
2892
+ resolveSlide(await ctx.getData(), no);
2893
+ nav.go(no, clicks ?? 0);
2894
+ return result(`Navigated the presentation to slide ${no}${clicks ? ` (click ${clicks})` : ""}.`);
2895
+ });
2896
+ }
2897
+ return server;
2898
+ }
2899
+ //#endregion
2900
+ //#region node/integrations/drawings.ts
2901
+ /**
2902
+ * Whether `key` is a safe bare slide id that can be joined onto `dir` without
2903
+ * escaping it. `loadDrawings` only ever produces numeric keys, so writes
2904
+ * should only ever accept numeric keys too.
2905
+ */
2906
+ function isSafeDrawingKey(dir, key) {
2907
+ if (!/^\d+$/.test(key)) return false;
2908
+ const rel = relative(dir, join(dir, `${key}.svg`));
2909
+ return !rel.startsWith("..") && !isAbsolute(rel);
2910
+ }
2911
+ function resolveDrawingsDir(options) {
2912
+ return options.data.config.drawings.persist ? resolve(dirname(options.entry), options.data.config.drawings.persist) : void 0;
2913
+ }
2914
+ async function loadDrawings(options) {
2915
+ const dir = resolveDrawingsDir(options);
2916
+ if (!dir || !existsSync(dir)) return {};
2917
+ const files = await fg("*.svg", {
2918
+ onlyFiles: true,
2919
+ cwd: dir,
2920
+ absolute: true,
2921
+ suppressErrors: true
2922
+ });
2923
+ const obj = {};
2924
+ await Promise.all(files.map(async (path) => {
2925
+ const num = +basename(path, ".svg");
2926
+ if (Number.isNaN(num)) return;
2927
+ const lines = (await fs$1.readFile(path, "utf8")).split(/\n/g);
2928
+ obj[num.toString()] = lines.slice(1, -1).join("\n");
2929
+ }));
2930
+ return obj;
2931
+ }
2932
+ async function writeDrawings(options, drawing) {
2933
+ const dir = resolveDrawingsDir(options);
2934
+ if (!dir) return;
2935
+ const width = options.data.config.canvasWidth;
2936
+ const SVG_HEAD = `<svg width="${width}" height="${Math.round(width / options.data.config.aspectRatio)}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">`;
2937
+ await fs$1.mkdir(dir, { recursive: true });
2938
+ return Promise.all(Object.entries(drawing).map(async ([key, value]) => {
2939
+ if (!value) return;
2940
+ if (!isSafeDrawingKey(dir, key)) {
2941
+ console.warn(`[slidev] Ignoring drawing with unsafe key: ${key}`);
2942
+ return;
2943
+ }
2944
+ const svg = `${SVG_HEAD}\n${value}\n</svg>`;
2945
+ await fs$1.writeFile(join(dir, `${key}.svg`), svg, "utf-8");
2946
+ }));
2947
+ }
2948
+ //#endregion
2949
+ //#region node/integrations/snapshots.ts
2950
+ function resolveSnapshotsDir(options) {
2951
+ return resolve(dirname(options.entry), ".slidev/snapshots");
2952
+ }
2953
+ async function loadSnapshots(options) {
2954
+ const dir = resolveSnapshotsDir(options);
2955
+ const file = join(dir, "snapshots.json");
2956
+ if (!dir || !existsSync(file)) return {};
2957
+ return JSON.parse(await fs$1.readFile(file, "utf8"));
2958
+ }
2959
+ async function writeSnapshots(options, data) {
2960
+ const dir = resolveSnapshotsDir(options);
2961
+ if (!dir) return;
2962
+ await fs$1.mkdir(dir, { recursive: true });
2963
+ await fs$1.writeFile(join(dir, "snapshots.json"), JSON.stringify(data, null, 2), "utf-8");
2964
+ }
2965
+ //#endregion
2966
+ //#region node/vite/serverRef.ts
2967
+ const serverRefStates = /* @__PURE__ */ new WeakMap();
2968
+ /**
2969
+ * Access the server-side state shared with the clients via
2970
+ * `vite-plugin-vue-server-ref` (e.g. `nav` holds the current position of the
2971
+ * live presentation).
2972
+ */
2973
+ function getServerRefState(options) {
2974
+ return serverRefStates.get(options);
2975
+ }
2976
+ async function createServerRefPlugin(options, pluginOptions) {
2977
+ const state = {
2978
+ sync: false,
2979
+ nav: {
2980
+ page: 0,
2981
+ clicks: 0,
2982
+ timer: {
2983
+ status: "stopped",
2984
+ slides: {},
2985
+ startedAt: 0,
2986
+ pausedAt: 0
2987
+ }
2988
+ },
2989
+ drawings: await loadDrawings(options),
2990
+ snapshots: await loadSnapshots(options),
2991
+ ...pluginOptions.serverRef?.state
2992
+ };
2993
+ serverRefStates.set(options, state);
2994
+ return ServerRef({
2995
+ debug: false,
2996
+ state,
2997
+ onChanged(key, data, patch, timestamp) {
2998
+ pluginOptions.serverRef?.onChanged?.(key, data, patch, timestamp);
2999
+ if (options.data.config.drawings.persist && key === "drawings") writeDrawings(options, patch ?? data);
3000
+ if (key === "snapshots") writeSnapshots(options, data);
3001
+ }
3002
+ });
3003
+ }
3004
+ const SERVER_REF_WS_EVENT = "vue-server-ref";
3005
+ const SERVER_REF_NAV_MODULE_IDS = ["/@server-reactive/nav", "/@server-ref/nav"];
3006
+ /**
3007
+ * Serves the Slidev MCP (Model Context Protocol) server on the dev server at
3008
+ * `/__mcp` (streamable HTTP transport), so AI agents can inspect, edit, and
3009
+ * navigate the slides. Can be disabled with `mcp: false` in the headmatter.
3010
+ */
3011
+ function createMcpPlugin(options) {
3012
+ return {
3013
+ name: "slidev:mcp",
3014
+ apply: "serve",
3015
+ configureServer(server) {
3016
+ if (options.mode !== "dev") return;
3017
+ const ctx = {
3018
+ version,
3019
+ entry: options.entry,
3020
+ getData: () => options.data,
3021
+ getServerUrl: () => server.resolvedUrls?.local[0],
3022
+ nav: {
3023
+ getState: () => {
3024
+ const nav = getServerRefState(options)?.nav;
3025
+ return {
3026
+ page: nav?.page ?? 0,
3027
+ clicks: nav?.clicks ?? 0
3028
+ };
3029
+ },
3030
+ go: (page, clicks) => navigateClients(server, options, page, clicks)
3031
+ }
3032
+ };
3033
+ server.middlewares.use(async (req, res, next) => {
3034
+ if (req.url?.split("?")[0]?.replace(/\/$/, "") !== "/__mcp") return next();
3035
+ if (options.data.config.mcp === false) {
3036
+ res.statusCode = 403;
3037
+ res.setHeader("Content-Type", "application/json");
3038
+ res.end(JSON.stringify({
3039
+ jsonrpc: "2.0",
3040
+ error: {
3041
+ code: -32e3,
3042
+ message: "The Slidev MCP server is disabled (`mcp: false` in the headmatter)"
3043
+ },
3044
+ id: null
3045
+ }));
3046
+ return;
3047
+ }
3048
+ try {
3049
+ const mcp = createSlidevMcpServer(ctx);
3050
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
3051
+ res.on("close", () => {
3052
+ transport.close();
3053
+ mcp.close();
3054
+ });
3055
+ await mcp.connect(transport);
3056
+ await transport.handleRequest(req, res);
3057
+ } catch (e) {
3058
+ console.error("[slidev] Error handling MCP request:", e);
3059
+ if (!res.headersSent) {
3060
+ res.statusCode = 500;
3061
+ res.setHeader("Content-Type", "application/json");
3062
+ res.end(JSON.stringify({
3063
+ jsonrpc: "2.0",
3064
+ error: {
3065
+ code: -32603,
3066
+ message: "Internal server error"
3067
+ },
3068
+ id: null
3069
+ }));
3070
+ }
3071
+ }
3072
+ });
3073
+ }
3074
+ };
3075
+ }
3076
+ /**
3077
+ * Drive all connected clients to the given slide by patching the shared
3078
+ * `nav` state, mimicking what `vite-plugin-vue-server-ref` does when a
3079
+ * presenter or remote-control client navigates.
3080
+ */
3081
+ function navigateClients(server, options, page, clicks) {
3082
+ const time = Date.now();
3083
+ const nav = getServerRefState(options)?.nav;
3084
+ if (nav) {
3085
+ nav.page = page;
3086
+ nav.clicks = clicks;
3087
+ nav.lastUpdate = {
3088
+ id: "slidev-mcp",
3089
+ type: "presenter",
3090
+ time
3091
+ };
3092
+ }
3093
+ for (const id of SERVER_REF_NAV_MODULE_IDS) {
3094
+ const module = server.moduleGraph.getModuleById(id);
3095
+ if (module) server.moduleGraph.invalidateModule(module);
3096
+ }
3097
+ for (const type of ["presenter", "viewer"]) server.hot.send({
3098
+ type: "custom",
3099
+ event: SERVER_REF_WS_EVENT,
3100
+ data: {
3101
+ key: "nav",
3102
+ source: "slidev-mcp",
3103
+ patch: {
3104
+ page,
3105
+ clicks,
3106
+ lastUpdate: {
3107
+ id: "slidev-mcp",
3108
+ type,
3109
+ time
3110
+ }
3111
+ },
3112
+ timestamp: time
3113
+ }
3114
+ });
3115
+ }
3116
+ //#endregion
2582
3117
  //#region node/vite/monacoTypes.ts
2583
3118
  function createMonacoTypesLoader({ userRoot, utils }) {
2584
3119
  return {
@@ -2648,6 +3183,34 @@ function createPatchMonacoSourceMapPlugin() {
2648
3183
  };
2649
3184
  }
2650
3185
  //#endregion
3186
+ //#region node/vite/pwa.ts
3187
+ function createPWAPlugin(options) {
3188
+ const matchMode = (mode) => mode === true || mode === options.mode;
3189
+ const enabled = matchMode(options.data.config.pwa);
3190
+ const base = options.base ?? "/";
3191
+ const title = options.data.config.title || "Slidev";
3192
+ return VitePWA({
3193
+ disable: !enabled,
3194
+ registerType: "autoUpdate",
3195
+ injectRegister: null,
3196
+ base,
3197
+ workbox: {
3198
+ maximumFileSizeToCacheInBytes: 100 * 1024 * 1024,
3199
+ globPatterns: ["**/*.{js,css,html,woff,woff2,ttf,png,jpg,jpeg,svg,gif,webp,avif,ico,mp4,webm,ogv,mov,m4v,mp3,wav,ogg,oga,m4a,aac,flac,opus,weba}"],
3200
+ navigateFallback: "index.html",
3201
+ navigateFallbackDenylist: [/^\/(presenter|notes|overview|print|export)/]
3202
+ },
3203
+ manifest: {
3204
+ name: title,
3205
+ short_name: "Slidev",
3206
+ start_url: base,
3207
+ display: "fullscreen",
3208
+ theme_color: "#121212",
3209
+ background_color: "#121212"
3210
+ }
3211
+ });
3212
+ }
3213
+ //#endregion
2651
3214
  //#region node/vite/remoteAssets.ts
2652
3215
  async function createRemoteAssetsPlugin({ data: { config }, mode }, pluginOptions) {
2653
3216
  if (!(config.remoteAssets === true || config.remoteAssets === mode)) return;
@@ -2667,86 +3230,6 @@ async function createRemoteAssetsPlugin({ data: { config }, mode }, pluginOption
2667
3230
  });
2668
3231
  }
2669
3232
  //#endregion
2670
- //#region node/integrations/drawings.ts
2671
- function resolveDrawingsDir(options) {
2672
- return options.data.config.drawings.persist ? resolve(dirname(options.entry), options.data.config.drawings.persist) : void 0;
2673
- }
2674
- async function loadDrawings(options) {
2675
- const dir = resolveDrawingsDir(options);
2676
- if (!dir || !existsSync(dir)) return {};
2677
- const files = await fg("*.svg", {
2678
- onlyFiles: true,
2679
- cwd: dir,
2680
- absolute: true,
2681
- suppressErrors: true
2682
- });
2683
- const obj = {};
2684
- await Promise.all(files.map(async (path) => {
2685
- const num = +basename(path, ".svg");
2686
- if (Number.isNaN(num)) return;
2687
- const lines = (await fs$1.readFile(path, "utf8")).split(/\n/g);
2688
- obj[num.toString()] = lines.slice(1, -1).join("\n");
2689
- }));
2690
- return obj;
2691
- }
2692
- async function writeDrawings(options, drawing) {
2693
- const dir = resolveDrawingsDir(options);
2694
- if (!dir) return;
2695
- const width = options.data.config.canvasWidth;
2696
- const SVG_HEAD = `<svg width="${width}" height="${Math.round(width / options.data.config.aspectRatio)}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">`;
2697
- await fs$1.mkdir(dir, { recursive: true });
2698
- return Promise.all(Object.entries(drawing).map(async ([key, value]) => {
2699
- if (!value) return;
2700
- const svg = `${SVG_HEAD}\n${value}\n</svg>`;
2701
- await fs$1.writeFile(join(dir, `${key}.svg`), svg, "utf-8");
2702
- }));
2703
- }
2704
- //#endregion
2705
- //#region node/integrations/snapshots.ts
2706
- function resolveSnapshotsDir(options) {
2707
- return resolve(dirname(options.entry), ".slidev/snapshots");
2708
- }
2709
- async function loadSnapshots(options) {
2710
- const dir = resolveSnapshotsDir(options);
2711
- const file = join(dir, "snapshots.json");
2712
- if (!dir || !existsSync(file)) return {};
2713
- return JSON.parse(await fs$1.readFile(file, "utf8"));
2714
- }
2715
- async function writeSnapshots(options, data) {
2716
- const dir = resolveSnapshotsDir(options);
2717
- if (!dir) return;
2718
- await fs$1.mkdir(dir, { recursive: true });
2719
- await fs$1.writeFile(join(dir, "snapshots.json"), JSON.stringify(data, null, 2), "utf-8");
2720
- }
2721
- //#endregion
2722
- //#region node/vite/serverRef.ts
2723
- async function createServerRefPlugin(options, pluginOptions) {
2724
- return ServerRef({
2725
- debug: false,
2726
- state: {
2727
- sync: false,
2728
- nav: {
2729
- page: 0,
2730
- clicks: 0,
2731
- timer: {
2732
- status: "stopped",
2733
- slides: {},
2734
- startedAt: 0,
2735
- pausedAt: 0
2736
- }
2737
- },
2738
- drawings: await loadDrawings(options),
2739
- snapshots: await loadSnapshots(options),
2740
- ...pluginOptions.serverRef?.state
2741
- },
2742
- onChanged(key, data, patch, timestamp) {
2743
- pluginOptions.serverRef?.onChanged?.(key, data, patch, timestamp);
2744
- if (options.data.config.drawings.persist && key === "drawings") writeDrawings(options, patch ?? data);
2745
- if (key === "snapshots") writeSnapshots(options, data);
2746
- }
2747
- });
2748
- }
2749
- //#endregion
2750
3233
  //#region node/vite/staticCopy.ts
2751
3234
  async function createStaticCopyPlugin({ themeRoots, addonRoots }, pluginOptions) {
2752
3235
  const publicDirs = [...themeRoots, ...addonRoots].map((i) => join(i, "public")).filter(existsSync);
@@ -2805,7 +3288,7 @@ const RE_VUE_QUERY_VUE = /\.vue\?vue/;
2805
3288
  const RE_VUE_QUERY_V = /\.vue\?v=/;
2806
3289
  const RE_MD_EXT = /\.md$/;
2807
3290
  const RE_MD_QUERY_VUE = /\.md\?vue/;
2808
- const customElements = new Set([
3291
+ const customElements = /* @__PURE__ */ new Set([
2809
3292
  "annotation",
2810
3293
  "math",
2811
3294
  "menclose",
@@ -2863,6 +3346,7 @@ async function createVuePlugin(_options, pluginOptions) {
2863
3346
  function ViteSlidevPlugin(options, pluginOptions = {}, serverOptions = {}) {
2864
3347
  return Promise.all([
2865
3348
  createSlidesLoader(options, serverOptions),
3349
+ createMcpPlugin(options),
2866
3350
  createMarkdownPlugin(options, pluginOptions),
2867
3351
  createLayoutWrapperPlugin(options),
2868
3352
  createContextInjectionPlugin(),
@@ -2881,6 +3365,7 @@ function ViteSlidevPlugin(options, pluginOptions = {}, serverOptions = {}) {
2881
3365
  createStaticCopyPlugin(options, pluginOptions),
2882
3366
  createInspectPlugin(options, pluginOptions),
2883
3367
  createPatchMonacoSourceMapPlugin(),
3368
+ createPWAPlugin(options),
2884
3369
  setupVitePlugins(options)
2885
3370
  ]);
2886
3371
  }
@@ -2920,4 +3405,4 @@ async function createServer$1(options, viteConfig = {}, serverOptions) {
2920
3405
  return await createServer(await resolveViteConfigs(options, { optimizeDeps: { entries: [join(options.clientRoot, "main.ts")] } }, viteConfig, "serve", serverOptions));
2921
3406
  }
2922
3407
  //#endregion
2923
- export { resolveOptions as a, resolveTheme as c, updateFrontmatterPatch as d, createDataUtils as i, version as l, resolveViteConfigs as n, parser as o, ViteSlidevPlugin as r, getThemeMeta as s, createServer$1 as t, loadSetups as u };
3408
+ export { createDataUtils as a, getThemeMeta as c, loadSetups as d, updateFrontmatterPatch as f, createSlidevMcpServer as i, resolveTheme as l, resolveViteConfigs as n, resolveOptions as o, ViteSlidevPlugin as r, parser as s, createServer$1 as t, version as u };