@slidev/cli 52.17.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,4 +1,4 @@
1
- import { n as resolveViteConfigs } from "./serve-CgOXLPUI.mjs";
1
+ import { n as resolveViteConfigs } from "./serve-bAegrEOc.mjs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { build as build$1 } from "vite";
4
4
  import { existsSync } from "node:fs";
@@ -51,7 +51,7 @@ async function build(options, viteConfig = {}, args) {
51
51
  dev: true
52
52
  }));
53
53
  server.listen(port);
54
- const { exportSlides } = await import("./export-CU2GL9dm.mjs");
54
+ const { exportSlides } = await import("./export-_6QaMWg2.mjs");
55
55
  const tempDir = resolve(outDir, "temp");
56
56
  await fs$1.mkdir(tempDir, { recursive: true });
57
57
  await exportSlides({
@@ -92,7 +92,7 @@ async function build(options, viteConfig = {}, args) {
92
92
  "true",
93
93
  "auto"
94
94
  ].includes(options.data.config.download)) {
95
- const { exportSlides, getExportOptions } = await import("./export-CU2GL9dm.mjs");
95
+ const { exportSlides, getExportOptions } = await import("./export-_6QaMWg2.mjs");
96
96
  const port = 12445;
97
97
  const app = connect();
98
98
  const server = http.createServer(app);
package/dist/cli.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import { i as resolveEntry, r as isInstalledGlobally } from "./resolver-BBT539iz.mjs";
2
- import { c as getThemeMeta, d as loadSetups, f as updateFrontmatterPatch, l as resolveTheme, o as resolveOptions, s as parser, t as createServer, u as version } from "./serve-CgOXLPUI.mjs";
2
+ import { c as getThemeMeta, d as loadSetups, f as updateFrontmatterPatch, l as resolveTheme, o as resolveOptions, s as parser, t as createServer, u as version } from "./serve-bAegrEOc.mjs";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import fs from "node:fs/promises";
6
6
  import { blue, bold, cyan, cyanBright, dim, gray, green, underline, yellow } from "ansis";
7
7
  import { injectPreparserExtensionLoader } from "@slidev/parser/fs";
8
8
  import equal from "fast-deep-equal";
9
- import { exec } from "node:child_process";
9
+ import { execFile } from "node:child_process";
10
10
  import os from "node:os";
11
11
  import * as readline from "node:readline";
12
12
  import { verifyConfig } from "@slidev/parser";
@@ -202,7 +202,7 @@ cli.command("* [entry]", "Start a local server for Slidev", (args) => commonOpti
202
202
  name: "e",
203
203
  fullname: "edit",
204
204
  action() {
205
- exec(`code "${entry}"`);
205
+ execFile(process.env.EDITOR || "code", [entry]);
206
206
  }
207
207
  },
208
208
  {
@@ -300,7 +300,7 @@ cli.command("build [entry..]", "Build hostable SPA", (args) => exportOptions(com
300
300
  describe: "enable the inspect plugin for debugging"
301
301
  }).strict().help(), async (args) => {
302
302
  const { entry, theme, base, download, out, inspect, "without-notes": withoutNotes, "router-mode": routerMode } = args;
303
- const { build } = await import("./build-Cszi5hDI.mjs");
303
+ const { build } = await import("./build-CsQUCdtJ.mjs");
304
304
  for (const entryFile of entry) {
305
305
  const options = await resolveOptions({
306
306
  entry: entryFile,
@@ -329,7 +329,7 @@ cli.command("format [entry..]", "Format the markdown file", (args) => commonOpti
329
329
  }
330
330
  });
331
331
  cli.command("mcp [entry]", "Start an MCP (Model Context Protocol) server over stdio for AI agents to inspect and edit the slides", (args) => commonOptions(args).strict().help(), async ({ entry }) => {
332
- const { startMcpStdioServer } = await import("./stdio-LsewwOTO.mjs");
332
+ const { startMcpStdioServer } = await import("./stdio-CjXw2-ks.mjs");
333
333
  await startMcpStdioServer(await resolveEntry(entry));
334
334
  });
335
335
  cli.command("theme [subcommand]", "Theme related operations", (command) => {
@@ -369,7 +369,7 @@ cli.command("theme [subcommand]", "Theme related operations", (command) => {
369
369
  });
370
370
  cli.command("export [entry..]", "Export slides to PDF", (args) => exportOptions(commonOptions(args)).strict().help(), async (args) => {
371
371
  const { entry, theme } = args;
372
- const { exportSlides, getExportOptions } = await import("./export-CU2GL9dm.mjs");
372
+ const { exportSlides, getExportOptions } = await import("./export-_6QaMWg2.mjs");
373
373
  const candidatePort = await getPort(12445);
374
374
  let warned = false;
375
375
  for (const entryFile of entry) {
@@ -421,7 +421,7 @@ cli.command("export-notes [entry..]", "Export slide notes to PDF", (args) => arg
421
421
  type: "number",
422
422
  describe: "wait for the specified ms before exporting"
423
423
  }).strict().help(), async ({ entry, output, timeout, wait }) => {
424
- const { exportNotes } = await import("./export-CU2GL9dm.mjs");
424
+ const { exportNotes } = await import("./export-_6QaMWg2.mjs");
425
425
  const candidatePort = await getPort(12445);
426
426
  for (const entryFile of entry) {
427
427
  const options = await resolveOptions({ entry: entryFile }, "export");
@@ -78,26 +78,29 @@ async function exportNotes({ port = 18724, base = "/", output = "notes", timeout
78
78
  const progress = createSlidevProgress(true);
79
79
  progress.start(1);
80
80
  if (!output.endsWith(".pdf")) output = `${output}.pdf`;
81
- await page.goto(`http://localhost:${port}${base}presenter/print`, {
82
- waitUntil: "networkidle",
83
- timeout
84
- });
85
- await page.waitForLoadState("networkidle");
86
- await page.emulateMedia({ media: "screen" });
87
- if (wait) await page.waitForTimeout(wait);
88
- await page.pdf({
89
- path: output,
90
- margin: {
91
- left: 0,
92
- top: 0,
93
- right: 0,
94
- bottom: 0
95
- },
96
- printBackground: true,
97
- preferCSSPageSize: true
98
- });
99
- progress.stop();
100
- browser.close();
81
+ try {
82
+ await page.goto(`http://localhost:${port}${base}presenter/print`, {
83
+ waitUntil: "networkidle",
84
+ timeout
85
+ });
86
+ await page.waitForLoadState("networkidle");
87
+ await page.emulateMedia({ media: "screen" });
88
+ if (wait) await page.waitForTimeout(wait);
89
+ await page.pdf({
90
+ path: output,
91
+ margin: {
92
+ left: 0,
93
+ top: 0,
94
+ right: 0,
95
+ bottom: 0
96
+ },
97
+ printBackground: true,
98
+ preferCSSPageSize: true
99
+ });
100
+ } finally {
101
+ progress.stop();
102
+ await browser.close();
103
+ }
101
104
  return output;
102
105
  }
103
106
  async function exportSlides({ port = 18724, total = 0, range, format = "pdf", output = "slides", slides, base = "/", timeout = 3e4, wait = 0, dark = false, routerMode = "history", width = 1920, height = 1080, withClicks = false, executablePath = void 0, withToc = false, perSlide = false, scale = 1, waitUntil, omitBackground = false }) {
@@ -113,13 +116,16 @@ async function exportSlides({ port = 18724, total = 0, range, format = "pdf", ou
113
116
  })).newPage();
114
117
  const progress = createSlidevProgress(!perSlide);
115
118
  progress.start(pages.length);
116
- if (format === "pdf") await genPagePdf();
117
- else if (format === "png") await genPagePng(output);
118
- else if (format === "md") await genPageMd();
119
- else if (format === "pptx") await genPagePptx(await genPagePng(false));
120
- else throw new Error(`[slidev] Unsupported exporting format "${format}"`);
121
- progress.stop();
122
- browser.close();
119
+ try {
120
+ if (format === "pdf") await genPagePdf();
121
+ else if (format === "png") await genPagePng(output);
122
+ else if (format === "md") await genPageMd();
123
+ else if (format === "pptx") await genPagePptx(await genPagePng(false));
124
+ else throw new Error(`[slidev] Unsupported exporting format "${format}"`);
125
+ } finally {
126
+ progress.stop();
127
+ await browser.close();
128
+ }
123
129
  const relativeOutput = slash(relative(".", output));
124
130
  return relativeOutput.startsWith(".") ? relativeOutput : `./${relativeOutput}`;
125
131
  async function go(no, clicks) {
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as createDataUtils, o as resolveOptions, r as ViteSlidevPlugin, s as parser, t as createServer } from "./serve-CgOXLPUI.mjs";
1
+ import { a as createDataUtils, o as resolveOptions, r as ViteSlidevPlugin, s as parser, t as createServer } from "./serve-bAegrEOc.mjs";
2
2
  export { ViteSlidevPlugin, createDataUtils, createServer, parser, resolveOptions };
@@ -1,6 +1,6 @@
1
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";
@@ -70,7 +70,172 @@ function MarkdownItLink(md) {
70
70
  };
71
71
  }
72
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
73
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
+ }
74
239
  const RE_WHITESPACE_ONLY = /^\s*$/;
75
240
  const RE_QUOTED_STRING = /^(['"])(.*)\1$/;
76
241
  const RE_WHITESPACE = /\s+/g;
@@ -243,14 +408,6 @@ function createComponentsPlugin({ clientRoot, roots }, pluginOptions) {
243
408
  });
244
409
  }
245
410
  //#endregion
246
- //#region node/vite/common.ts
247
- const regexSlideReqPath = /^\/__slidev\/slides\/(\d+)\.json$/;
248
- const regexSlideFacadeId = /^\/@slidev\/slides\/(\d+)\/(md|frontmatter)($|\?)/;
249
- const regexSlideSourceId = /__slidev_(\d+)\.(md|frontmatter)$/;
250
- const templateInjectionMarker = "/* @slidev-injection */";
251
- const templateImportContextUtils = `import { useSlideContext as _useSlideContext, frontmatterToProps as _frontmatterToProps } from "@slidev/client/context.ts"`;
252
- const templateInitContext = `const { $slidev, $nav, $clicksContext, $clicks, $page, $renderContext, $frontmatter } = _useSlideContext()`;
253
- //#endregion
254
411
  //#region node/vite/contextInjection.ts
255
412
  const RE_EXPORT_DEFAULT_OBJECT = /export\s+default\s+\{/;
256
413
  const RE_INJECT_OPTION = /.*inject\s*:\s*([[{])/;
@@ -484,155 +641,6 @@ function createIconsPlugin(options, pluginOptions) {
484
641
  });
485
642
  }
486
643
  //#endregion
487
- //#region node/virtual/deprecated.ts
488
- /**
489
- * Kept for backward compatibility, use #slidev/slides instead
490
- *
491
- * @deprecated
492
- */
493
- const templateLegacyRoutes = {
494
- id: "/@slidev/routes",
495
- getContent() {
496
- return [`export { slides } from '#slidev/slides'`, `console.warn('[slidev] #slidev/routes is deprecated, use #slidev/slides instead')`].join("\n");
497
- }
498
- };
499
- /**
500
- * Kept for backward compatibility, use #slidev/title-renderer instead
501
- *
502
- * @deprecated
503
- */
504
- const templateLegacyTitles = {
505
- id: "/@slidev/titles.md",
506
- getContent() {
507
- return `
508
- <script setup lang="ts">
509
- import TitleRenderer from '#slidev/title-renderer'
510
- defineProps<{ no: number | string }>()
511
- console.warn('/@slidev/titles.md is deprecated, import from #slidev/title-renderer instead')
512
- <\/script>
513
-
514
- <TitleRenderer :no="no" />
515
- `;
516
- }
517
- };
518
- //#endregion
519
- //#region node/virtual/titles.ts
520
- const templateTitleRendererMd = {
521
- id: "/@slidev/title-renderer.md",
522
- getContent({ data }) {
523
- const lines = data.slides.map(({ title }, i) => `<template ${i === 0 ? "v-if" : "v-else-if"}="no === ${i + 1}">\n\n${title}\n\n</template>`);
524
- 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>`);
525
- return lines.join("\n");
526
- }
527
- };
528
- const templateTitleRenderer = {
529
- id: "/@slidev/title-renderer",
530
- async getContent() {
531
- return "export { default } from \"/@slidev/title-renderer.md\"";
532
- }
533
- };
534
- //#endregion
535
- //#region node/vite/importGuard.ts
536
- const virtualSlideMarkdownIds = /* @__PURE__ */ new Set([templateTitleRendererMd.id, templateLegacyTitles.id]);
537
- function createSlideImportGuardPlugin() {
538
- let config;
539
- let allowRoots = [];
540
- return {
541
- name: "slidev:slide-import-guard",
542
- configResolved(resolved) {
543
- config = resolved;
544
- allowRoots = config.server.fs.allow.map(normalizeFsPath);
545
- },
546
- async transform(code, id) {
547
- if (!isSlideMarkdownId(id) || !config?.server.fs.strict) return null;
548
- const importer = filePathFromId(id) ?? id;
549
- await Promise.all(extractImportSources(code, id).map(async ({ value, start }) => {
550
- const resolved = await this.resolve(value, importer, { skipSelf: true });
551
- if (!resolved || resolved.external) return;
552
- const filePath = filePathFromId(resolved.id);
553
- if (!filePath) return;
554
- const normalized = normalizeFsPath(filePath);
555
- if (isAllowedFile(normalized, allowRoots)) return;
556
- if (isBareImport(value) && isDependencyFile(normalized)) return;
557
- this.error(`[slidev] Import "${value}" from slide Markdown resolves outside of Vite server.fs.allow: ${normalized}`, start);
558
- }));
559
- return null;
560
- }
561
- };
562
- }
563
- function isSlideMarkdownId(id) {
564
- const clean = cleanUrl$1(id);
565
- return regexSlideSourceId.test(clean) || virtualSlideMarkdownIds.has(clean);
566
- }
567
- function filePathFromId(id) {
568
- const clean = cleanUrl$1(id);
569
- if (clean.startsWith("file://")) return fileURLToPath(clean);
570
- if (clean.startsWith("/@fs/")) return clean.slice(4);
571
- if (clean.startsWith("/@")) return null;
572
- if (path.isAbsolute(clean)) return clean;
573
- return null;
574
- }
575
- function isAllowedFile(filePath, allowRoots) {
576
- return allowRoots.some((root) => isFileInRoot(root, filePath));
577
- }
578
- function extractImportSources(code, id) {
579
- const result = parseSync(id, code);
580
- const sources = [];
581
- for (const item of result.module.staticImports) sources.push({
582
- value: item.moduleRequest.value,
583
- start: item.moduleRequest.start
584
- });
585
- for (const item of result.module.staticExports) for (const entry of item.entries) if (entry.moduleRequest) sources.push({
586
- value: entry.moduleRequest.value,
587
- start: entry.moduleRequest.start
588
- });
589
- for (const item of result.module.dynamicImports) {
590
- const source = parseStringLiteral(code.slice(item.moduleRequest.start, item.moduleRequest.end));
591
- if (source) sources.push({
592
- value: source,
593
- start: item.moduleRequest.start
594
- });
595
- }
596
- return sources;
597
- }
598
- function cleanUrl$1(id) {
599
- return id.replace(/[?#].*$/, "");
600
- }
601
- function normalizeFsPath(filePath) {
602
- const absolute = path.resolve(filePath);
603
- if (!existsSync(absolute)) return normalizeMissingFsPath(absolute);
604
- return realpathSync.native(absolute);
605
- }
606
- function normalizeMissingFsPath(filePath) {
607
- const dir = path.dirname(filePath);
608
- if (dir === filePath) return filePath;
609
- if (existsSync(dir)) return path.join(realpathSync.native(dir), path.basename(filePath));
610
- return path.join(normalizeMissingFsPath(dir), path.basename(filePath));
611
- }
612
- function isFileInRoot(root, filePath) {
613
- const relative = path.relative(root, filePath);
614
- return relative === "" || !!relative && !relative.startsWith("..") && !path.isAbsolute(relative);
615
- }
616
- function isDependencyFile(filePath) {
617
- return filePath.split(/[\\/]/).includes("node_modules");
618
- }
619
- function isBareImport(source) {
620
- return /^(?![a-z]:)[\w@](?!.*:\/\/)/i.test(source);
621
- }
622
- function parseStringLiteral(raw) {
623
- const trimmed = raw.trim();
624
- const quote = trimmed[0];
625
- if (quote !== "\"" && quote !== "'" && quote !== "`") return null;
626
- if (trimmed.at(-1) !== quote) return null;
627
- if (quote === "`" && trimmed.includes("${")) return null;
628
- try {
629
- if (quote === "\"") return JSON.parse(trimmed);
630
- return JSON.parse(`"${trimmed.slice(1, -1).replace(/"/g, "\\\"")}"`);
631
- } catch {
632
- return trimmed.slice(1, -1);
633
- }
634
- }
635
- //#endregion
636
644
  //#region node/vite/inspect.ts
637
645
  async function createInspectPlugin(options, pluginOptions) {
638
646
  if (!options.inspect) return;
@@ -690,7 +698,7 @@ function createLayoutWrapperPlugin({ data, utils }) {
690
698
  }
691
699
  //#endregion
692
700
  //#region package.json
693
- var version = "52.17.0";
701
+ var version = "52.17.1";
694
702
  //#endregion
695
703
  //#region node/integrations/addons.ts
696
704
  async function resolveAddons(addonsInConfig) {
@@ -986,22 +994,31 @@ const debug = createDebug("slidev:options");
986
994
  async function resolveOptions(entryOptions, mode) {
987
995
  const entry = await resolveEntry(entryOptions.entry);
988
996
  const rootsInfo = await getRoots(entry);
989
- const loaded = await parser.load({
997
+ const initialLoaded = await parser.load({
990
998
  userRoot: rootsInfo.userRoot,
991
- roots: [rootsInfo.userRoot]
999
+ roots: [rootsInfo.userRoot],
1000
+ allowedRoots: uniq([rootsInfo.userWorkspaceRoot, rootsInfo.userRoot])
992
1001
  }, entry, void 0, mode);
993
- let themeRaw = entryOptions.theme || loaded.headmatter.theme;
1002
+ let themeRaw = entryOptions.theme || initialLoaded.headmatter.theme;
994
1003
  themeRaw = themeRaw === null ? "none" : themeRaw || "default";
995
1004
  const [theme, themeRoot] = await resolveTheme(themeRaw, entry);
996
1005
  const themeRoots = themeRoot ? [themeRoot] : [];
997
1006
  const themeMeta = themeRoot ? await getThemeMeta(theme, themeRoot) : void 0;
998
- const config = parser.resolveConfig(loaded.headmatter, themeMeta, entryOptions.entry);
999
- const addonRoots = await resolveAddons(config.addons);
1007
+ const initialConfig = parser.resolveConfig(initialLoaded.headmatter, themeMeta, entryOptions.entry);
1008
+ const addonRoots = await resolveAddons(initialConfig.addons);
1000
1009
  const roots = uniq([
1001
1010
  ...themeRoots,
1002
1011
  ...addonRoots,
1003
1012
  rootsInfo.userRoot
1004
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;
1005
1022
  if (entryOptions.download) config.download ||= entryOptions.download;
1006
1023
  if (entryOptions.routerMode) config.routerMode = entryOptions.routerMode;
1007
1024
  debug({
@@ -2043,7 +2060,12 @@ function createMonacoWriterPlugin({ userRoot }) {
2043
2060
  console.error(`[Slidev] Unauthorized file write: ${file}`);
2044
2061
  return;
2045
2062
  }
2046
- 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
+ }
2047
2069
  console.log("[Slidev] Writing file:", filepath);
2048
2070
  await fs$1.writeFile(filepath, content, "utf-8");
2049
2071
  }
@@ -2128,7 +2150,7 @@ function findRegion(lines, regionName) {
2128
2150
  return null;
2129
2151
  }
2130
2152
  const RE_SNIPPET_IMPORT = /^<<<[ \t]*(\S.*?)(#[\w-]+)?[ \t]*(?:[ \t](\S+?))?[ \t]*(\{.*)?$/;
2131
- function resolveSnippetImport(lineText, userRoot, slide) {
2153
+ function resolveSnippetImport(lineText, userRoot, slide, allowedRoots = [userRoot]) {
2132
2154
  const match = lineText.trimStart().match(RE_SNIPPET_IMPORT);
2133
2155
  if (!match) return null;
2134
2156
  let [, filepath = "", regionName = "", lang = "", meta = ""] = match;
@@ -2136,6 +2158,7 @@ function resolveSnippetImport(lineText, userRoot, slide) {
2136
2158
  const src = slash(filepath.startsWith("@/") ? path.resolve(userRoot, filepath.slice(2)) : path.resolve(dir, filepath));
2137
2159
  lang = lang.trim() || path.extname(filepath).slice(1);
2138
2160
  meta = meta.trim();
2161
+ if (!isPathInsideRoots(src, allowedRoots)) throw new Error(`Code snippet path escapes the project root: ${src}`);
2139
2162
  if (!(fs.existsSync(src) && fs.statSync(src).isFile())) throw new Error(`Code snippet path not found: ${src}`);
2140
2163
  let content = fs.readFileSync(src, "utf8");
2141
2164
  if (regionName) {
@@ -2151,7 +2174,12 @@ function resolveSnippetImport(lineText, userRoot, slide) {
2151
2174
  src
2152
2175
  };
2153
2176
  }
2154
- 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))];
2155
2183
  md.block.ruler.before("fence", "snippet_import", (state, startLine, _endLine, silent) => {
2156
2184
  const pos = state.bMarks[startLine] + state.tShift[startLine];
2157
2185
  const max = state.eMarks[startLine];
@@ -2164,7 +2192,7 @@ function MarkdownItSnippet(md, { userRoot, data: { watchFiles, slides } }) {
2164
2192
  console.warn(yellow(`[markdown-it-snippet] Snippet syntax is not supported in ${state.env.id || "unknown source"}. Skipped.`));
2165
2193
  return false;
2166
2194
  }
2167
- const snippet = resolveSnippetImport(lineText, userRoot, slide);
2195
+ const snippet = resolveSnippetImport(lineText, userRoot, slide, allowedRoots);
2168
2196
  if (!snippet) return false;
2169
2197
  const { content, filepath, src } = snippet;
2170
2198
  let { lang, meta } = snippet;
@@ -2870,6 +2898,16 @@ function createSlidevMcpServer(ctx) {
2870
2898
  }
2871
2899
  //#endregion
2872
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
+ }
2873
2911
  function resolveDrawingsDir(options) {
2874
2912
  return options.data.config.drawings.persist ? resolve(dirname(options.entry), options.data.config.drawings.persist) : void 0;
2875
2913
  }
@@ -2899,6 +2937,10 @@ async function writeDrawings(options, drawing) {
2899
2937
  await fs$1.mkdir(dir, { recursive: true });
2900
2938
  return Promise.all(Object.entries(drawing).map(async ([key, value]) => {
2901
2939
  if (!value) return;
2940
+ if (!isSafeDrawingKey(dir, key)) {
2941
+ console.warn(`[slidev] Ignoring drawing with unsafe key: ${key}`);
2942
+ return;
2943
+ }
2902
2944
  const svg = `${SVG_HEAD}\n${value}\n</svg>`;
2903
2945
  await fs$1.writeFile(join(dir, `${key}.svg`), svg, "utf-8");
2904
2946
  }));
@@ -1,5 +1,5 @@
1
1
  import { n as getRoots } from "./resolver-BBT539iz.mjs";
2
- import { i as createSlidevMcpServer, s as parser, u as version } from "./serve-CgOXLPUI.mjs";
2
+ import { i as createSlidevMcpServer, s as parser, u as version } from "./serve-bAegrEOc.mjs";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  //#region node/mcp/stdio.ts
5
5
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/cli",
3
3
  "type": "module",
4
- "version": "52.17.0",
4
+ "version": "52.17.1",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -114,9 +114,9 @@
114
114
  "yaml": "^2.9.0",
115
115
  "yargs": "^18.0.0",
116
116
  "zod": "^3.25.76",
117
- "@slidev/parser": "52.17.0",
118
- "@slidev/client": "52.17.0",
119
- "@slidev/types": "52.17.0"
117
+ "@slidev/types": "52.17.1",
118
+ "@slidev/client": "52.17.1",
119
+ "@slidev/parser": "52.17.1"
120
120
  },
121
121
  "devDependencies": {
122
122
  "@hedgedoc/markdown-it-plugins": "^2.1.4",