import { createServer } from "./chunk-UVOSJW3S.js"; import { getThemeMeta, loadSetups, parser, resolveAddons, resolveOptions, resolveTheme, version } from "./chunk-7E4FQJTV.js"; import { getRoots, isInstalledGlobally, resolveEntry } from "./chunk-UNQ5DBLZ.js"; // node/cli.ts import { exec } from "node:child_process"; import os from "node:os"; import path from "node:path"; import process from "node:process"; import * as readline from "node:readline"; import { verifyConfig } from "@slidev/parser"; import equal from "fast-deep-equal"; import fs from "fs-extra"; import { getPort } from "get-port-please"; import { blue, bold, cyan, dim, gray, green, lightCyan, underline, yellow } from "kolorist"; import openBrowser from "open"; import yargs from "yargs"; // node/setups/preparser.ts import { uniq } from "@antfu/utils"; import { injectPreparserExtensionLoader } from "@slidev/parser/fs"; function setupPreparser() { injectPreparserExtensionLoader(async (headmatter, filepath, mode) => { const addons = Array.isArray(headmatter?.addons) ? headmatter.addons : []; const { userRoot } = await getRoots(); const roots = uniq([ ...await resolveAddons(addons), userRoot ]); const returns = await loadSetups(roots, "preparser.ts", [{ filepath, headmatter, mode }]); return returns.flat(); }); } // node/cli.ts var CONFIG_RESTART_FIELDS = [ "monaco", "routerMode", "fonts", "css", "mdc", "editor", "theme" ]; var FILES_CREATE_RESTART = [ "global-bottom.vue", "global-top.vue", "uno.config.js", "uno.config.ts", "unocss.config.js", "unocss.config.ts" ]; var FILES_CHANGE_RESTART = [ "setup/shiki.ts", "setup/katex.ts", "setup/preparser.ts" ]; setupPreparser(); var cli = yargs(process.argv.slice(2)).scriptName("slidev").usage("$0 [args]").version(version).strict().showHelpOnFail(false).alias("h", "help").alias("v", "version"); cli.command( "* [entry]", "Start a local server for Slidev", (args) => commonOptions(args).option("port", { alias: "p", type: "number", describe: "port" }).option("open", { alias: "o", default: false, type: "boolean", describe: "open in browser" }).option("remote", { type: "string", describe: "listen public host and enable remote control" }).option("tunnel", { default: false, type: "boolean", describe: "open a Cloudflare Quick Tunnel to make Slidev available on the internet" }).option("log", { default: "warn", type: "string", choices: ["error", "warn", "info", "silent"], describe: "log level" }).option("inspect", { default: false, type: "boolean", describe: "enable the inspect plugin for debugging" }).option("force", { alias: "f", default: false, type: "boolean", describe: "force the optimizer to ignore the cache and re-bundle " }).option("bind", { type: "string", default: "0.0.0.0", describe: "specify which IP addresses the server should listen on in remote mode" }).strict().help(), async ({ entry, theme, port: userPort, open, log, remote, tunnel, force, inspect, bind }) => { let server; let port = 3030; let lastRemoteUrl; let restartTimer; function restartServer() { clearTimeout(restartTimer); restartTimer = setTimeout(() => { console.log(yellow("\n restarting...\n")); initServer(); }, 500); } async function initServer() { if (server) await server.close(); const options = await resolveOptions({ entry, remote, theme, inspect }, "dev"); const host = remote !== void 0 ? bind : "localhost"; port = userPort || await getPort({ port: 3030, random: false, portRange: [3030, 4e3], host }); server = await createServer( options, { server: { port, strictPort: true, open, host, // @ts-expect-error Vite <= 4 force }, optimizeDeps: { // Vite 5 force }, logLevel: log }, { async loadData(loadedSource) { const { data: oldData, entry: entry2 } = options; const loaded = await parser.load(options.userRoot, entry2, loadedSource, "dev"); const themeRaw = theme || loaded.headmatter.theme || "default"; if (options.themeRaw !== themeRaw) { console.log(yellow("\n restarting on theme change\n")); restartServer(); return false; } const themeMeta = options.themeRoots[0] ? await getThemeMeta(themeRaw, options.themeRoots[0]) : void 0; const newData = { ...loaded, themeMeta, config: parser.resolveConfig(loaded.headmatter, themeMeta, entry2) }; if (CONFIG_RESTART_FIELDS.some((i) => !equal(newData.config[i], oldData.config[i]))) { console.log(yellow("\n restarting on config change\n")); restartServer(); return false; } if (newData.features.katex && !oldData.features.katex || newData.features.monaco && !oldData.features.monaco) { console.log(yellow("\n restarting on feature change\n")); restartServer(); return false; } return newData; } } ); await server.listen(); let tunnelUrl = ""; if (tunnel) { if (remote != null) tunnelUrl = await openTunnel(port); else console.log(yellow("\n --remote is required for tunneling, Cloudflare Quick Tunnel is not enabled.\n")); } let publicIp; if (remote) publicIp = await import("public-ip").then((r) => r.publicIpv4()); lastRemoteUrl = printInfo(options, port, remote, tunnelUrl, publicIp); } async function openTunnel(port2) { const { startTunnel } = await import("untun"); const tunnel2 = await startTunnel({ port: port2, acceptCloudflareNotice: true }); return await tunnel2?.getURL() ?? ""; } const SHORTCUTS = [ { name: "r", fullname: "restart", action() { initServer(); } }, { name: "o", fullname: "open", action() { openBrowser(`http://localhost:${port}`); } }, { name: "e", fullname: "edit", action() { exec(`code "${entry}"`); } }, { name: "q", fullname: "quit", action() { try { server?.close(); } finally { process.exit(); } } }, { name: "c", fullname: "qrcode", async action() { if (!lastRemoteUrl) return; await import("uqr").then(async (r) => { const code = r.renderUnicodeCompact(lastRemoteUrl); console.log(` ${dim(" QR Code for remote control: ")} ${blue(lastRemoteUrl)} `); console.log(code.split("\n").map((i) => ` ${i}`).join("\n")); const publicIp = await import("public-ip").then((r2) => r2.publicIpv4()); if (publicIp) console.log(` ${dim(" Public IP: ")} ${blue(publicIp)} `); }); } } ]; function bindShortcut() { process.stdin.resume(); process.stdin.setEncoding("utf8"); readline.emitKeypressEvents(process.stdin); if (process.stdin.isTTY) process.stdin.setRawMode(true); process.stdin.on("keypress", (str, key) => { if (key.ctrl && key.name === "c") { process.exit(); } else { const [sh] = SHORTCUTS.filter((item) => item.name === str); if (sh) { try { sh.action(); } catch (err) { console.error(`Failed to execute shortcut ${sh.fullname}`, err); } } } }); } initServer(); bindShortcut(); const { watch } = await import("chokidar"); const watcher = watch([ ...FILES_CREATE_RESTART, ...FILES_CHANGE_RESTART ], { ignored: ["node_modules", ".git"], ignoreInitial: true }); watcher.on("unlink", (file) => { console.log(yellow(` file ${file} removed, restarting... `)); restartServer(); }); watcher.on("add", (file) => { console.log(yellow(` file ${file} added, restarting... `)); restartServer(); }); watcher.on("change", (file) => { if (typeof file !== "string") return; if (FILES_CREATE_RESTART.includes(file)) return; console.log(yellow(` file ${file} changed, restarting... `)); restartServer(); }); } ); cli.command( "build [entry..]", "Build hostable SPA", (args) => exportOptions(commonOptions(args)).option("out", { alias: "o", type: "string", default: "dist", describe: "output dir" }).option("base", { type: "string", describe: "output base" }).option("download", { alias: "d", type: "boolean", describe: "allow download as PDF" }).option("inspect", { default: false, type: "boolean", describe: "enable the inspect plugin for debugging" }).strict().help(), async (args) => { const { entry, theme, base, download, out, inspect } = args; const { build } = await import("./build-N6BEUQKX.js"); for (const entryFile of entry) { const options = await resolveOptions({ entry: entryFile, theme, inspect, download }, "build"); printInfo(options); await build( options, { base, build: { outDir: entry.length === 1 ? out : path.join(out, path.basename(entryFile, ".md")) } }, { ...args, entry: entryFile } ); } } ); cli.command( "format [entry..]", "Format the markdown file", (args) => commonOptions(args).strict().help(), async ({ entry }) => { for (const entryFile of entry) { const md = await parser.parse(await fs.readFile(entryFile, "utf-8"), entryFile); parser.prettify(md); await parser.save(md); } } ); cli.command( "theme [subcommand]", "Theme related operations", (command) => { return command.command( "eject", "Eject current theme into local file system", (args) => commonOptions(args).option("dir", { type: "string", default: "theme" }), async ({ entry: entryRaw, dir, theme: themeInput }) => { const entry = await resolveEntry(entryRaw); const roots = await getRoots(entry); const data = await parser.load(roots.userRoot, entry); let themeRaw = themeInput || data.headmatter.theme; themeRaw = themeRaw === null ? "none" : themeRaw || "default"; if (themeRaw === "none") { console.error('Cannot eject theme "none"'); process.exit(1); } if ("/.".includes(themeRaw[0]) || themeRaw[0] !== "@" && themeRaw.includes("/")) { console.error("Theme is already ejected"); process.exit(1); } const [name, root] = await resolveTheme(themeRaw, entry); await fs.copy(root, path.resolve(dir), { filter: (i) => !/node_modules|.git/.test(path.relative(root, i)) }); const dirPath = `./${dir}`; const firstSlide = data.entry.slides[0]; firstSlide.frontmatter.theme = dirPath; parser.prettifySlide(firstSlide); await parser.save(data.entry); console.log(`Theme "${name}" ejected successfully to "${dirPath}"`); } ); }, () => { cli.showHelp(); process.exit(1); } ); cli.command( "export [entry..]", "Export slides to PDF", (args) => exportOptions(commonOptions(args)).strict().help(), async (args) => { const { entry, theme } = args; const { exportSlides, getExportOptions } = await import("./export-I2C456L6.js"); const port = await getPort(12445); let warned = false; for (const entryFile of entry) { const options = await resolveOptions({ entry: entryFile, theme }, "export"); if (options.data.config.browserExporter !== false && !warned) { warned = true; console.log(lightCyan("[Slidev] Try the new browser exporter!")); console.log( lightCyan("You can use the browser exporter instead by starting the dev server as normal and visit"), `${blue("localhost:")}${dim("")}${blue("/export")} ` ); } const server = await createServer( options, { server: { port }, clearScreen: false } ); await server.listen(port); printInfo(options); const result = await exportSlides({ port, ...getExportOptions({ ...args, entry: entryFile }, options) }); console.log(`${green(" \u2713 ")}${dim("exported to ")}./${result} `); server.close(); } process.exit(0); } ); cli.command( "export-notes [entry..]", "Export slide notes to PDF", (args) => args.positional("entry", { default: "slides.md", type: "string", describe: "path to the slides markdown entry" }).option("output", { type: "string", describe: "path to the output" }).option("timeout", { default: 3e4, type: "number", describe: "timeout for rendering the print page" }).option("wait", { default: 0, type: "number", describe: "wait for the specified ms before exporting" }).strict().help(), async ({ entry, output, timeout, wait }) => { const { exportNotes } = await import("./export-I2C456L6.js"); const port = await getPort(12445); for (const entryFile of entry) { const options = await resolveOptions({ entry: entryFile }, "export"); const server = await createServer( options, { server: { port }, clearScreen: false } ); await server.listen(port); printInfo(options); const result = await exportNotes({ port, output: output || (options.data.config.exportFilename ? `${options.data.config.exportFilename}-notes` : `${path.basename(entryFile, ".md")}-export-notes`), timeout, wait }); console.log(`${green(" \u2713 ")}${dim("exported to ")}./${result} `); server.close(); } process.exit(0); } ); cli.help().parse(); function commonOptions(args) { return args.positional("entry", { default: "slides.md", type: "string", describe: "path to the slides markdown entry" }).option("theme", { alias: "t", type: "string", describe: "override theme" }); } function exportOptions(args) { return args.option("output", { type: "string", describe: "path to the output" }).option("format", { type: "string", choices: ["pdf", "png", "pptx", "md"], describe: "output format" }).option("timeout", { type: "number", describe: "timeout for rendering the print page" }).option("wait", { type: "number", describe: "wait for the specified ms before exporting" }).option("wait-until", { type: "string", choices: ["networkidle", "load", "domcontentloaded", "none"], describe: "wait until the specified event before exporting each slide" }).option("range", { type: "string", describe: 'page ranges to export, for example "1,4-5,6"' }).option("dark", { type: "boolean", describe: "export as dark theme" }).option("with-clicks", { alias: "c", type: "boolean", describe: "export pages for every clicks" }).option("executable-path", { type: "string", describe: "executable to override playwright bundled browser" }).option("with-toc", { type: "boolean", describe: "export pages with outline" }).option("per-slide", { type: "boolean", describe: "slide slides slide by slide. Works better with global components, but will break cross slide links and TOC in PDF" }).option("scale", { type: "number", describe: "scale factor for image export" }).option("omit-background", { type: "boolean", describe: "export png pages without the default browser background" }); } function printInfo(options, port, remote, tunnelUrl, publicIp) { console.log(); console.log(); console.log(` ${cyan("\u25CF") + blue("\u25A0") + yellow("\u25B2")}`); console.log(`${bold(" Slidev")} ${blue(`v${version}`)} ${isInstalledGlobally.value ? yellow("(global)") : ""}`); console.log(); verifyConfig(options.data.config, options.data.themeMeta, (v) => console.warn(yellow(` ! ${v}`))); console.log(dim(" theme ") + (options.theme ? green(options.theme) : gray("none"))); console.log(dim(" css engine ") + blue("unocss")); console.log(dim(" entry ") + dim(path.normalize(path.dirname(options.entry)) + path.sep) + path.basename(options.entry)); if (port) { const query = remote ? `?password=${remote}` : ""; const presenterPath = `${options.data.config.routerMode === "hash" ? "/#/" : "/"}presenter/${query}`; const entryPath = `${options.data.config.routerMode === "hash" ? "/#/" : "/"}entry${query}/`; const overviewPath = `${options.data.config.routerMode === "hash" ? "/#/" : "/"}overview${query}/`; console.log(); console.log(`${dim(" public slide show ")} > ${cyan(`http://localhost:${bold(port)}/`)}`); if (query) console.log(`${dim(" private slide show ")} > ${cyan(`http://localhost:${bold(port)}/${query}`)}`); if (options.utils.define.__SLIDEV_FEATURE_PRESENTER__) console.log(`${dim(" presenter mode ")} > ${blue(`http://localhost:${bold(port)}${presenterPath}`)}`); console.log(`${dim(" slides overview ")} > ${blue(`http://localhost:${bold(port)}${overviewPath}`)}`); if (options.utils.define.__SLIDEV_FEATURE_BROWSER_EXPORTER__) console.log(`${dim(" export slides")} > ${blue(`http://localhost:${bold(port)}/export/`)}`); if (options.inspect) console.log(`${dim(" vite inspector")} > ${yellow(`http://localhost:${bold(port)}/__inspect/`)}`); let lastRemoteUrl = ""; if (remote !== void 0) { Object.values(os.networkInterfaces()).forEach((v) => (v || []).filter((details) => String(details.family).slice(-1) === "4" && !details.address.includes("127.0.0.1")).forEach(({ address }) => { lastRemoteUrl = `http://${address}:${port}${entryPath}`; console.log(`${dim(" remote control ")} > ${blue(lastRemoteUrl)}`); })); if (publicIp) { lastRemoteUrl = `http://${publicIp}:${port}${entryPath}`; console.log(`${dim(" remote control ")} > ${blue(lastRemoteUrl)}`); } if (tunnelUrl) { lastRemoteUrl = `${tunnelUrl}${entryPath}`; console.log(`${dim(" remote via tunnel")} > ${yellow(lastRemoteUrl)}`); } } else { console.log(`${dim(" remote control ")} > ${dim("pass --remote to enable")}`); } console.log(); console.log(`${dim(" shortcuts ")} > ${underline("r")}${dim("estart | ")}${underline("o")}${dim("pen | ")}${underline("e")}${dim("dit | ")}${underline("q")}${dim("uit")}${lastRemoteUrl ? ` | ${dim("qr")}${underline("c")}${dim("ode")}` : ""}`); return lastRemoteUrl; } }