feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+76
View File
@@ -0,0 +1,76 @@
import {
resolveViteConfigs
} from "./chunk-7E4FQJTV.js";
import "./chunk-UNQ5DBLZ.js";
// node/commands/build.ts
import http from "node:http";
import { resolve } from "node:path";
import connect from "connect";
import fs from "fs-extra";
import sirv from "sirv";
import { build as viteBuild } from "vite";
async function build(options, viteConfig = {}, args) {
const indexPath = resolve(options.userRoot, "index.html");
let originalIndexHTML;
if (fs.existsSync(indexPath))
originalIndexHTML = await fs.readFile(indexPath, "utf-8");
await fs.writeFile(indexPath, options.utils.indexHtml, "utf-8");
let config = void 0;
try {
const inlineConfig = await resolveViteConfigs(
options,
{
plugins: [
{
name: "resolve-config",
configResolved(_config) {
config = _config;
}
}
],
build: {
chunkSizeWarningLimit: 2e3
}
},
viteConfig,
"build"
);
await viteBuild(inlineConfig);
} finally {
if (originalIndexHTML != null)
await fs.writeFile(indexPath, originalIndexHTML, "utf-8");
else
await fs.unlink(indexPath);
}
const outDir = resolve(options.userRoot, config.build.outDir);
await fs.copyFile(resolve(outDir, "index.html"), resolve(outDir, "404.html"));
const redirectsPath = resolve(outDir, "_redirects");
if (!fs.existsSync(redirectsPath))
await fs.writeFile(redirectsPath, `${config.base}* ${config.base}index.html 200
`, "utf-8");
if ([true, "true", "auto"].includes(options.data.config.download)) {
const { exportSlides, getExportOptions } = await import("./export-I2C456L6.js");
const port = 12445;
const app = connect();
const server = http.createServer(app);
app.use(
config.base,
sirv(outDir, {
etag: true,
single: true,
dev: true
})
);
server.listen(port);
await exportSlides({
port,
base: config.base,
...getExportOptions(args, options, outDir, "slidev-exported.pdf")
});
server.close();
}
}
export {
build
};
File diff suppressed because it is too large Load Diff
+190
View File
@@ -0,0 +1,190 @@
// node/resolver.ts
import * as fs from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { parseNi, run } from "@antfu/ni";
import { ensurePrefix, slash } from "@antfu/utils";
import globalDirs from "global-directory";
import { underline, yellow } from "kolorist";
import { resolvePath } from "mlly";
import prompts from "prompts";
import { resolveGlobal } from "resolve-global";
import { findClosestPkgJsonPath, findDepPkgJsonPath } from "vitefu";
var cliRoot = fileURLToPath(new URL("..", import.meta.url));
var isInstalledGlobally = {};
async function resolveImportUrl(id) {
return toAtFS(await resolveImportPath(id, true));
}
function toAtFS(path) {
return `/@fs${ensurePrefix("/", slash(path))}`;
}
async function resolveImportPath(importName, ensure = false) {
try {
return await resolvePath(importName, {
url: import.meta.url
});
} catch {
}
if (isInstalledGlobally.value) {
try {
return resolveGlobal(importName);
} catch {
}
}
if (ensure)
throw new Error(`Failed to resolve package "${importName}"`);
}
async function findPkgRoot(dep, parent, ensure = false) {
const pkgJsonPath = await findDepPkgJsonPath(dep, parent);
const path = pkgJsonPath ? dirname(pkgJsonPath) : isInstalledGlobally.value ? await findGlobalPkgRoot(dep, false) : void 0;
if (ensure && !path)
throw new Error(`Failed to resolve package "${dep}"`);
return path;
}
async function findGlobalPkgRoot(name, ensure = false) {
const yarnPath = join(globalDirs.yarn.packages, name);
if (fs.existsSync(`${yarnPath}/package.json`))
return yarnPath;
const npmPath = join(globalDirs.npm.packages, name);
if (fs.existsSync(`${npmPath}/package.json`))
return npmPath;
if (ensure)
throw new Error(`Failed to resolve global package "${name}"`);
}
async function resolveEntry(entryRaw) {
if (!fs.existsSync(entryRaw) && !entryRaw.endsWith(".md") && !/[/\\]/.test(entryRaw))
entryRaw += ".md";
const entry = resolve(entryRaw);
if (!fs.existsSync(entry)) {
const { create } = await prompts({
name: "create",
type: "confirm",
initial: "Y",
message: `Entry file ${yellow(`"${entry}"`)} does not exist, do you want to create it?`
});
if (create)
fs.copyFileSync(resolve(cliRoot, "template.md"), entry);
else
process.exit(0);
}
return slash(entry);
}
function createResolver(type, officials) {
async function promptForInstallation(pkgName) {
const { confirm } = await prompts({
name: "confirm",
initial: "Y",
type: "confirm",
message: `The ${type} "${pkgName}" was not found ${underline(isInstalledGlobally.value ? "globally" : "in your project")}, do you want to install it now?`
});
if (!confirm)
process.exit(1);
if (isInstalledGlobally.value)
await run(parseNi, ["-g", pkgName]);
else
await run(parseNi, [pkgName]);
}
return async function(name, importer) {
const { userRoot } = await getRoots();
if (name === "none")
return ["", null];
if (name[0] === "/")
return [name, name];
if (name.startsWith("@/"))
return [name, resolve(userRoot, name.slice(2))];
if (name[0] === "." || name[0] !== "@" && name.includes("/"))
return [name, resolve(dirname(importer), name)];
if (name.startsWith(`@slidev/${type}-`) || name.startsWith(`slidev-${type}-`)) {
const pkgRoot = await findPkgRoot(name, importer);
if (!pkgRoot)
await promptForInstallation(name);
return [name, await findPkgRoot(name, importer, true)];
}
{
const possiblePkgNames = [
`@slidev/${type}-${name}`,
`slidev-${type}-${name}`,
name
];
for (const pkgName2 of possiblePkgNames) {
const pkgRoot = await findPkgRoot(pkgName2, importer);
if (pkgRoot)
return [pkgName2, pkgRoot];
}
}
const pkgName = officials[name] ?? (name[0] === "@" ? name : `slidev-${type}-${name}`);
await promptForInstallation(pkgName);
return [pkgName, await findPkgRoot(pkgName, importer, true)];
};
}
function getUserPkgJson(userRoot) {
const path = resolve(userRoot, "package.json");
if (fs.existsSync(path))
return JSON.parse(fs.readFileSync(path, "utf-8"));
return {};
}
function hasWorkspacePackageJSON(root) {
const path = join(root, "package.json");
try {
fs.accessSync(path, fs.constants.R_OK);
} catch {
return false;
}
const content = JSON.parse(fs.readFileSync(path, "utf-8")) || {};
return !!content.workspaces;
}
function hasRootFile(root) {
const ROOT_FILES = [
// '.git',
// https://pnpm.js.org/workspaces/
"pnpm-workspace.yaml"
// https://rushjs.io/pages/advanced/config_files/
// 'rush.json',
// https://nx.dev/latest/react/getting-started/nx-setup
// 'workspace.json',
// 'nx.json'
];
return ROOT_FILES.some((file) => fs.existsSync(join(root, file)));
}
function searchForWorkspaceRoot(current, root = current) {
if (hasRootFile(current))
return current;
if (hasWorkspacePackageJSON(current))
return current;
const dir = dirname(current);
if (!dir || dir === current)
return root;
return searchForWorkspaceRoot(dir, root);
}
var rootsInfo = null;
async function getRoots(entry) {
if (rootsInfo)
return rootsInfo;
if (!entry)
throw new Error("[slidev] Cannot find roots without entry");
const userRoot = dirname(entry);
isInstalledGlobally.value = slash(relative(userRoot, process.argv[1])).includes("/.pnpm/") || (await import("is-installed-globally")).default;
const clientRoot = await findPkgRoot("@slidev/client", cliRoot, true);
const closestPkgRoot = dirname(await findClosestPkgJsonPath(userRoot) || userRoot);
const userPkgJson = getUserPkgJson(closestPkgRoot);
const userWorkspaceRoot = searchForWorkspaceRoot(closestPkgRoot);
rootsInfo = {
cliRoot,
clientRoot,
userRoot,
userPkgJson,
userWorkspaceRoot
};
return rootsInfo;
}
export {
isInstalledGlobally,
resolveImportUrl,
toAtFS,
resolveImportPath,
resolveEntry,
createResolver,
getRoots
};
+29
View File
@@ -0,0 +1,29 @@
import {
resolveViteConfigs
} from "./chunk-7E4FQJTV.js";
// node/commands/serve.ts
import { join } from "node:path";
import process from "node:process";
import { createServer as createViteServer } from "vite";
async function createServer(options, viteConfig = {}, serverOptions) {
process.env.EDITOR = process.env.EDITOR || "code";
const inlineConfig = await resolveViteConfigs(
options,
{
optimizeDeps: {
entries: [
join(options.clientRoot, "main.ts")
]
}
},
viteConfig,
"serve",
serverOptions
);
return await createViteServer(inlineConfig);
}
export {
createServer
};
+2
View File
@@ -0,0 +1,2 @@
export { }
+602
View File
@@ -0,0 +1,602 @@
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("<port>")}${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;
}
}
+527
View File
@@ -0,0 +1,527 @@
import {
getRoots
} from "./chunk-UNQ5DBLZ.js";
// node/commands/export.ts
import { Buffer } from "node:buffer";
import path from "node:path";
import process from "node:process";
import { clearUndefined, slash } from "@antfu/utils";
import { outlinePdfFactory } from "@lillallol/outline-pdf";
import { parseRangeString } from "@slidev/parser/core";
import { Presets, SingleBar } from "cli-progress";
import fs from "fs-extra";
import { blue, cyan, dim, green, yellow } from "kolorist";
import { resolve } from "mlly";
import * as pdfLib from "pdf-lib";
import { PDFDocument } from "pdf-lib";
function addToTree(tree, info, slideIndexes, level = 1) {
const titleLevel = info.level;
if (titleLevel && titleLevel > level && tree.length > 0 && tree[tree.length - 1].titleLevel < titleLevel) {
addToTree(tree[tree.length - 1].children, info, slideIndexes, level + 1);
} else {
tree.push({
no: info.index,
children: [],
level,
titleLevel: titleLevel ?? level,
path: String(slideIndexes[info.index + 1]),
hideInToc: Boolean(info.frontmatter?.hideInToc),
title: info.title
});
}
}
function makeOutline(tree) {
return tree.map(({ title, path: path2, level, children }) => {
const rootOutline = title ? `${path2}|${"-".repeat(level - 1)}|${title}` : null;
const childrenOutline = makeOutline(children);
return childrenOutline.length > 0 ? `${rootOutline}
${childrenOutline}` : rootOutline;
}).filter((outline) => !!outline).join("\n");
}
function createSlidevProgress(indeterminate = false) {
function getSpinner(n = 0) {
return [cyan("\u25CF"), green("\u25C6"), blue("\u25A0"), yellow("\u25B2")][n % 4];
}
let current = 0;
let spinner = 0;
let timer;
const progress = new SingleBar({
clearOnComplete: true,
hideCursor: true,
format: ` {spin} ${yellow("rendering")}${indeterminate ? dim(yellow("...")) : " {bar} {value}/{total}"}`,
linewrap: false,
barsize: 30
}, Presets.shades_grey);
return {
bar: progress,
start(total) {
progress.start(total, 0, { spin: getSpinner(spinner) });
timer = setInterval(() => {
spinner += 1;
progress.update(current, { spin: getSpinner(spinner) });
}, 200);
},
update(v) {
current = v;
progress.update(v, { spin: getSpinner(spinner) });
},
stop() {
clearInterval(timer);
progress.stop();
}
};
}
async function exportNotes({
port = 18724,
base = "/",
output = "notes",
timeout = 3e4,
wait = 0
}) {
const { chromium } = await importPlaywright();
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const progress = createSlidevProgress(true);
progress.start(1);
if (!output.endsWith(".pdf"))
output = `${output}.pdf`;
await page.goto(`http://localhost:${port}${base}presenter/print`, { waitUntil: "networkidle", timeout });
await page.waitForLoadState("networkidle");
await page.emulateMedia({ media: "screen" });
if (wait)
await page.waitForTimeout(wait);
await page.pdf({
path: output,
margin: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
printBackground: true,
preferCSSPageSize: true
});
progress.stop();
browser.close();
return output;
}
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
}) {
const pages = parseRangeString(total, range);
const { chromium } = await importPlaywright();
const browser = await chromium.launch({
executablePath
});
const context = await browser.newContext({
viewport: {
width,
// Calculate height for every slides to be in the viewport to trigger the rendering of iframes (twitter, youtube...)
height: perSlide ? height : height * pages.length
},
deviceScaleFactor: scale
});
const page = await context.newPage();
const progress = createSlidevProgress(!perSlide);
async function go(no, clicks) {
const query = new URLSearchParams();
if (withClicks)
query.set("print", "clicks");
else
query.set("print", "true");
if (range)
query.set("range", range);
if (clicks)
query.set("clicks", clicks);
const url = routerMode === "hash" ? `http://localhost:${port}${base}?${query}#${no}` : `http://localhost:${port}${base}${no}?${query}`;
await page.goto(url, {
waitUntil,
timeout
});
if (waitUntil)
await page.waitForLoadState(waitUntil);
await page.emulateMedia({ colorScheme: dark ? "dark" : "light", media: "screen" });
const slide = no === "print" ? page.locator("body") : page.locator(`[data-slidev-no="${no}"]`);
await slide.waitFor();
{
const elements = slide.locator(".slidev-slide-loading");
const count = await elements.count();
for (let index = 0; index < count; index++)
await elements.nth(index).waitFor({ state: "detached" });
}
{
const elements = slide.locator("[data-waitfor]");
const count = await elements.count();
for (let index = 0; index < count; index++) {
const element = elements.nth(index);
const attribute = await element.getAttribute("data-waitfor");
if (attribute) {
await element.locator(attribute).waitFor({ state: "visible" }).catch((e) => {
console.error(e);
process.exitCode = 1;
});
}
}
}
{
const frames = page.frames();
await Promise.all(frames.map((frame) => frame.waitForLoadState()));
}
{
const container = slide.locator("#mermaid-rendering-container");
const count = await container.count();
if (count > 0) {
while (true) {
const element = container.locator("div").first();
if (await element.count() === 0)
break;
await element.waitFor({ state: "detached" });
}
await container.evaluate((node) => node.style.display = "none");
}
}
{
const elements = slide.locator(".monaco-aria-container");
const count = await elements.count();
for (let index = 0; index < count; index++) {
const element = elements.nth(index);
await element.evaluate((node) => node.style.display = "none");
}
}
if (wait)
await page.waitForTimeout(wait);
}
async function getSlidesIndex() {
const clicksBySlide = {};
const slides2 = page.locator(".print-slide-container");
const count = await slides2.count();
for (let i = 0; i < count; i++) {
const id = await slides2.nth(i).getAttribute("id") || "";
const path2 = Number(id.split("-")[0]);
clicksBySlide[path2] = (clicksBySlide[path2] || 0) + 1;
}
const slideIndexes = Object.fromEntries(Object.entries(clicksBySlide).reduce((acc, [path2, clicks], i) => {
acc.push([path2, clicks + (acc[i - 1]?.[1] ?? 0)]);
return acc;
}, []));
return slideIndexes;
}
function getClicksFromUrl(url) {
return url.match(/clicks=([1-9]\d*)/)?.[1];
}
async function genPageWithClicks(fn, no, clicks) {
await fn(no, clicks);
if (withClicks) {
await page.keyboard.press("ArrowRight", { delay: 100 });
const _clicks = getClicksFromUrl(page.url());
if (_clicks && clicks !== _clicks)
await genPageWithClicks(fn, no, _clicks);
}
}
async function genPagePdfPerSlide() {
const buffers = [];
const genPdfBuffer = async (i, clicks) => {
await go(i, clicks);
const pdf = await page.pdf({
width,
height,
margin: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
pageRanges: "1",
printBackground: true,
preferCSSPageSize: true
});
buffers.push(pdf);
};
let idx = 0;
for (const i of pages) {
await genPageWithClicks(genPdfBuffer, i);
progress.update(++idx);
}
let mergedPdf = await PDFDocument.create({});
for (const pdfBytes of buffers) {
const pdf = await PDFDocument.load(pdfBytes);
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach((page2) => {
mergedPdf.addPage(page2);
});
}
addPdfMetadata(mergedPdf);
if (withToc)
mergedPdf = await addTocToPdf(mergedPdf);
const buffer = await mergedPdf.save();
await fs.writeFile(output, buffer);
}
async function genPagePdfOnePiece() {
await go("print");
await page.pdf({
path: output,
width,
height,
margin: {
left: 0,
top: 0,
right: 0,
bottom: 0
},
printBackground: true,
preferCSSPageSize: true
});
let pdfData = await fs.readFile(output);
let pdf = await PDFDocument.load(pdfData);
addPdfMetadata(pdf);
if (withToc)
pdf = await addTocToPdf(pdf);
pdfData = Buffer.from(await pdf.save());
await fs.writeFile(output, pdfData);
}
async function genPagePngOnePiece(writeToDisk) {
const result = [];
await go("print");
await fs.emptyDir(output);
const slideContainers = page.locator(".print-slide-container");
const count = await slideContainers.count();
for (let i = 0; i < count; i++) {
progress.update(i + 1);
const id = await slideContainers.nth(i).getAttribute("id") || "";
const slideNo = +id.split("-")[0];
const buffer = await slideContainers.nth(i).screenshot({
omitBackground
});
result.push({ slideIndex: slideNo - 1, buffer });
if (writeToDisk)
await fs.writeFile(path.join(output, `${withClicks ? id : slideNo}.png`), buffer);
}
return result;
}
async function genPagePngPerSlide(writeToDisk) {
const result = [];
const genScreenshot = async (no, clicks) => {
await go(no, clicks);
const buffer = await page.screenshot({
omitBackground
});
result.push({ slideIndex: no - 1, buffer });
if (writeToDisk) {
await fs.writeFile(
path.join(output, `${no.toString().padStart(2, "0")}${clicks ? `-${clicks}` : ""}.png`),
buffer
);
}
};
for (const no of pages)
await genPageWithClicks(genScreenshot, no);
return result;
}
function genPagePdf() {
if (!output.endsWith(".pdf"))
output = `${output}.pdf`;
return perSlide ? genPagePdfPerSlide() : genPagePdfOnePiece();
}
function genPagePng(writeToDisk = true) {
return perSlide ? genPagePngPerSlide(writeToDisk) : genPagePngOnePiece(writeToDisk);
}
async function genPageMd() {
const files = await fs.readdir(output);
const mds = files.map((file, i, files2) => {
const slideIndex = getSlideIndex(file);
const mdImg = `![${slides[slideIndex]?.title}](./${slash(path.join(output, file))})
`;
if ((i + 1 === files2.length || getSlideIndex(files2[i + 1]) !== slideIndex) && slides[slideIndex]?.note)
return `${mdImg}${slides[slideIndex]?.note}
`;
return mdImg;
});
if (!output.endsWith(".md"))
output = `${output}.md`;
await fs.writeFile(output, mds.join(""));
}
async function genPagePptx(pngs) {
const { default: PptxGenJS } = await import("pptxgenjs");
const pptx = new PptxGenJS();
const layoutName = `${width}x${height}`;
pptx.defineLayout({
name: layoutName,
width: width / 96,
height: height / 96
});
pptx.layout = layoutName;
const titleSlide = slides[0];
pptx.author = titleSlide?.frontmatter?.author;
pptx.company = "Created using Slidev";
if (titleSlide?.title)
pptx.title = titleSlide?.title;
if (titleSlide?.frontmatter?.info)
pptx.subject = titleSlide?.frontmatter?.info;
pngs.forEach(({ slideIndex, buffer: buffer2 }) => {
const slide = pptx.addSlide();
slide.background = {
data: `data:image/png;base64,${buffer2.toString("base64")}`
};
const note = slides[slideIndex].note;
if (note)
slide.addNotes(note);
});
const buffer = await pptx.write({
outputType: "nodebuffer"
});
if (!output.endsWith(".pptx"))
output = `${output}.pptx`;
await fs.writeFile(output, buffer);
}
function getSlideIndex(file) {
const slideId = file.substring(0, file.indexOf(".")).split("-")[0];
return Number(slideId) - 1;
}
function addPdfMetadata(pdf) {
const titleSlide = slides[0];
if (titleSlide?.title)
pdf.setTitle(titleSlide.title);
if (titleSlide?.frontmatter?.info)
pdf.setSubject(titleSlide.frontmatter.info);
if (titleSlide?.frontmatter?.author)
pdf.setAuthor(titleSlide.frontmatter.author);
if (titleSlide?.frontmatter?.keywords) {
if (Array.isArray(titleSlide?.frontmatter?.keywords))
pdf.setKeywords(titleSlide?.frontmatter?.keywords);
else
pdf.setKeywords(titleSlide?.frontmatter?.keywords.split(","));
}
}
async function addTocToPdf(pdf) {
const outlinePdf = outlinePdfFactory(pdfLib);
const slideIndexes = await getSlidesIndex();
const tocTree = slides.filter((slide) => slide.title).reduce((acc, slide) => {
addToTree(acc, slide, slideIndexes);
return acc;
}, []);
const outline = makeOutline(tocTree);
return await outlinePdf({ outline, pdf });
}
progress.start(pages.length);
if (format === "pdf") {
await genPagePdf();
} else if (format === "png") {
await genPagePng();
} else if (format === "md") {
await genPagePng();
await genPageMd();
} else if (format === "pptx") {
const buffers = await genPagePng(false);
await genPagePptx(buffers);
} else {
throw new Error(`Unsupported exporting format "${format}"`);
}
progress.stop();
browser.close();
return output;
}
function getExportOptions(args, options, outDir, outFilename) {
const config = {
...options.data.config.export,
...args,
...clearUndefined({
waitUntil: args["wait-until"],
withClicks: args["with-clicks"],
executablePath: args["executable-path"],
withToc: args["with-toc"],
perSlide: args["per-slide"],
omitBackground: args["omit-background"]
})
};
const {
entry,
output,
format,
timeout,
wait,
waitUntil,
range,
dark,
withClicks,
executablePath,
withToc,
perSlide,
scale,
omitBackground
} = config;
outFilename = output || options.data.config.exportFilename || outFilename || `${path.basename(entry, ".md")}-export`;
if (outDir)
outFilename = path.join(outDir, outFilename);
return {
output: outFilename,
slides: options.data.slides,
total: options.data.slides.length,
range,
format: format || "pdf",
timeout: timeout ?? 3e4,
wait: wait ?? 0,
waitUntil: waitUntil === "none" ? void 0 : waitUntil ?? "networkidle",
dark: dark || options.data.config.colorSchema === "dark",
routerMode: options.data.config.routerMode,
width: options.data.config.canvasWidth,
height: Math.round(options.data.config.canvasWidth / options.data.config.aspectRatio),
withClicks: withClicks ?? format === "pptx",
executablePath,
withToc: withToc || false,
perSlide: perSlide || false,
scale: scale || 2,
omitBackground: omitBackground ?? false
};
}
async function importPlaywright() {
const { userRoot, userWorkspaceRoot } = await getRoots();
try {
return await import(await resolve("playwright-chromium", { url: userRoot }));
} catch {
}
if (userWorkspaceRoot !== userRoot) {
try {
return await import(await resolve("playwright-chromium", { url: userWorkspaceRoot }));
} catch {
}
}
const { resolveGlobal } = await import("resolve-global");
try {
const imported = await import(resolveGlobal("playwright-chromium"));
return imported.default ?? imported;
} catch {
}
try {
return await import("playwright-chromium");
} catch {
}
throw new Error("The exporting for Slidev is powered by Playwright, please install it via `npm i -D playwright-chromium`");
}
export {
exportNotes,
exportSlides,
getExportOptions
};
+14
View File
@@ -0,0 +1,14 @@
import * as vite from 'vite';
import { InlineConfig, PluginOption } from 'vite';
import { ResolvedSlidevOptions, SlidevServerOptions, SlidevEntryOptions, ResolvedSlidevUtils, SlidevPluginOptions } from '@slidev/types';
import * as fs from '@slidev/parser/fs';
export { fs as parser };
declare function createServer(options: ResolvedSlidevOptions, viteConfig?: InlineConfig, serverOptions?: SlidevServerOptions): Promise<vite.ViteDevServer>;
declare function resolveOptions(entryOptions: SlidevEntryOptions, mode: ResolvedSlidevOptions['mode']): Promise<ResolvedSlidevOptions>;
declare function createDataUtils(resolved: Omit<ResolvedSlidevOptions, 'utils'>): Promise<ResolvedSlidevUtils>;
declare function ViteSlidevPlugin(options: ResolvedSlidevOptions, pluginOptions?: SlidevPluginOptions, serverOptions?: SlidevServerOptions): Promise<PluginOption[]>;
export { ViteSlidevPlugin, createDataUtils, createServer, resolveOptions };
+17
View File
@@ -0,0 +1,17 @@
import {
createServer
} from "./chunk-UVOSJW3S.js";
import {
ViteSlidevPlugin,
createDataUtils,
parser,
resolveOptions
} from "./chunk-7E4FQJTV.js";
import "./chunk-UNQ5DBLZ.js";
export {
ViteSlidevPlugin,
createDataUtils,
createServer,
parser,
resolveOptions
};