@vivliostyle/cli
Version:
Save the pdf file via headless browser and Vivliostyle.
731 lines (724 loc) • 23.1 kB
JavaScript
import {
resolveViteConfig
} from "./chunk-4V7POPZ3.js";
import {
buildWebPublication,
cleanupWorkspace,
compile,
copyAssets,
createViteServer,
getFullBrowserName,
getSourceUrl,
getViewerFullUrl,
isWebPubConfig,
launchPreview,
loadVivliostyleConfig,
mergeInlineConfig,
prepareThemeDirectory,
resolveTaskConfig,
warnDeprecatedConfig
} from "./chunk-5YYKJN5S.js";
import {
importNodeModule
} from "./chunk-FXUEYQRY.js";
import {
Logger,
cwd,
isInContainer,
isUnicodeSupported,
isValidUri,
pathEquals,
randomBookSymbol,
runExitHandlers,
setupConfigFromFlags
} from "./chunk-HCZKJQUX.js";
import {
CONTAINER_LOCAL_HOSTNAME,
CONTAINER_ROOT_DIR,
coreVersion
} from "./chunk-4IIM6RSG.js";
import {
__callDispose,
__using
} from "./chunk-I7BWSAN6.js";
// src/core/build.ts
import { pathToFileURL as pathToFileURL2 } from "node:url";
import terminalLink2 from "terminal-link";
import upath4 from "upath";
import { build as viteBuild } from "vite";
import { cyan as cyan2, gray as gray2 } from "yoctocolors";
// src/container.ts
import { execFile } from "node:child_process";
import process from "node:process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import upath from "upath";
var execFileAsync = promisify(execFile);
function toContainerPath(urlOrAbsPath) {
if (isValidUri(urlOrAbsPath)) {
if (urlOrAbsPath.toLowerCase().startsWith("file")) {
return pathToFileURL(
upath.posix.join(
CONTAINER_ROOT_DIR,
upath.toUnix(fileURLToPath(urlOrAbsPath)).replace(/^\w:/, "")
)
).href;
} else {
return urlOrAbsPath;
}
}
return upath.posix.join(
CONTAINER_ROOT_DIR,
upath.toUnix(urlOrAbsPath).replace(/^\w:/, "")
);
}
function collectVolumeArgs(mountPoints) {
return mountPoints.filter((p, i, array) => {
if (i !== array.indexOf(p)) {
return false;
}
let parent = p;
while (!pathEquals(parent, upath.dirname(parent))) {
parent = upath.dirname(parent);
if (array.includes(parent)) {
return false;
}
}
return true;
}).map((p) => `${p}:${toContainerPath(p)}`);
}
async function runContainer({
image,
userVolumeArgs,
commandArgs,
entrypoint,
env,
workdir
}) {
const { default: commandExists } = await importNodeModule("command-exists");
if (!await commandExists("docker")) {
throw new Error(
`Docker isn't be installed. To use this feature, you'll need to install Docker.`
);
}
const versionCmd = await execFileAsync("docker", [
"version",
"--format",
"{{.Server.Version}}"
]);
const version = versionCmd.stdout.trim();
const [major, minor] = version.split(".").map(Number);
if (major < 20 || major === 20 && minor < 10) {
throw new Error(
`Docker version ${version} is not supported. Please upgrade to Docker 20.10.0 or later.`
);
}
try {
var _stack = [];
try {
const _2 = __using(_stack, Logger.suspendLogging("Launching docker container"));
const args = [
"run",
...Logger.isInteractive ? ["-it"] : [],
"--rm",
...entrypoint ? ["--entrypoint", entrypoint] : [],
...env ? env.flatMap(([k, v]) => ["-e", `${k}=${v}`]) : [],
...process.env.DEBUG ? ["-e", `DEBUG=${process.env.DEBUG}`] : [],
...userVolumeArgs.flatMap((arg) => ["-v", arg]),
...workdir ? ["-w", workdir] : [],
image,
...commandArgs
];
Logger.debug(`docker ${args.join(" ")}`);
const { execa } = await importNodeModule("execa");
const proc = execa("docker", args, {
stdio: "inherit"
});
await proc;
} catch (_) {
var _error = _, _hasError = true;
} finally {
__callDispose(_stack, _error, _hasError);
}
} catch (error) {
throw new Error(
"An error occurred on the running container. Please see logs above."
);
}
}
async function buildPDFWithContainer({
target,
config,
inlineConfig
}) {
const sourceUrl = new URL(await getSourceUrl(config));
if (sourceUrl.origin === config.rootUrl) {
sourceUrl.hostname = CONTAINER_LOCAL_HOSTNAME;
}
const bypassedOption = {
...inlineConfig,
input: {
format: "webbook",
entry: sourceUrl.href
},
output: [
{
...target,
path: toContainerPath(target.path)
}
],
host: CONTAINER_LOCAL_HOSTNAME
};
await runContainer({
image: config.image,
userVolumeArgs: collectVolumeArgs([
...typeof config.serverRootDir === "string" ? [config.serverRootDir] : [],
upath.dirname(target.path)
]),
env: [["VS_CLI_BUILD_PDF_OPTIONS", JSON.stringify(bypassedOption)]],
commandArgs: ["build"],
workdir: typeof config.serverRootDir === "string" ? toContainerPath(config.serverRootDir) : void 0
});
return target.path;
}
// src/output/pdf.ts
import fs2 from "node:fs";
import { URL as URL2 } from "node:url";
import terminalLink from "terminal-link";
import upath3 from "upath";
import { cyan, gray, green, red } from "yoctocolors";
// src/output/pdf-postprocess.ts
import decamelize from "decamelize";
import fs from "node:fs";
import os from "node:os";
import upath2 from "upath";
import { v1 as uuid } from "uuid";
var prefixes = {
dcterms: "http://purl.org/dc/terms/",
meta: "http://idpf.org/epub/vocab/package/meta/#"
};
var metaTerms = {
title: `${prefixes.dcterms}title`,
creator: `${prefixes.dcterms}creator`,
description: `${prefixes.dcterms}description`,
subject: `${prefixes.dcterms}subject`,
contributor: `${prefixes.dcterms}contributor`,
language: `${prefixes.dcterms}language`,
role: `${prefixes.meta}role`,
created: `${prefixes.meta}created`,
date: `${prefixes.meta}date`
};
async function pressReadyWithContainer({
input,
output,
preflightOption,
image
}) {
await runContainer({
image,
entrypoint: "press-ready",
userVolumeArgs: collectVolumeArgs([
upath2.dirname(input),
upath2.dirname(output)
]),
commandArgs: [
"build",
"-i",
toContainerPath(input),
"-o",
toContainerPath(output),
...preflightOption.map((opt) => `--${decamelize(opt, { separator: "-" })}`).filter((str) => /^[\w-]+/.test(str))
]
});
}
var PostProcess = class _PostProcess {
constructor(document2) {
this.document = document2;
}
static async load(pdf) {
const { PDFDocument } = await importNodeModule("pdf-lib");
const document2 = await PDFDocument.load(pdf, { updateMetadata: false });
return new _PostProcess(document2);
}
async save(output, { preflight, preflightOption, image }) {
const input = preflight ? upath2.join(os.tmpdir(), `vivliostyle-cli-${uuid()}.pdf`) : output;
const pdf = await this.document.save();
await fs.promises.writeFile(input, pdf);
if (preflight === "press-ready-local" || preflight === "press-ready" && isInContainer()) {
var _stack = [];
try {
const _3 = __using(_stack, Logger.suspendLogging("Running press-ready"));
const { build: build2 } = await importNodeModule("press-ready");
await build2({
...preflightOption.reduce((acc, opt) => {
const optName = decamelize(opt, { separator: "-" });
return optName.startsWith("no-") ? {
...acc,
[optName.slice(3)]: false
} : {
...acc,
[optName]: true
};
}, {}),
input,
output
});
} catch (_) {
var _error = _, _hasError = true;
} finally {
__callDispose(_stack, _error, _hasError);
}
} else if (preflight === "press-ready") {
var _stack2 = [];
try {
const _3 = __using(_stack2, Logger.suspendLogging("Running press-ready"));
await pressReadyWithContainer({
input,
output,
preflightOption,
image
});
} catch (_2) {
var _error2 = _2, _hasError2 = true;
} finally {
__callDispose(_stack2, _error2, _hasError2);
}
}
}
async metadata(tree, {
pageProgression,
browserVersion,
viewerCoreVersion,
disableCreatorOption
} = {}) {
const { ReadingDirection } = await importNodeModule("pdf-lib");
const title = tree[metaTerms.title]?.[0].v;
if (title) {
this.document.setTitle(title);
}
const author = tree[metaTerms.creator]?.map((item) => item.v)?.join("; ");
if (author) {
this.document.setAuthor(author);
}
const subject = tree[metaTerms.description]?.[0].v;
if (subject) {
this.document.setSubject(subject);
}
const keywords = tree[metaTerms.subject]?.map((item) => item.v);
if (keywords) {
this.document.setKeywords(keywords);
}
let creatorOpt = `Vivliostyle.js ${viewerCoreVersion ?? coreVersion}`;
if (browserVersion) {
creatorOpt += `; ${browserVersion}`;
}
this.document.setCreator(
disableCreatorOption ? "Vivliostyle" : `Vivliostyle (${creatorOpt})`
);
const language = tree[metaTerms.language]?.[0].v;
if (language) {
this.document.setLanguage(language);
}
const creation = (tree[metaTerms.created] || tree[metaTerms.date])?.[0].v;
const creationDate = creation && new Date(creation);
if (creationDate) {
this.document.setCreationDate(creationDate);
}
if (pageProgression === "rtl") {
const viewerPrefs = this.document.catalog.getOrCreateViewerPreferences();
viewerPrefs.setReadingDirection(ReadingDirection.R2L);
}
}
async toc(items) {
const { PDFDict, PDFHexString, PDFName, PDFNumber } = await importNodeModule("pdf-lib");
if (!items || !items.length) {
return;
}
const addRefs = (items2, parentRef) => items2.map((item) => {
const ref = this.document.context.nextRef();
return {
...item,
parentRef,
ref,
children: addRefs(item.children, ref)
};
});
const countAll = (items2) => items2.reduce((sum, item) => sum + countAll(item.children), items2.length);
const addObjectsToPDF = (items2) => {
for (const [i, item] of items2.entries()) {
const child = PDFDict.withContext(this.document.context);
child.set(PDFName.of("Title"), PDFHexString.fromText(item.title));
child.set(PDFName.of("Dest"), PDFName.of(item.id));
child.set(PDFName.of("Parent"), item.parentRef);
const prev = items2[i - 1];
if (prev) {
child.set(PDFName.of("Prev"), prev.ref);
}
const next = items2[i + 1];
if (next) {
child.set(PDFName.of("Next"), next.ref);
}
if (item.children.length) {
child.set(PDFName.of("First"), item.children[0].ref);
child.set(
PDFName.of("Last"),
item.children[item.children.length - 1].ref
);
child.set(PDFName.of("Count"), PDFNumber.of(countAll(item.children)));
}
this.document.context.assign(item.ref, child);
addObjectsToPDF(item.children);
}
};
const outlineRef = this.document.context.nextRef();
const itemsWithRefs = addRefs(items, outlineRef);
addObjectsToPDF(itemsWithRefs);
const outline = PDFDict.withContext(this.document.context);
outline.set(PDFName.of("First"), itemsWithRefs[0].ref);
outline.set(
PDFName.of("Last"),
itemsWithRefs[itemsWithRefs.length - 1].ref
);
outline.set(PDFName.of("Count"), PDFNumber.of(countAll(itemsWithRefs)));
this.document.context.assign(outlineRef, outline);
this.document.catalog.set(PDFName.of("Outlines"), outlineRef);
}
async setPageBoxes(pageSizeData) {
if (pageSizeData.length + 1 === this.document.getPageCount()) {
this.document.removePage(pageSizeData.length);
}
if (pageSizeData.length !== this.document.getPageCount()) {
return;
}
for (let i = 0; i < pageSizeData.length; i++) {
const page = this.document.getPage(i);
const sizeData = pageSizeData[i];
if (!sizeData.mediaWidth || !sizeData.mediaHeight || isNaN(sizeData.bleedOffset) || isNaN(sizeData.bleedSize)) {
continue;
}
const yOffset = page.getHeight() - sizeData.mediaHeight;
page.setMediaBox(0, yOffset, sizeData.mediaWidth, sizeData.mediaHeight);
if (!sizeData.bleedOffset && !sizeData.bleedSize) {
continue;
}
page.setBleedBox(
sizeData.bleedOffset,
yOffset + sizeData.bleedOffset,
sizeData.mediaWidth - sizeData.bleedOffset * 2,
sizeData.mediaHeight - sizeData.bleedOffset * 2
);
const trimOffset = sizeData.bleedOffset + sizeData.bleedSize;
page.setTrimBox(
trimOffset,
yOffset + trimOffset,
sizeData.mediaWidth - trimOffset * 2,
sizeData.mediaHeight - trimOffset * 2
);
}
}
};
// src/output/pdf.ts
async function buildPDF({
target,
config
}) {
Logger.logUpdate(`Launching PDF build environment`);
const viewerFullUrl = await getViewerFullUrl(config);
Logger.debug("viewerFullUrl", viewerFullUrl);
let lastEntry;
function stringifyEntry(entry) {
const formattedSourcePath = cyan(
entry.source.type === "file" ? upath3.relative(config.entryContextDir, entry.source.pathname) : entry.source.href
);
return `${terminalLink(
formattedSourcePath,
entry.source.type === "file" ? `file://${entry.source.pathname}` : entry.source.href,
{
fallback: () => formattedSourcePath
}
)} ${entry.title ? gray(entry.title) : ""}`;
}
function handleEntry(response) {
const entry = config.entries.find((entry2) => {
if (!("source" in entry2)) {
return false;
}
const url = new URL2(response.url());
return url.protocol === "file:" ? pathEquals(entry2.target, url.pathname) : pathEquals(
upath3.relative(config.workspaceDir, entry2.target),
url.pathname.substring(1)
);
});
if (entry) {
if (!lastEntry) {
lastEntry = entry;
Logger.logUpdate(stringifyEntry(entry));
return;
}
Logger.logSuccess(stringifyEntry(lastEntry));
Logger.startLogging(stringifyEntry(entry));
lastEntry = entry;
}
}
const { browser, page } = await launchPreview({
mode: "build",
url: viewerFullUrl,
config,
onBrowserOpen: () => {
Logger.logUpdate("Building pages");
},
onPageOpen: async (page2) => {
page2.on("pageerror", (error) => {
Logger.logError(red(error.message));
});
page2.on("console", (msg) => {
switch (msg.type()) {
case "error":
if (/\/vivliostyle-viewer\.js$/.test(msg.location().url ?? "")) {
Logger.logError(msg.text());
throw msg.text();
}
return;
case "debug":
if (/time slice/.test(msg.text())) {
return;
}
break;
}
if (msg.type() === "error") {
Logger.logVerbose(red("console.error()"), msg.text());
} else {
Logger.logVerbose(gray(`console.${msg.type()}()`), msg.text());
}
});
page2.on("response", (response) => {
Logger.debug(
gray("viewer:response"),
green(response.status().toString()),
response.url()
);
handleEntry(response);
if (300 > response.status() && 200 <= response.status()) return;
if (response.url().startsWith("file://") && response.ok()) return;
Logger.logError(red(`${response.status()}`), response.url());
});
await page2.setDefaultTimeout(config.timeout);
}
});
const browserName = getFullBrowserName(config.browser.type);
const browserVersion = `${browserName}/${await browser.version()}`;
Logger.debug(green("success"), `browserVersion=${browserVersion}`);
let remainTime = config.timeout;
const startTime = Date.now();
await page.waitForLoadState("networkidle");
await page.waitForFunction(() => !!window.coreViewer);
await page.emulateMedia({ media: "print" });
await page.waitForFunction(
/* v8 ignore next */
() => window.coreViewer.readyState === "complete",
void 0,
{ polling: 1e3 }
);
if (lastEntry) {
Logger.logSuccess(stringifyEntry(lastEntry));
}
const pageProgression = await page.evaluate(
() => (
/* v8 ignore next 5 */
document.querySelector("#vivliostyle-viewer-viewport")?.getAttribute("data-vivliostyle-page-progression") === "rtl" ? "rtl" : "ltr"
)
);
const viewerCoreVersion = await page.evaluate(
() => (
/* v8 ignore next 3 */
document.querySelector("#vivliostyle-menu_settings .version")?.textContent?.replace(/^.*?: (\d[-+.\w]+).*$/, "$1")
)
);
const metadata = await loadMetadata(page);
const toc = await loadTOC(page);
const pageSizeData = await loadPageSizeData(page);
remainTime -= Date.now() - startTime;
if (remainTime <= 0) {
throw new Error("Typesetting process timed out");
}
Logger.debug("Remaining timeout:", remainTime);
Logger.logUpdate("Building PDF");
const pdf = await page.pdf({
margin: {
top: 0,
bottom: 0,
right: 0,
left: 0
},
printBackground: true,
preferCSSPageSize: true,
tagged: true
// timeout: remainTime,
});
await browser.close();
Logger.logUpdate("Processing PDF");
fs2.mkdirSync(upath3.dirname(target.path), { recursive: true });
const post = await PostProcess.load(pdf);
await post.metadata(metadata, {
pageProgression,
browserVersion,
viewerCoreVersion,
// If custom viewer is set and its version info is not available,
// there is no guarantee that the default creator option is correct.
disableCreatorOption: !!config.viewer && !viewerCoreVersion
});
await post.toc(toc);
await post.setPageBoxes(pageSizeData);
await post.save(target.path, {
preflight: target.preflight,
preflightOption: target.preflightOption,
image: config.image
});
return target.path;
}
async function loadMetadata(page) {
return page.evaluate(() => window.coreViewer.getMetadata());
}
async function loadTOC(page) {
return page.evaluate(
() => new Promise((resolve) => {
function listener(payload) {
if (payload.a !== "toc") {
return;
}
window.coreViewer.removeListener("done", listener);
window.coreViewer.showTOC(false);
resolve(window.coreViewer.getTOC());
}
window.coreViewer.addListener("done", listener);
window.coreViewer.showTOC(true);
})
);
}
async function loadPageSizeData(page) {
return page.evaluate(() => {
const sizeData = [];
const pageContainers = document.querySelectorAll(
"#vivliostyle-viewer-viewport > div > div > div[data-vivliostyle-page-container]"
);
for (const pageContainer of pageContainers) {
const bleedBox = pageContainer.querySelector(
"div[data-vivliostyle-bleed-box]"
);
sizeData.push({
mediaWidth: parseFloat(pageContainer.style.width) * 0.75,
mediaHeight: parseFloat(pageContainer.style.height) * 0.75,
bleedOffset: parseFloat(bleedBox?.style.left) * 0.75,
bleedSize: parseFloat(bleedBox?.style.paddingLeft) * 0.75
});
}
return sizeData;
});
}
// src/core/build.ts
async function build(inlineConfig, { containerForkMode = false } = {}) {
Logger.setLogLevel(inlineConfig.logLevel);
Logger.setCustomLogger(inlineConfig.logger);
if (containerForkMode) {
Logger.setLogPrefix(gray2("[Docker]"));
}
Logger.debug("build > inlineConfig %O", inlineConfig);
let vivliostyleConfig = await loadVivliostyleConfig(inlineConfig) ?? setupConfigFromFlags(inlineConfig);
warnDeprecatedConfig(vivliostyleConfig);
vivliostyleConfig = mergeInlineConfig(vivliostyleConfig, {
...inlineConfig,
quick: false
});
const { inlineOptions } = vivliostyleConfig;
Logger.debug("build > vivliostyleConfig %O", vivliostyleConfig);
for (let [i, task] of vivliostyleConfig.tasks.entries()) {
var _stack2 = [];
try {
const _3 = __using(_stack2, Logger.startLogging("Start building"));
const config = resolveTaskConfig(task, inlineOptions);
Logger.debug("build > config %O", config);
const viteConfig = await resolveViteConfig({
...config,
mode: "build"
});
let server;
if (!containerForkMode) {
Logger.debug("build > viteConfig.configFile %s", viteConfig.configFile);
if (viteConfig.configFile && typeof config.serverRootDir === "string") {
var _stack = [];
try {
const _4 = __using(_stack, Logger.suspendLogging("Building Vite project"));
await viteBuild({
configFile: viteConfig.configFile,
root: config.serverRootDir
});
} catch (_) {
var _error = _, _hasError = true;
} finally {
__callDispose(_stack, _error, _hasError);
}
}
if (!inlineConfig.disableServerStartup) {
server = await createViteServer({
config,
viteConfig,
inlineConfig,
mode: "build"
});
}
if (isWebPubConfig(config)) {
await cleanupWorkspace(config);
await prepareThemeDirectory(config);
await compile(config);
await copyAssets(config);
}
}
for (const target of config.outputs) {
let output = null;
const { format } = target;
if (format === "pdf") {
if (!containerForkMode && target.renderMode === "docker") {
output = await buildPDFWithContainer({
target,
config,
inlineConfig
});
} else {
output = await buildPDF({ target, config });
}
} else if (format === "webpub" || format === "epub") {
output = await buildWebPublication({ target, config });
}
if (output && !containerForkMode) {
const formattedOutput = cyan2(
upath4.relative(inlineConfig.cwd ?? cwd, output)
);
Logger.logSuccess(
`Finished building ${terminalLink2(
formattedOutput,
pathToFileURL2(output).href,
{
fallback: () => formattedOutput
}
)}`
);
}
}
await server?.close();
} catch (_2) {
var _error2 = _2, _hasError2 = true;
} finally {
__callDispose(_stack2, _error2, _hasError2);
}
}
runExitHandlers();
if (!containerForkMode) {
const num = vivliostyleConfig.tasks.flatMap((t) => t.output ?? []).length;
const symbol = isUnicodeSupported ? `${num > 1 ? "\u{1F4DA}" : randomBookSymbol} ` : "";
Logger.log(`${symbol}Built successfully!`);
}
}
export {
build
};
//# sourceMappingURL=chunk-XDT2MS4O.js.map