UNPKG

@vivliostyle/cli

Version:

Save the pdf file via headless browser and Vivliostyle.

1,605 lines (1,593 loc) 130 kB
import { importNodeModule } from "./chunk-ECEGM36O.js"; import { DetailError, GlobMatcher, Logger, assertPubManifestSchema, cwd, debounce, detectBrowserPlatform, getAssetMatcher, getCacheDir, getDefaultBrowserTag, getDefaultEpubOpfPath, getEpubRootDir, getFormattedError, getOsLocale, getWebPubResourceMatcher, isInContainer, isRunningOnWSL, isValidUri, openEpub, parseJsonc, pathContains, pathEquals, prettifySchemaError, readJSON, registerExitHandler, runExitHandlers, setupConfigFromFlags, statFileSync, touchTmpFile, useTmpDirectory, writeFileIfChanged } from "./chunk-DBK27BAR.js"; import { VivliostyleConfigSchema } from "./chunk-7GIJVX4M.js"; import { CONTAINER_LOCAL_HOSTNAME, CONTAINER_URL, COVER_HTML_FILENAME, COVER_HTML_IMAGE_ALT, DEFAULT_BROWSER_VERSIONS, EMPTY_DATA_URI, EPUB_CONTAINER_XML, EPUB_LANDMARKS_COVER_ENTRY, EPUB_LANDMARKS_TITLE, EPUB_LANDMARKS_TOC_ENTRY, EPUB_NS, EPUB_OUTPUT_VERSION, MANIFEST_FILENAME, TOC_FILENAME, TOC_TITLE, VIEWER_ROOT_PATH, XML_DECLARATION, cliVersion, viewerRoot } from "./chunk-OAFXM4ES.js"; import { __callDispose, __using } from "./chunk-I7BWSAN6.js"; // src/config/load.ts import fs from "node:fs"; import { createRequire } from "node:module"; import { pathToFileURL } from "node:url"; import upath from "upath"; import * as v from "valibot"; var require2 = createRequire(import.meta.url); function locateVivliostyleConfig({ config, cwd: cwd2 = cwd }) { if (config) { return upath.resolve(cwd2, config); } return [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".json"].map((ext) => upath.join(cwd2, `vivliostyle.config${ext}`)).find((p) => fs.existsSync(p)); } async function loadVivliostyleConfig({ config, configData, cwd: cwd2 }) { if (configData) { return v.parse(VivliostyleConfigSchema, configData); } const absPath = locateVivliostyleConfig({ config, cwd: cwd2 }); if (!absPath) { return; } let parsedConfig; let jsonRaw; try { if (upath.extname(absPath) === ".json") { jsonRaw = fs.readFileSync(absPath, "utf8"); parsedConfig = parseJsonc(jsonRaw); } else { delete require2.cache[require2.resolve(absPath)]; const url = pathToFileURL(absPath); url.search = `version=${Date.now()}`; parsedConfig = (await import( /* @vite-ignore */ url.href )).default; jsonRaw = JSON.stringify(parsedConfig, null, 2); } } catch (error) { const thrownError = error; throw new DetailError( `An error occurred on loading a config file: ${absPath}`, thrownError.stack ?? thrownError.message ); } const result = v.safeParse(VivliostyleConfigSchema, parsedConfig); if (result.success) { const { tasks, inlineOptions } = result.output; return { tasks, inlineOptions: { ...inlineOptions, cwd: cwd2 ?? cwd, config: absPath } }; } else { const errorString = prettifySchemaError(jsonRaw, result.issues); throw new DetailError( `Validation of vivliostyle config failed. Please check the schema: ${config}`, errorString ); } } function warnDeprecatedConfig(config) { if (config.tasks.some((task) => task.includeAssets)) { Logger.logWarn( "'includeAssets' property of Vivliostyle config was deprecated and will be removed in a future release. Please use 'copyAsset.includes' property instead." ); } if (config.tasks.some((task) => task.tocTitle)) { Logger.logWarn( "'tocTitle' property of Vivliostyle config was deprecated and will be removed in a future release. Please use 'toc.title' property instead." ); } if (config.tasks.some((task) => task.http)) { Logger.logWarn( "'http' property of Vivliostyle config was deprecated and will be removed in a future release. This option is enabled by default, and the file protocol is no longer supported." ); } if (config.tasks.some((task) => task.pressReady !== void 0)) { Logger.logWarn( `'pressReady' property of Vivliostyle config was deprecated and will be removed in a future release. Please use 'pdfPostprocess.preflight: "press-ready"' property instead.` ); } if (config.tasks.some( (task) => task.output && [task.output].flat().some((o) => o.preflight) )) { Logger.logWarn( "'preflight' property of output config was deprecated and will be removed in a future release. Please use 'pdfPostprocess.preflight' property instead." ); } if (config.tasks.some( (task) => task.output && [task.output].flat().some((o) => o.preflightOption) )) { Logger.logWarn( "'preflightOption' property of output config was deprecated and will be removed in a future release. Please use 'pdfPostprocess.preflightOption' property instead." ); } } // src/config/merge.ts var pruneObject = (obj) => { const ret = { ...obj }; for (const key in ret) { if (ret[key] === void 0 || ret[key] === null) { delete ret[key]; } } return ret; }; function mergeConfig(base, override) { return { tasks: base.tasks.map((task, i) => ({ ...pruneObject(task), ...pruneObject(override) })), inlineOptions: base.inlineOptions }; } function mergeInlineConfig({ tasks, inlineOptions }, inlineConfig) { const { theme, size, pressReady, title, author, language, readingProgression, timeout, image, viewer, viewerParam, browser, output, renderMode, preflight, preflightOption, vite: vite5, viteConfigFile, host, port, ...overrideInlineOptions } = inlineConfig; return { tasks: tasks.map((task) => ({ ...pruneObject(task), ...pruneObject({ theme: theme === false ? void 0 : theme, size, pressReady, title, author, language, readingProgression, timeout, image, viewer, viewerParam, browser, vite: vite5, viteConfigFile }), output: (output?.length ? output : task.output)?.map((o) => ({ ...pruneObject(o), ...pruneObject({ renderMode, preflight, preflightOption }) })), server: { ...pruneObject(task.server ?? {}), ...pruneObject({ host, port }) } })), inlineOptions: { ...pruneObject(inlineOptions), ...pruneObject({ renderMode, preflight, preflightOption }), ...pruneObject( overrideInlineOptions ) } }; } // src/config/resolve.ts import { readMetadata, VFM } from "@vivliostyle/vfm"; import { lookup as mime } from "mime-types"; import fs3 from "node:fs"; import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url"; import npa from "npm-package-arg"; import { globSync } from "tinyglobby"; import upath2 from "upath"; // src/processor/markdown.ts import fs2 from "node:fs"; import vfile from "vfile"; async function processMarkdown(documentProcessorFactory, documentMetadataReader, filepath, options = {}) { const markdownString = fs2.readFileSync(filepath, "utf8"); const processor = documentProcessorFactory( options, documentMetadataReader(markdownString) ); const processed = await processor.process( vfile({ path: filepath, contents: markdownString }) ); return processed; } function readMarkdownMetadata(filepath, documentMetadataReader) { return documentMetadataReader(fs2.readFileSync(filepath, "utf8")); } // src/config/resolve.ts var manuscriptMediaTypes = [ "text/markdown", "text/html", "text/plain", "application/xhtml+xml", // a special MIME type indicates that a custom processor is used "text/x-vivliostyle-custom" ]; var UseTemporaryServerRoot = Symbol("UseTemporaryServerRoot"); var DEFAULT_ASSET_EXTENSIONS = [ "css", "css.map", "png", "jpg", "jpeg", "svg", "gif", "webp", "apng", "ttf", "otf", "woff", "woff2" ]; function isManuscriptMediaType(mediaType) { return !!(mediaType && manuscriptMediaTypes.includes(mediaType)); } var htmlExtensions = [".html", ".htm", ".xhtml", ".xht"]; function toHtmlExtension(filename) { const ext = upath2.extname(filename).toLowerCase(); if (htmlExtensions.includes( // @ts-expect-error check membership ext )) { return filename; } return `${filename.slice(0, -ext.length)}.html`; } function isWebPubConfig(config) { return config.viewerInput.type === "webpub"; } function isWebbookConfig(config) { return config.viewerInput.type === "webbook"; } function parsePackageName(specifier, cwd2) { try { let result = npa(specifier, cwd2); if (result.type === "git" && result.saveSpec?.startsWith("github:")) { result = npa(`file:${specifier}`, cwd2); } return result; } catch (error) { return null; } } function parseTheme({ theme, context, workspaceDir, themesDir }) { const { specifier, import: importPath } = typeof theme === "string" ? { specifier: theme, import: void 0 } : theme; if (isValidUri(specifier)) { return { type: "uri", name: upath2.basename(specifier), location: specifier }; } const stylePath = upath2.resolve(context, specifier); if (fs3.existsSync(stylePath) && stylePath.endsWith(".css")) { const sourceRelPath = upath2.relative(context, stylePath); return { type: "file", name: upath2.basename(specifier), source: stylePath, location: upath2.resolve(workspaceDir, sourceRelPath) }; } const parsed = parsePackageName(specifier, context); if (!parsed) { throw new Error(`Invalid package name: ${specifier}`); } if (!parsed.registry && parsed.type !== "directory") { throw new Error(`This package specifier is not allowed: ${specifier}`); } let name = parsed.name; let resolvedSpecifier = specifier; if (parsed.type === "directory" && parsed.fetchSpec) { const pkgJsonPath = upath2.join(parsed.fetchSpec, "package.json"); if (fs3.existsSync(pkgJsonPath)) { const packageJson = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8")); name = packageJson.name; resolvedSpecifier = parsed.fetchSpec; } } if (!name) { throw new Error(`Could not determine the package name: ${specifier}`); } return { type: "package", name, specifier: resolvedSpecifier, location: upath2.join(themesDir, "node_modules", name), registry: Boolean(parsed.registry), importPath }; } function parsePageSize(size) { const [width, height, ...others] = `${size}`.split(","); if (!width || others.length) { throw new Error(`Cannot parse size: ${size}`); } else if (width && height) { return { width, height }; } else { return { format: width }; } } function parseFileMetadata({ contentType, sourcePath, workspaceDir, themesDir, documentMetadataReader }) { const sourceDir = upath2.dirname(sourcePath); let title; let themes; if (documentMetadataReader) { const metadata = readMarkdownMetadata(sourcePath, documentMetadataReader); title = metadata.title; if (metadata.vfm?.theme && themesDir) { themes = [metadata.vfm.theme].flat().filter( (entry) => !!entry && (typeof entry === "string" || typeof entry === "object") ).map( (theme) => parseTheme({ theme, context: sourceDir, workspaceDir, themesDir }) ); } } else if (contentType === "text/html" || contentType === "application/xhtml+xml") { const content = fs3.readFileSync(sourcePath, "utf8"); title = content.match(/<title>([^<]*)<\/title>/)?.[1] || void 0; } return { title, themes }; } function parseCustomStyle({ customStyle, entryContextDir }) { if (isValidUri(customStyle)) { return customStyle; } const stylePath = upath2.resolve(entryContextDir, customStyle); if (!pathContains(entryContextDir, stylePath)) { throw Error( `Custom style file ${customStyle} is not in ${entryContextDir}. Make sure the file is located in the context directory or a subdirectory.` ); } if (!fs3.existsSync(stylePath)) { throw new Error(`Custom style file not found: ${customStyle}`); } return pathToFileURL2(stylePath).href.slice( pathToFileURL2(entryContextDir).href.replace(/\/$/, "").length + 1 ); } function resolveTaskConfig(config, options) { const context = options.cwd ?? cwd; Logger.debug("resolveTaskConfig > context %s", context); const entryContextDir = config.entryContext ? upath2.resolve(context, config.entryContext) : context; const language = config.language; const readingProgression = config.readingProgression; const size = config.size ? parsePageSize(config.size) : void 0; const cropMarks = options.cropMarks ?? false; const bleed = options.bleed; const cropOffset = options.cropOffset; const css = options.css; const singleDoc = options.singleDoc ?? false; const quick = options.quick ?? false; const temporaryFilePrefix = config.temporaryFilePrefix ?? `.vs-${Date.now()}.`; const vfmOptions = { ...config?.vfm, hardLineBreaks: config?.vfm?.hardLineBreaks ?? false, disableFormatHtml: config?.vfm?.disableFormatHtml ?? false }; const timeout = config.timeout ?? 3e5; const sandbox = options.sandbox ?? false; const browser = (() => { const type = config.browser?.type ?? "chrome"; const platform = detectBrowserPlatform(); return { type, tag: config.browser?.tag ?? (platform ? DEFAULT_BROWSER_VERSIONS[type][platform] : "latest"), executablePath: options.executableBrowser }; })(); const proxyServer = options.proxyServer ?? process.env.HTTP_PROXY ?? void 0; const proxy = proxyServer ? { server: proxyServer, bypass: options.proxyBypass ?? process.env.NOPROXY ?? void 0, username: options.proxyUser, password: options.proxyPass } : void 0; const image = config.image ?? `${CONTAINER_URL}:${cliVersion}`; const viewer = config.viewer ?? void 0; const viewerParam = config.viewerParam ?? void 0; const logLevel = options.logLevel ?? "silent"; const ignoreHttpsErrors = options.ignoreHttpsErrors ?? false; const base = config.base ?? "/vivliostyle"; const staticRoutes = config.static ?? {}; const viteConfig = config.vite; const viteConfigFile = config.viteConfigFile ?? true; const customStyle = options.style && parseCustomStyle({ customStyle: options.style, entryContextDir }) || void 0; const customUserStyle = options.userStyle && parseCustomStyle({ customStyle: options.userStyle, entryContextDir }) || void 0; const outputs = (() => { const resolveCmykConfig = (cmykOption) => { if (cmykOption && typeof cmykOption === "object") { return { warnUnmapped: cmykOption.warnUnmapped ?? true, overrideMap: cmykOption.overrideMap ?? [], mapOutput: cmykOption.mapOutput ? upath2.resolve(context, cmykOption.mapOutput) : void 0 }; } if (options.cmyk || cmykOption === true) { return { warnUnmapped: true, overrideMap: [], mapOutput: void 0 }; } return false; }; const resolveReplaceImageConfig = (replaceImageOption) => { if (!replaceImageOption) { return []; } const allFiles = globSync("**/*", { cwd: entryContextDir, onlyFiles: true }); return replaceImageOption.flatMap(({ source, replacement }) => { if (source instanceof RegExp) { return allFiles.filter((file) => source.test(file)).map((file) => ({ source: upath2.resolve(entryContextDir, file), replacement: upath2.resolve( entryContextDir, file.replace(source, replacement) ) })); } return { source: upath2.resolve(entryContextDir, source), replacement: upath2.resolve(entryContextDir, replacement) }; }); }; const resolveDefaultPreflight = () => { if (options.preflight) { return options.preflight; } const pp = config.pdfPostprocess; if (pp && "preflight" in pp) { return pp.preflight; } if (config.pressReady) { return "press-ready"; } return void 0; }; const resolveDefaultPreflightOption = () => { if (options.preflightOption) { return options.preflightOption; } return config.pdfPostprocess?.preflightOption ?? []; }; const defaultPdfOptions = { format: "pdf", renderMode: options.renderMode ?? "local", preflight: resolveDefaultPreflight(), preflightOption: resolveDefaultPreflightOption(), cmyk: resolveCmykConfig(config.pdfPostprocess?.cmyk), replaceImage: resolveReplaceImageConfig( config.pdfPostprocess?.replaceImage ) }; if (config.output) { return config.output.map((target) => { const outputPath = upath2.resolve(context, target.path); const format = target.format; switch (format) { case "pdf": { const targetPp = target.pdfPostprocess; const { pdfPostprocess: _, ...targetRest } = target; const resolvedPreflight = (() => { if (options.preflight) return options.preflight; if (targetPp?.preflight) return targetPp.preflight; if (target.preflight) return target.preflight; return defaultPdfOptions.preflight; })(); const resolvedPreflightOption = (() => { if (options.preflightOption) return options.preflightOption; if (targetPp?.preflightOption) return targetPp.preflightOption; if (target.preflightOption) return target.preflightOption; return defaultPdfOptions.preflightOption; })(); const resolvedCmyk = targetPp?.cmyk !== void 0 ? resolveCmykConfig(targetPp.cmyk) : defaultPdfOptions.cmyk; const resolvedReplaceImage = targetPp?.replaceImage !== void 0 ? resolveReplaceImageConfig(targetPp.replaceImage) : defaultPdfOptions.replaceImage; return { ...defaultPdfOptions, ...targetRest, format, path: outputPath, preflight: resolvedPreflight, preflightOption: resolvedPreflightOption, cmyk: resolvedCmyk, replaceImage: resolvedReplaceImage }; } case "epub": return { ...target, format, path: outputPath, version: EPUB_OUTPUT_VERSION }; case "webpub": return { ...target, format, path: outputPath }; default: return format; } }); } const filename = config.title ? `${config.title}.pdf` : "output.pdf"; return [ { ...defaultPdfOptions, path: upath2.resolve(context, filename) } ]; })(); const { server, rootUrl } = (() => { let host = config.server?.host ?? false; let allowedHosts = config.server?.allowedHosts || []; const port = config.server?.port ?? 13e3; if (outputs.some( (target) => target.format === "pdf" && target.renderMode === "docker" ) && !isInContainer()) { host = true; if (Array.isArray(allowedHosts) && !allowedHosts.includes(CONTAINER_LOCAL_HOSTNAME)) { allowedHosts.push(CONTAINER_LOCAL_HOSTNAME); } } const rootHostname = !host ? "localhost" : host === true ? "0.0.0.0" : host; return { server: { host, port, proxy: config.server?.proxy ?? {}, allowedHosts }, rootUrl: `http://${rootHostname}:${port}` }; })(); const cover = config.cover && { src: upath2.resolve(entryContextDir, config.cover.src), name: config.cover.name || COVER_HTML_IMAGE_ALT }; const copyAsset = { includes: config.copyAsset?.includes ?? config.includeAssets ?? [], excludes: config.copyAsset?.excludes ?? [], fileExtensions: [ .../* @__PURE__ */ new Set([ ...DEFAULT_ASSET_EXTENSIONS, ...config.copyAsset?.includeFileExtensions ?? [] ]) ].filter( (ext) => !(config.copyAsset?.excludeFileExtensions ?? []).includes(ext) ) }; const themeIndexes = /* @__PURE__ */ new Set(); const projectConfig = !options.config && options.input ? resolveSingleInputConfig({ config, input: options.input, context, temporaryFilePrefix, themeIndexes, base }) : resolveComposedProjectConfig({ config, context, entryContextDir, outputs, temporaryFilePrefix, themeIndexes, cover }); for (const output of outputs) { const relPath = upath2.relative(context, output.path); if (pathContains(output.path, entryContextDir) || pathEquals(output.path, entryContextDir)) { throw new Error( `The output path is set to "${relPath}", but this will overwrite the original manuscript file. Please specify a different path.` ); } if (pathContains(output.path, projectConfig.workspaceDir) || pathEquals(output.path, projectConfig.workspaceDir)) { throw new Error( `The output path is set to "${relPath}", but this will overwrite the working directory of Vivliostyle. Please specify a different path.` ); } } const { entries, workspaceDir } = projectConfig; const duplicatedTarget = entries.find( (v1, i) => entries.findLastIndex((v2) => v1.target === v2.target) !== i )?.target; if (duplicatedTarget) { const sourceFile = entries.find( (entry) => entry.target === duplicatedTarget && entry.source?.type === "file" )?.source; throw new Error( `The output path "${upath2.relative(workspaceDir, duplicatedTarget)}" will overwrite existing content.` + (sourceFile ? ` Please choose a different name for the source file: ${sourceFile.pathname}` : "") ); } const resolvedConfig = { ...projectConfig, entryContextDir, outputs, themeIndexes, copyAsset, temporaryFilePrefix, size, cropMarks, bleed, cropOffset, css, customStyle, customUserStyle, singleDoc, quick, language, readingProgression, vfmOptions, cover, timeout, sandbox, browser, proxy, image, viewer, viewerParam, logLevel, ignoreHttpsErrors, base, server, static: staticRoutes, rootUrl, viteConfig, viteConfigFile }; return resolvedConfig; } function resolveSingleInputConfig({ config, input, context, temporaryFilePrefix, themeIndexes, base }) { Logger.debug("entering single entry config mode"); let serverRootDir; let sourcePath; let workspaceDir; const inputFormat = input.format; const title = config?.title; const author = config?.author; const entries = []; const exportAliases = []; let isLocalResource = true; if (isValidUri(input.entry)) { const url = new URL(input.entry); if (url.protocol === "file:") { sourcePath = fileURLToPath(url); } else { isLocalResource = false; sourcePath = input.entry; } } else { sourcePath = upath2.resolve(context, input.entry); } if (isLocalResource) { statFileSync(sourcePath); switch (input.format) { case "webbook": case "markdown": case "pub-manifest": case "epub": workspaceDir = upath2.dirname(sourcePath); break; case "epub-opf": { const rootDir = getEpubRootDir(sourcePath); if (!rootDir) { throw new Error( `Could not determine the EPUB root directory for the OPF file: ${sourcePath}` ); } workspaceDir = rootDir; break; } default: return input.format; } serverRootDir = workspaceDir; } else { serverRootDir = UseTemporaryServerRoot; workspaceDir = context; } const themesDir = upath2.resolve(workspaceDir, "themes"); if (input.format === "markdown") { const contentType = "text/markdown"; const documentProcessor = { processorFactory: config.documentProcessor ?? VFM, metadataReader: config.documentMetadataReader ?? readMetadata }; const metadata = parseFileMetadata({ contentType, sourcePath, workspaceDir, documentMetadataReader: documentProcessor.metadataReader }); const target = toHtmlExtension( upath2.resolve( workspaceDir, `${temporaryFilePrefix}${upath2.basename(sourcePath)}` ) ); touchTmpFile(target); const themes = metadata.themes ?? config.theme?.map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) ?? []; themes.forEach((t) => themeIndexes.add(t)); entries.push({ contentType, source: { type: "file", pathname: sourcePath, contentType, documentProcessor }, target, title: metadata.title, themes }); exportAliases.push({ source: target, target: upath2.resolve( upath2.dirname(target), toHtmlExtension(upath2.basename(sourcePath)) ) }); } let fallbackTitle; let viewerInput; if (inputFormat === "markdown") { const manifestPath = upath2.resolve( workspaceDir, `${temporaryFilePrefix}${MANIFEST_FILENAME}` ); touchTmpFile(manifestPath); exportAliases.push({ source: manifestPath, target: upath2.resolve(workspaceDir, MANIFEST_FILENAME) }); fallbackTitle = entries.length === 1 && entries[0].title ? entries[0].title : upath2.basename(sourcePath); viewerInput = { type: "webpub", manifestPath, needToGenerateManifest: true }; } else if (inputFormat === "webbook") { let webbookEntryUrl; let webbookPath; if (isValidUri(sourcePath)) { const url = new URL(sourcePath); webbookEntryUrl = url.href; } else { const rootFileUrl = pathToFileURL2(workspaceDir).href; const urlPath = pathToFileURL2(sourcePath).href.slice(rootFileUrl.length); webbookEntryUrl = `${base}${urlPath}`; webbookPath = sourcePath; } viewerInput = { type: "webbook", webbookEntryUrl, webbookPath }; } else if (inputFormat === "pub-manifest") { viewerInput = { type: "webpub", manifestPath: sourcePath, needToGenerateManifest: false }; } else if (inputFormat === "epub-opf") { viewerInput = { type: "epub-opf", epubOpfPath: sourcePath }; } else if (inputFormat === "epub") { viewerInput = { type: "epub", epubPath: sourcePath, epubTmpOutputDir: upath2.join( sourcePath, `../${temporaryFilePrefix}${upath2.basename(sourcePath)}` ) }; } else { return inputFormat; } return { serverRootDir, workspaceDir, themesDir, entries, input: { format: inputFormat, entry: sourcePath }, viewerInput, exportAliases, title: title || fallbackTitle, author }; } function resolveComposedProjectConfig({ config, context, entryContextDir, outputs, temporaryFilePrefix, themeIndexes, cover }) { Logger.debug("entering composed project config mode"); const workspaceDir = upath2.resolve( context, config.workspaceDir ?? ".vivliostyle" ); const themesDir = upath2.resolve(workspaceDir, "themes"); const pkgJsonPath = upath2.resolve(context, "package.json"); const pkgJson = fs3.existsSync(pkgJsonPath) ? readJSON(pkgJsonPath) : void 0; if (pkgJson) { Logger.debug("located package.json path", pkgJsonPath); } const exportAliases = []; const rootThemes = config.theme?.map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) ?? []; rootThemes.forEach((t) => themeIndexes.add(t)); const tocConfig = { tocTitle: config.toc?.title ?? config?.tocTitle ?? TOC_TITLE, target: upath2.resolve(workspaceDir, config.toc?.htmlPath ?? TOC_FILENAME), sectionDepth: config.toc?.sectionDepth ?? 0, transform: { transformDocumentList: config.toc?.transformDocumentList, transformSectionList: config.toc?.transformSectionList } }; const coverHtml = config.cover && ("htmlPath" in config.cover && !config.cover.htmlPath ? void 0 : upath2.resolve( workspaceDir, config.cover?.htmlPath || COVER_HTML_FILENAME )); const ensureCoverImage = (src) => { const absPath = src && upath2.resolve(entryContextDir, src); if (absPath) { statFileSync(absPath, { errorMessage: "Specified cover image does not exist" }); } return absPath; }; const projectTitle = config?.title ?? pkgJson?.name; const projectAuthor = config?.author ?? pkgJson?.author; const rootDocumentProcessor = { processorFactory: config.documentProcessor ?? VFM, metadataReader: config.documentMetadataReader ?? readMetadata }; const isContentsEntry = (entry) => entry.rel === "contents"; const isCoverEntry = (entry) => entry.rel === "cover"; const isArticleEntry = (entry) => !isContentsEntry(entry) && !isCoverEntry(entry); function parseEntry(entry) { const getInputInfo = (entryPath) => { if (/^https?:/.test(entryPath)) { return { type: "uri", href: entryPath, rootDir: upath2.join(workspaceDir, new URL(entryPath).host) }; } else if (entryPath.startsWith("/")) { return { type: "uri", href: entryPath, rootDir: upath2.join(workspaceDir, "localhost") }; } const pathname = upath2.resolve(entryContextDir, entryPath); statFileSync(pathname); const rawContentType = mime(pathname); const documentProcessor = { processorFactory: "documentProcessor" in entry && entry.documentProcessor || rootDocumentProcessor.processorFactory, metadataReader: "documentMetadataReader" in entry && entry.documentMetadataReader || rootDocumentProcessor.metadataReader }; const hasCustomProcessor = !!(documentProcessor.processorFactory !== VFM || documentProcessor.metadataReader !== readMetadata); const contentType = hasCustomProcessor && rawContentType !== "text/markdown" ? "text/x-vivliostyle-custom" : rawContentType; if (!isManuscriptMediaType(contentType) || contentType === "text/plain") { throw new Error( `Invalid manuscript type ${rawContentType} detected: ${entry}` ); } const useDocumentProcessor = contentType === "text/markdown" || contentType === "text/x-vivliostyle-custom"; return { type: "file", pathname, contentType, metadata: parseFileMetadata({ contentType, sourcePath: pathname, workspaceDir, themesDir, documentMetadataReader: useDocumentProcessor ? documentProcessor.metadataReader : void 0 }), ...useDocumentProcessor && { documentProcessor } }; }; const getTargetPath = (source) => { switch (source.type) { case "file": return upath2.resolve( workspaceDir, toHtmlExtension(upath2.relative(entryContextDir, source.pathname)) ); case "uri": { const url = new URL(source.href, "a://dummy"); let pathname = url.pathname; if (!/\.\w+$/.test(pathname)) { pathname = `${pathname.replace(/\/$/, "")}/index.html`; } return upath2.join(source.rootDir, pathname); } default: return source; } }; if ((isContentsEntry(entry) || isCoverEntry(entry)) && entry.path) { const source = upath2.resolve(entryContextDir, entry.path); try { statFileSync(source); } catch (error) { Logger.logWarn( `The "path" option is set but the file does not exist: ${source} Maybe you want to set the "output" field instead.` ); entry.output = entry.path; entry.path = void 0; } } if (isContentsEntry(entry)) { const inputInfo = entry.path ? getInputInfo(entry.path) : void 0; const { metadata, ...template } = inputInfo || {}; let target = entry.output ? upath2.resolve(workspaceDir, entry.output) : inputInfo && getTargetPath(inputInfo); const themes = entry.theme ? [entry.theme].flat().map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) : metadata?.themes ?? [...rootThemes]; themes.forEach((t) => themeIndexes.add(t)); target ??= tocConfig.target; if (inputInfo?.type === "file" && pathEquals(inputInfo.pathname, target)) { const tmpPath = upath2.resolve( upath2.dirname(target), `${temporaryFilePrefix}${upath2.basename(target)}` ); exportAliases.push({ source: tmpPath, target }); touchTmpFile(tmpPath); target = tmpPath; } const parsedEntry = { rel: "contents", ...tocConfig, target, title: entry.title ?? metadata?.title ?? projectTitle, themes, pageBreakBefore: entry.pageBreakBefore, pageCounterReset: entry.pageCounterReset, ..."type" in template && { template } }; return parsedEntry; } if (isCoverEntry(entry)) { const inputInfo = entry.path ? getInputInfo(entry.path) : void 0; const { metadata, ...template } = inputInfo || {}; let target = entry.output ? upath2.resolve(workspaceDir, entry.output) : inputInfo && getTargetPath(inputInfo); const themes = entry.theme ? [entry.theme].flat().map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) : metadata?.themes ?? []; themes.forEach((t) => themeIndexes.add(t)); const coverImageSrc = ensureCoverImage(entry.imageSrc || cover?.src); if (!coverImageSrc) { throw new Error( `A CoverEntryConfig is set in the entry list but a location of cover file is not set. Please set 'cover' property in your config file.` ); } target ??= upath2.resolve( workspaceDir, entry.path || coverHtml || COVER_HTML_FILENAME ); if (inputInfo?.type === "file" && pathEquals(inputInfo.pathname, target)) { const tmpPath = upath2.resolve( upath2.dirname(target), `${temporaryFilePrefix}${upath2.basename(target)}` ); exportAliases.push({ source: tmpPath, target }); touchTmpFile(tmpPath); target = tmpPath; } const parsedEntry = { rel: "cover", target, title: entry.title ?? metadata?.title ?? projectTitle, themes, coverImageSrc, coverImageAlt: entry.imageAlt || cover?.name || COVER_HTML_IMAGE_ALT, pageBreakBefore: entry.pageBreakBefore, ..."type" in template && { template } }; return parsedEntry; } if (isArticleEntry(entry)) { const inputInfo = getInputInfo(entry.path); const { metadata, ...source } = inputInfo; const target = entry.output ? upath2.resolve(workspaceDir, entry.output) : getTargetPath(inputInfo); const themes = entry.theme ? [entry.theme].flat().map( (theme) => parseTheme({ theme, context, workspaceDir, themesDir }) ) : metadata?.themes ?? [...rootThemes]; themes.forEach((t) => themeIndexes.add(t)); const parsedEntry = { contentType: inputInfo.type === "file" ? inputInfo.contentType : "text/html", source, target, title: entry.title ?? metadata?.title ?? projectTitle, themes, ...entry.rel && { rel: entry.rel } }; return parsedEntry; } return entry; } const entries = config.entry.map(parseEntry); let fallbackProjectTitle; if (!projectTitle) { if (entries.length === 1 && entries[0].title) { fallbackProjectTitle = entries[0].title; } else { fallbackProjectTitle = upath2.basename(outputs[0].path); } } if (!!config?.toc && !entries.find(({ rel }) => rel === "contents")) { entries.unshift({ rel: "contents", ...tocConfig, themes: [...rootThemes] }); } if (cover && coverHtml && !entries.find(({ rel }) => rel === "cover")) { entries.unshift({ rel: "cover", target: coverHtml, title: projectTitle, themes: [], // Don't inherit rootThemes for cover documents coverImageSrc: ensureCoverImage(cover.src), coverImageAlt: cover.name }); } return { serverRootDir: context, workspaceDir, themesDir, entries, input: { format: "pub-manifest", entry: upath2.join(workspaceDir, MANIFEST_FILENAME) }, viewerInput: { type: "webpub", manifestPath: upath2.join(workspaceDir, MANIFEST_FILENAME), needToGenerateManifest: true }, exportAliases, title: projectTitle || fallbackProjectTitle, author: projectAuthor }; } // src/vite/vite-plugin-browser.ts import "vite"; // src/browser.ts import fs4 from "node:fs"; import upath3 from "upath"; var browserEnumMap = { chrome: "chrome", chromium: "chromium", firefox: "firefox" }; async function launchBrowser({ browserType, proxy, executablePath, headless, noSandbox, disableDevShmUsage, ignoreHttpsErrors, protocolTimeout }) { const puppeteer = await importNodeModule("puppeteer-core"); const args = []; if (browserType === "chrome" || browserType === "chromium") { args.push( "--disable-field-trial-config", "--disable-back-forward-cache", "--disable-component-update", "--no-default-browser-check", "--disable-features=AcceptCHFrame,AvoidUnnecessaryBeforeUnloadCheckSync,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument", "--enable-features=CDPScreenshotNewSurface", "--no-service-autorun", "--unsafely-disable-devtools-self-xss-warnings", "--edge-skip-compat-layer-relaunch" ); if (process.platform === "darwin") { args.push("--enable-unsafe-swiftshader"); } if (noSandbox) { args.push("--no-sandbox"); } if (headless) { args.push( "--hide-scrollbars", "--mute-audio", "--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4" ); } if (proxy?.server) { const proxyURL = new URL(proxy.server); const isSocks = proxyURL.protocol === "socks5:"; if (isSocks) { args.push( `--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"` ); } args.push(`--proxy-server=${proxy.server}`); const proxyBypassRules = []; if (proxy.bypass) { proxyBypassRules.push( ...proxy.bypass.split(",").map((t) => t.trim()).map((t) => t.startsWith(".") ? "*" + t : t) ); } proxyBypassRules.push("<-loopback>"); args.push(`--proxy-bypass-list=${proxyBypassRules.join(";")}`); } args.push("--disable-web-security"); if (disableDevShmUsage) { args.push("--disable-dev-shm-usage"); } if (headless) { args.push("--force-device-scale-factor=1"); } if (isRunningOnWSL()) { args.push("--disable-gpu"); } args.push("--lang=en"); if (!headless && process.platform === "darwin") { args.push("-AppleLanguages", "(en)"); } args.push("--no-startup-window"); } const launchOptions = { executablePath, args, browser: browserType === "chromium" ? "chrome" : browserType, headless, acceptInsecureCerts: ignoreHttpsErrors, waitForInitialPage: false, protocolTimeout }; Logger.debug("launchOptions %O", launchOptions); const browser = await puppeteer.launch({ ...launchOptions, env: { ...process.env, LANG: "en.UTF-8" } }); registerExitHandler("Closing browser", async () => { await browser.close(); }); const [browserContext] = browser.browserContexts(); return { browser, browserContext }; } function getPuppeteerCacheDir() { if (isInContainer()) { return "/opt/puppeteer"; } return upath3.join(getCacheDir(), "browsers"); } async function resolveBuildId({ type, tag, browsers }) { const cacheDataFilename = upath3.join( getPuppeteerCacheDir(), "build-ids.json" ); let cacheData; try { cacheData = JSON.parse(fs4.readFileSync(cacheDataFilename, "utf-8")); if (Date.now() - cacheData.createdAt > 24 * 60 * 60 * 1e3) { cacheData = { createdAt: Date.now(), buildIds: {} }; } } catch (_) { cacheData = { createdAt: Date.now(), buildIds: {} }; } if (cacheData.buildIds[type]?.[tag]) { return cacheData.buildIds[type][tag]; } const platform = detectBrowserPlatform(); if (!platform) { throw new Error("The current platform is not supported."); } const buildId = await browsers.resolveBuildId( browserEnumMap[type], platform, tag ); (cacheData.buildIds[type] ??= {})[tag] = buildId; fs4.mkdirSync(upath3.dirname(cacheDataFilename), { recursive: true }); fs4.writeFileSync(cacheDataFilename, JSON.stringify(cacheData)); return buildId; } async function cleanupOutdatedBrowsers() { for (const browser of Object.values(browserEnumMap)) { const browsersDir = upath3.join(getPuppeteerCacheDir(), browser); if (!fs4.existsSync(browsersDir)) { continue; } const entries = fs4.readdirSync(browsersDir); for (const entry of entries) { const entryPath = upath3.join(browsersDir, entry); const stat = fs4.statSync(entryPath); if (!stat.isDirectory() || Date.now() - stat.mtimeMs > 7 * 24 * 60 * 60 * 1e3) { Logger.debug(`Removing outdated browser at ${entryPath}`); await fs4.promises.rm(entryPath, { recursive: true, force: true }); } } } } async function getExecutableBrowserPath({ type, tag }) { const browsers = await importNodeModule("@puppeteer/browsers"); const buildId = await resolveBuildId({ type, tag, browsers }); return browsers.computeExecutablePath({ cacheDir: getPuppeteerCacheDir(), browser: browserEnumMap[type], buildId }); } function checkBrowserAvailability(path) { return fs4.existsSync(path); } async function downloadBrowser({ type, tag }) { const browsers = await importNodeModule("@puppeteer/browsers"); const buildId = await resolveBuildId({ type, tag, browsers }); let installedBrowser; if (isInContainer()) { const defaultBrowserVersion = getDefaultBrowserTag("chrome"); Logger.logWarn( `The container you are using already includes a browser (chrome@${defaultBrowserVersion}); however, the specified browser ${type}@${tag} was not found. Downloading the browser inside the container may take a long time. Consider using a container image that includes the required browser version.` ); } { var _stack = []; try { const _2 = __using(_stack, Logger.suspendLogging( "Rendering browser is not installed yet. Downloading now." )); installedBrowser = await browsers.install({ cacheDir: getPuppeteerCacheDir(), browser: browserEnumMap[type], buildId, downloadProgressCallback: "default" }); } catch (_) { var _error = _, _hasError = true; } finally { __callDispose(_stack, _error, _hasError); } } return installedBrowser.executablePath; } async function launchPreview({ mode, url, onBrowserOpen, onPageOpen, config: { browser: browserConfig, proxy, sandbox, ignoreHttpsErrors, timeout } }) { let executableBrowser = browserConfig.executablePath; Logger.debug(`Specified browser path: ${executableBrowser}`); if (executableBrowser) { if (!checkBrowserAvailability(executableBrowser)) { throw new Error( `Cannot find the browser. Please check the executable browser path: ${executableBrowser}` ); } } else if (detectBrowserPlatform() === "linux_arm" && (browserConfig.type === "chrome" || browserConfig.type === "chromium")) { Logger.logInfo( "The official Chrome/Chromium binaries are not available for ARM64 Linux. Using the system-installed Chromium browser instead." ); executableBrowser = "/usr/bin/chromium"; } else { executableBrowser = await getExecutableBrowserPath(browserConfig); Logger.debug(`Using default browser: ${executableBrowser}`); if (!checkBrowserAvailability(executableBrowser)) { await cleanupOutdatedBrowsers(); await downloadBrowser(browserConfig); } } const { browser, browserContext } = await launchBrowser({ browserType: browserConfig.type, proxy, executablePath: executableBrowser, headless: mode === "build", noSandbox: !sandbox, disableDevShmUsage: isInContainer(), ignoreHttpsErrors, protocolTimeout: timeout }); await onBrowserOpen?.(browser); const page = (await browserContext.pages())[0] ?? await browserContext.newPage(); await page.setViewport( mode === "build" ? ( // This viewport size is important to detect headless environment in Vivliostyle viewer // https://github.com/vivliostyle/vivliostyle.js/blob/73bcf323adcad80126b0175630609451ccd09d8a/packages/core/src/vivliostyle/vgen.ts#L2489-L2500 { width: 800, height: 600 } ) : null ); await onPageOpen?.(page); page.on("dialog", () => { }); if (proxy?.username && proxy?.password) { await page.authenticate({ username: proxy.username, password: proxy.password }); } await page.goto(url); return { browser, page }; } // src/server.ts import fs10 from "node:fs"; import { URL as URL2 } from "node:url"; import upath12 from "upath"; import { createServer, preview } from "vite"; // src/vite/vite-plugin-dev-server.ts import escapeRe from "escape-string-regexp"; import { pathToFileURL as pathToFileURL6 } from "node:url"; import sirv from "sirv"; import upath9 from "upath"; import "vite"; // src/processor/compile.ts import "@vivliostyle/jsdom"; import { copy as copy3, move } from "fs-extra/esm"; import fs8 from "node:fs"; import upath8 from "upath"; import serializeToXml2 from "w3c-xmlserializer"; import MIMEType2 from "whatwg-mimetype"; // src/output/webbook.ts import { copy as copy2 } from "fs-extra/esm"; import { lookup as mime3 } from "mime-types"; import fs6 from "node:fs"; import { pathToFileURL as pathToFileURL5 } from "node:url"; import { glob } from "tinyglobby"; import upath6 from "upath"; // src/processor/html.tsx import jsdom, { ResourceLoader as BaseResourceLoader, JSDOM } from "@vivliostyle/jsdom"; import DOMPurify from "dompurify"; import { toHtml } from "hast-util-to-html"; import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from "node:url"; import upath4 from "upath"; import MIMEType from "whatwg-mimetype"; import { jsx, jsxs } from "hastscript/jsx-runtime"; var createVirtualConsole = (onError) => { const virtualConsole = new jsdom.VirtualConsole(); virtualConsole.on("error", (message) => { Logger.debug("[JSDOM Console] error:", message); }); virtualConsole.on("warn", (message) => { Logger.debug("[JSDOM Console] warn:", message); }); virtualConsole.on("log", (message) => { Logger.debug("[JSDOM Console] log:", message); }); virtualConsole.on("info", (message) => { Logger.debug("[JSDOM Console] info:", message); }); virtualConsole.on("dir", (message) => { Logger.debug("[JSDOM Console] dir:", message); }); virtualConsole.on("jsdomError", (error) => { if (error.message === "Could not parse CSS stylesheet") { return; } onError( new DetailError( "Error occurred when loading content", error.stack ?? error.message ) ); }); return virtualConsole; }; var htmlPurify = DOMPurify( // @ts-expect-error: jsdom.DOMWindow should have trustedTypes property new JSDOM("").window ); var ResourceLoader = class _ResourceLoader extends BaseResourceLoader { static dataUrlOrigin = "http://localhost/"; fetcherMap = /* @__PURE__ */ new Map(); fetch(url, options) { Logger.debug(`[JSDOM] Fetching resource: ${url}`); const fetcher = super.fetch(url, options); if (fetcher) { this.fetcherMap.set(url, fetcher); } return fetcher; } static async saveFetchedResources({ fetcherMap, rootUrl, outputDir, onError }) { const rootHref = rootUrl.startsWith("data:") ? _ResourceLoader.dataUrlOrigin : /^https?:/i.test(rootUrl) ? new URL("/", rootUrl).href : new URL(".", rootUrl).href; const normalizeToLocalPath = (urlString, mimeType) => { let url = new URL(urlString); url.hash = ""; if (mimeType === "text/html" && !/\.html?$/.test(url.pathname)) { url.pathname = `${url.pathname.replace(/\/$/, "")}/index.html`; } let relTarget = upath4.relative(rootHref, url.href); return decodeURI(relTarget); }; const fetchedResources = []; await Promise.allSettled( [...fetcherMap.entries()].flatMap(async ([url, fetcher]) => { if (!url.startsWith(rootHref)) { return []; } return fetcher.then(async (buffer) => { let encodingFormat; try { const contentType = fetcher.response?.headers["content-type"]; if (contentType) { encodingFormat = new MIMEType(contentType).essence; } } catch (e) {