diff --git a/package.json b/package.json index 97275d4f..8e7283c5 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "prettier": "^3.3.3", "resolve-pkg": "^2.0.0", "serve-handler": "^6.1.3", + "sirv": "^3.0.0", "terminal-link": "^2.1.1", "tinyglobby": "^0.2.10", "tmp": "^0.2.1", @@ -62,6 +63,7 @@ "uuid": "^8.3.2", "valibot": "^0.42.1", "vfile": "^4.2.1", + "vite": "^5.4.11", "w3c-xmlserializer": "^4.0.0", "whatwg-mimetype": "^3.0.0" }, @@ -71,6 +73,7 @@ "@types/archiver": "^5.3.2", "@types/babel__code-frame": "^7.0.6", "@types/command-exists": "1.2.0", + "@types/connect": "^3.4.38", "@types/debug": "^4.1.7", "@types/dompurify": "^3.0.5", "@types/fs-extra": "^11.0.1", @@ -80,6 +83,7 @@ "@types/node": "^22.5.4", "@types/npm-package-arg": "^6.1.1", "@types/npmcli__arborist": "^5.6.0", + "@types/picomatch": "^3.0.1", "@types/serve-handler": "^6.1.1", "@types/tmp": "^0.2.1", "@types/uuid": "^8.3.1", diff --git a/src/build.ts b/src/build.ts index be118b15..8e3ca700 100644 --- a/src/build.ts +++ b/src/build.ts @@ -36,15 +36,15 @@ export async function getFullConfig( cliFlags: BuildCliFlags, ): Promise { const loadedConf = await collectVivliostyleConfig(cliFlags); - const { vivliostyleConfig, vivliostyleConfigPath } = loadedConf; + const { config: jsConfig } = loadedConf; const loadedCliFlags = loadedConf.cliFlags; - const context = vivliostyleConfig - ? upath.dirname(vivliostyleConfigPath) + const context = loadedCliFlags.configPath + ? upath.dirname(loadedCliFlags.configPath) : cwd; const configEntries: MergedConfig[] = []; - for (const entry of vivliostyleConfig ?? [vivliostyleConfig]) { + for (const entry of jsConfig ?? [jsConfig]) { const config = await mergeConfig(loadedCliFlags, entry, context); checkUnsupportedOutputs(config); diff --git a/src/input/config.ts b/src/input/config.ts index dca2fbf2..63936676 100644 --- a/src/input/config.ts +++ b/src/input/config.ts @@ -215,6 +215,8 @@ export type MergedConfig = { source: string; target: string; }[]; + cliFlags: CliFlags; + tmpPrefix: string; size: PageSize | undefined; cropMarks: boolean; bleed: string | undefined; @@ -421,82 +423,62 @@ function parseFileMetadata({ return { title, themes }; } -export async function collectVivliostyleConfig( - cliFlags: T, -): Promise< - { - cliFlags: T; - } & ( - | { - vivliostyleConfig: VivliostyleConfigEntry[]; - vivliostyleConfigPath: string; - } - | { - vivliostyleConfig?: undefined; - vivliostyleConfigPath?: undefined; - } - ) -> { - const load = async (configPath: string) => { - let config: unknown; - let jsonRaw: string | undefined; - try { - if (upath.extname(configPath) === '.json') { - jsonRaw = fs.readFileSync(configPath, 'utf8'); - config = parseJsonc(jsonRaw); - } else { - // Clear require cache to reload CJS config files - delete require.cache[require.resolve(configPath)]; - const url = pathToFileURL(configPath); - // Invalidate cache for ESM config files - // https://github.com/nodejs/node/issues/49442 - url.search = `version=${Date.now()}`; - config = (await import(url.href)).default; - jsonRaw = JSON.stringify(config, null, 2); - } - } catch (error) { - const thrownError = error as Error; - throw new DetailError( - `An error occurred on loading a config file: ${configPath}`, - thrownError.stack ?? thrownError.message, - ); - } - - const result = v.safeParse(VivliostyleConfigSchema, config); - if (result.success) { - return result.output; +export async function loadVivliostyleConfig(configPath: string) { + let config: unknown; + let jsonRaw: string | undefined; + try { + if (upath.extname(configPath) === '.json') { + jsonRaw = fs.readFileSync(configPath, 'utf8'); + config = parseJsonc(jsonRaw); } else { - const errorString = prettifySchemaError(jsonRaw, result.issues); - throw new DetailError( - `Validation of vivliostyle config failed. Please check the schema: ${configPath}`, - errorString, - ); + // Clear require cache to reload CJS config files + delete require.cache[require.resolve(configPath)]; + const url = pathToFileURL(configPath); + // Invalidate cache for ESM config files + // https://github.com/nodejs/node/issues/49442 + url.search = `version=${Date.now()}`; + config = (await import(url.href)).default; + jsonRaw = JSON.stringify(config, null, 2); } - }; + } catch (error) { + const thrownError = error as Error; + throw new DetailError( + `An error occurred on loading a config file: ${configPath}`, + thrownError.stack ?? thrownError.message, + ); + } - let configEntry: - | { - vivliostyleConfig: VivliostyleConfigEntry[]; - vivliostyleConfigPath: string; - } - | { - vivliostyleConfig?: undefined; - vivliostyleConfigPath?: undefined; - } = {}; - let vivliostyleConfigPath: string | undefined; + const result = v.safeParse(VivliostyleConfigSchema, config); + if (result.success) { + return result.output; + } else { + const errorString = prettifySchemaError(jsonRaw, result.issues); + throw new DetailError( + `Validation of vivliostyle config failed. Please check the schema: ${configPath}`, + errorString, + ); + } +} + +export async function collectVivliostyleConfig( + _cliFlags: T, +): Promise<{ + cliFlags: T; + config?: VivliostyleConfigEntry[]; +}> { + const cliFlags = { ..._cliFlags }; + let config: VivliostyleConfigEntry[] | undefined; + let configPath: string | undefined; if (cliFlags.configPath) { - vivliostyleConfigPath = upath.resolve(cwd, cliFlags.configPath); + configPath = upath.resolve(cwd, cliFlags.configPath); } else { - vivliostyleConfigPath = ['.js', '.mjs', '.cjs'] + configPath = ['.js', '.mjs', '.cjs'] .map((ext) => upath.join(cwd, `vivliostyle.config${ext}`)) .find((p) => fs.existsSync(p)); } - // let vivliostyleConfig: VivliostyleConfigSchema | undefined; - if (vivliostyleConfigPath) { - configEntry = { - vivliostyleConfigPath, - vivliostyleConfig: [await load(vivliostyleConfigPath)].flat(), - }; + if (configPath) { + config = [await loadVivliostyleConfig(configPath)].flat(); + cliFlags.configPath = configPath; } else if ( cliFlags.input && upath.basename(cliFlags.input).startsWith('vivliostyle.config') @@ -504,16 +486,13 @@ export async function collectVivliostyleConfig( // Load an input argument as a Vivliostyle config try { const inputPath = upath.resolve(cwd, cliFlags.input); - const inputConfig = await load(inputPath); - cliFlags = { - ...cliFlags, - input: undefined, - }; - configEntry = { - vivliostyleConfigPath: inputPath, - vivliostyleConfig: [inputConfig].flat(), - }; - } catch (_err) {} + const inputConfig = await loadVivliostyleConfig(inputPath); + cliFlags.configPath = inputPath; + cliFlags.input = undefined; + config = [inputConfig].flat(); + } catch (_err) { + // Ignore here because input may be a normal manuscript file + } } if (cliFlags.executableChromium) { @@ -541,7 +520,7 @@ export async function collectVivliostyleConfig( ); } - const configEntries = (configEntry.vivliostyleConfig ?? []).flat(); + const configEntries = (config ?? []).flat(); if (configEntries.some((config) => config.includeAssets)) { logWarn( chalk.yellowBright( @@ -558,10 +537,7 @@ export async function collectVivliostyleConfig( ); } - return { - cliFlags, - ...configEntry, - }; + return { cliFlags, config }; } export async function mergeConfig( @@ -614,6 +590,7 @@ export async function mergeConfig( const renderMode = cliFlags.renderMode ?? 'local'; const preflight = cliFlags.preflight ?? (pressReady ? 'press-ready' : null); const preflightOption = cliFlags.preflightOption ?? []; + const tmpPrefix = prevConfig?.tmpPrefix || `.vs-${Date.now()}.`; const documentProcessorFactory = config?.documentProcessor ?? VFM; @@ -817,6 +794,8 @@ export async function mergeConfig( outputs, themeIndexes, rootThemes, + cliFlags, + tmpPrefix, size, cropMarks, bleed, @@ -889,7 +868,6 @@ async function composeSingleInputConfig( const workspaceDir = otherConfig.workspaceDir; const entries: ParsedEntry[] = []; const exportAliases: { source: string; target: string }[] = []; - const tmpPrefix = `.vs-${Date.now()}.`; if (cliFlags.input && isUrlString(cliFlags.input)) { sourcePath = cliFlags.input; @@ -922,7 +900,7 @@ async function composeSingleInputConfig( .resolve( workspaceDir, relDir, - `${tmpPrefix}${upath.basename(sourcePath)}`, + `${otherConfig.tmpPrefix}${upath.basename(sourcePath)}`, ) .replace(/\.md$/, '.html'); await touchTmpFile(target); @@ -951,7 +929,7 @@ async function composeSingleInputConfig( // create temporary manifest file const manifestPath = upath.resolve( workspaceDir, - `${tmpPrefix}${MANIFEST_FILENAME}`, + `${otherConfig.tmpPrefix}${MANIFEST_FILENAME}`, ); await touchTmpFile(manifestPath); exportAliases.push({ @@ -1027,7 +1005,6 @@ async function composeProjectConfig( debug('located package.json path', pkgJsonPath); } const exportAliases: { source: string; target: string }[] = []; - const tmpPrefix = `.vs-${Date.now()}.`; const tocConfig = (() => { const c = @@ -1131,7 +1108,7 @@ async function composeProjectConfig( if (inputInfo?.source && pathEquals(inputInfo.source, target)) { const tmpPath = upath.resolve( upath.dirname(target), - `${tmpPrefix}${upath.basename(target)}`, + `${otherConfig.tmpPrefix}${upath.basename(target)}`, ); exportAliases.push({ source: tmpPath, target }); await touchTmpFile(tmpPath); @@ -1181,7 +1158,7 @@ async function composeProjectConfig( if (inputInfo?.source && pathEquals(inputInfo.source, target)) { const tmpPath = upath.resolve( upath.dirname(target), - `${tmpPrefix}${upath.basename(target)}`, + `${otherConfig.tmpPrefix}${upath.basename(target)}`, ); exportAliases.push({ source: tmpPath, target }); await touchTmpFile(tmpPath); diff --git a/src/output/webbook.ts b/src/output/webbook.ts index 757bc7c5..5aa1dca1 100644 --- a/src/output/webbook.ts +++ b/src/output/webbook.ts @@ -191,6 +191,7 @@ export function writePublicationManifest( : (thrownError.stack ?? thrownError.message), ); } + fs.mkdirSync(upath.dirname(output), { recursive: true }); fs.writeFileSync(output, JSON.stringify(encodedManifest, null, 2)); return publication; } diff --git a/src/preview.ts b/src/preview.ts index 875055ce..a6882e8f 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -1,4 +1,5 @@ import chokidar from 'chokidar'; +import { AddressInfo } from 'node:net'; import upath from 'upath'; import { checkBrowserAvailability, @@ -17,7 +18,7 @@ import { copyAssets, prepareThemeDirectory, } from './processor/compile.js'; -import { prepareServer } from './server.js'; +import { createViteServer, prepareServer } from './server.js'; import { cwd, debug, @@ -36,26 +37,39 @@ let timer: NodeJS.Timeout; export interface PreviewCliFlags extends CliFlags {} +export async function preview(cliFlags: PreviewCliFlags) { + const { cliFlags: resolvedCliFlags } = + await collectVivliostyleConfig(cliFlags); + const { configPath } = resolvedCliFlags; + const context = configPath ? upath.dirname(configPath) : cwd; + const viteServer = await createViteServer({ + cliFlags: resolvedCliFlags, + context, + }); + const dev = await viteServer.listen(13000); + const { port } = dev.httpServer!.address() as AddressInfo; + console.log(`Vite server running at http://localhost:${port}`); +} + /** * Open a preview of the publication. * * @param cliFlags * @returns */ -export async function preview(cliFlags: PreviewCliFlags) { +export async function _preview(cliFlags: PreviewCliFlags) { setLogLevel(cliFlags.logLevel); const stopLogging = startLogging('Collecting preview config'); const loadedConf = await collectVivliostyleConfig(cliFlags); - const { vivliostyleConfig, vivliostyleConfigPath } = loadedConf; + const { config: jsConfig, cliFlags: resolvedCliFlags } = loadedConf; + const { configPath } = resolvedCliFlags; cliFlags = loadedConf.cliFlags; - const context = vivliostyleConfig - ? upath.dirname(vivliostyleConfigPath) - : cwd; + const context = configPath ? upath.dirname(configPath) : cwd; - if (!cliFlags.input && !vivliostyleConfig) { + if (!cliFlags.input && !jsConfig) { // Empty input, open Viewer start page cliFlags.input = 'data:,'; } @@ -63,7 +77,7 @@ export async function preview(cliFlags: PreviewCliFlags) { let config = await mergeConfig( cliFlags, // Only show preview of first entry - vivliostyleConfig?.[0], + jsConfig?.[0], context, ); @@ -213,8 +227,8 @@ export async function preview(cliFlags: PreviewCliFlags) { closePreview?.(); // reload vivliostyle config const loadedConf = await collectVivliostyleConfig(cliFlags); - const { vivliostyleConfig } = loadedConf; - config = await mergeConfig(cliFlags, vivliostyleConfig?.[0], context); + const { config: jsConfig } = loadedConf; + config = await mergeConfig(cliFlags, jsConfig?.[0], context); // build artifacts if (config.manifestPath) { await prepareThemeDirectory(config); @@ -229,12 +243,12 @@ export async function preview(cliFlags: PreviewCliFlags) { async function rebuildFile(path: string) { const stopLogging = startLogging(`Rebuilding ${path}`); // update mergedConfig - config = await mergeConfig( - cliFlags, - vivliostyleConfig?.[0], - context, - config, - ); + // config = await mergeConfig( + // cliFlags, + // vivliostyleConfig?.[0], + // context, + // config, + // ); // build artifacts if (config.manifestPath) { await prepareThemeDirectory(config); @@ -260,10 +274,7 @@ export async function preview(cliFlags: PreviewCliFlags) { ) { clearTimeout(timer); timer = setTimeout(() => rebuildFile(path).catch(handleError), 2000); - } else if ( - vivliostyleConfigPath && - pathEquals(path, upath.basename(vivliostyleConfigPath)) - ) { + } else if (configPath && pathEquals(path, upath.basename(configPath))) { clearTimeout(timer); timer = setTimeout(() => reloadConfig(path).catch(handleError), 0); } diff --git a/src/processor/compile.ts b/src/processor/compile.ts index e100bb7c..53cac40c 100644 --- a/src/processor/compile.ts +++ b/src/processor/compile.ts @@ -7,6 +7,7 @@ import { CoverEntry, ManuscriptEntry, MergedConfig, + ParsedEntry, ParsedTheme, WebPublicationManifestConfig, } from '../input/config.js'; @@ -126,163 +127,172 @@ export async function prepareThemeDirectory({ } } -export async function compile({ - entryContextDir, - workspaceDir, - manifestPath, - needToGenerateManifest, - title, - author, - entries, - language, - readingProgression, - cover, - documentProcessorFactory, - vfmOptions, -}: MergedConfig & WebPublicationManifestConfig): Promise { - const manuscriptEntries = entries.filter( - (e): e is ManuscriptEntry => 'source' in e, - ); - const processedTocEntries: { entry: ContentsEntry; content: string }[] = []; - const processedCoverEntries: { entry: CoverEntry; content: string }[] = []; - - for (const entry of entries) { - const { source, type } = - entry.rel === 'contents' || entry.rel === 'cover' - ? (entry as ContentsEntry | CoverEntry).template || {} - : (entry as ManuscriptEntry); - let content: string; +export async function transformManuscript( + entry: ParsedEntry, + { + entryContextDir, + workspaceDir, + manifestPath, + title, + entries, + language, + documentProcessorFactory, + vfmOptions, + }: MergedConfig & WebPublicationManifestConfig, +): Promise { + const { source, type } = + entry.rel === 'contents' || entry.rel === 'cover' + ? (entry as ContentsEntry | CoverEntry).template || {} + : (entry as ManuscriptEntry); + let content: string | undefined = undefined; - // calculate style path - const style = entry.themes.flatMap((theme) => - locateThemePath(theme, upath.dirname(entry.target)), - ); + // calculate style path + const style = entry.themes.flatMap((theme) => + locateThemePath(theme, upath.dirname(entry.target)), + ); - if (source && type) { - if (type === 'text/markdown') { - // compile markdown - const vfile = await processMarkdown(documentProcessorFactory, source, { - ...vfmOptions, - style, - title: entry.title, - language: language ?? undefined, - }); - content = String(vfile); - } else if (type === 'text/html' || type === 'application/xhtml+xml') { - content = fs.readFileSync(source, 'utf8'); - content = processManuscriptHtml(content, { - style, - title: entry.title, - contentType: type, - language, - }); - } else { - if (!pathEquals(source, entry.target)) { - await copy(source, entry.target); - } - continue; - } - } else if (entry.rel === 'contents') { - content = generateDefaultTocHtml({ - language, - title, - }); - content = processManuscriptHtml(content, { + if (source && type) { + if (type === 'text/markdown') { + // compile markdown + const vfile = await processMarkdown(documentProcessorFactory, source, { + ...vfmOptions, style, - title, - contentType: 'text/html', - language, + title: entry.title, + language: language ?? undefined, }); - } else if (entry.rel === 'cover') { - content = generateDefaultCoverHtml({ language, title: entry.title }); + content = String(vfile); + } else if (type === 'text/html' || type === 'application/xhtml+xml') { + content = fs.readFileSync(source, 'utf8'); content = processManuscriptHtml(content, { style, title: entry.title, - contentType: 'text/html', + contentType: type, language, }); } else { - continue; - } - - if (entry.rel === 'contents') { - processedTocEntries.push({ - entry: entry as ContentsEntry, - content, - }); - continue; - } else if (entry.rel === 'cover') { - processedCoverEntries.push({ - entry: entry as CoverEntry, - content, - }); - continue; + if (!pathEquals(source, entry.target)) { + await copy(source, entry.target); + } } + } else if (entry.rel === 'contents') { + content = generateDefaultTocHtml({ + language, + title, + }); + content = processManuscriptHtml(content, { + style, + title, + contentType: 'text/html', + language, + }); + } else if (entry.rel === 'cover') { + content = generateDefaultCoverHtml({ language, title: entry.title }); + content = processManuscriptHtml(content, { + style, + title: entry.title, + contentType: 'text/html', + language, + }); + } - if (!source || !pathEquals(source, entry.target)) { - fs.mkdirSync(upath.dirname(entry.target), { recursive: true }); - fs.writeFileSync(entry.target, content); - } + if (!content) { + return; } - for (const { entry, content } of processedTocEntries) { - const transformedContent = await processTocHtml(content, { + if (entry.rel === 'contents') { + const contentsEntry = entry as ContentsEntry; + const manuscriptEntries = entries.filter( + (e): e is ManuscriptEntry => 'source' in e, + ); + content = await processTocHtml(content, { entries: manuscriptEntries, manifestPath, - distDir: upath.dirname(entry.target), - tocTitle: entry.tocTitle, - sectionDepth: entry.sectionDepth, - styleOptions: entry, - transform: entry.transform, + distDir: upath.dirname(contentsEntry.target), + tocTitle: contentsEntry.tocTitle, + sectionDepth: contentsEntry.sectionDepth, + styleOptions: contentsEntry, + transform: contentsEntry.transform, }); - fs.mkdirSync(upath.dirname(entry.target), { recursive: true }); - fs.writeFileSync(entry.target, transformedContent); } - for (const { entry, content } of processedCoverEntries) { - const transformedContent = await processCoverHtml(content, { + if (entry.rel === 'cover') { + const coverEntry = entry as CoverEntry; + content = await processCoverHtml(content, { imageSrc: upath.relative( upath.join( entryContextDir, - upath.relative(workspaceDir, entry.target), + upath.relative(workspaceDir, coverEntry.target), '..', ), - entry.coverImageSrc, + coverEntry.coverImageSrc, ), - imageAlt: entry.coverImageAlt, - styleOptions: entry, + imageAlt: coverEntry.coverImageAlt, + styleOptions: coverEntry, }); + } + + const contentBuffer = Buffer.from(content, 'utf8'); + if ( + (!source || !pathEquals(source, entry.target)) && + // write only if the content is changed to avoid file update events + (!fs.existsSync(entry.target) || + !fs.readFileSync(entry.target).equals(contentBuffer)) + ) { fs.mkdirSync(upath.dirname(entry.target), { recursive: true }); - fs.writeFileSync(entry.target, transformedContent); + fs.writeFileSync(entry.target, contentBuffer); + } + + return content; +} + +export async function generateManifest({ + entryContextDir, + workspaceDir, + manifestPath, + title, + author, + entries, + language, + readingProgression, + cover, +}: MergedConfig & WebPublicationManifestConfig) { + const manifestEntries: ArticleEntryObject[] = entries.map((entry) => ({ + title: + (entry.rel === 'contents' && (entry as ContentsEntry).tocTitle) || + entry.title, + path: upath.relative(workspaceDir, entry.target), + encodingFormat: + !('type' in entry) || + entry.type === 'text/markdown' || + entry.type === 'text/html' + ? undefined + : entry.type, + rel: entry.rel, + })); + writePublicationManifest(manifestPath, { + title, + author, + language, + readingProgression, + cover: cover && { + url: upath.relative(entryContextDir, cover.src), + name: cover.name, + }, + entries: manifestEntries, + modified: new Date().toISOString(), + }); +} + +export async function compile( + config: MergedConfig & WebPublicationManifestConfig, +): Promise { + for (const entry of config.entries) { + await transformManuscript(entry, config); } // generate manifest - if (needToGenerateManifest) { - const manifestEntries: ArticleEntryObject[] = entries.map((entry) => ({ - title: - (entry.rel === 'contents' && (entry as ContentsEntry).tocTitle) || - entry.title, - path: upath.relative(workspaceDir, entry.target), - encodingFormat: - !('type' in entry) || - entry.type === 'text/markdown' || - entry.type === 'text/html' - ? undefined - : entry.type, - rel: entry.rel, - })); - writePublicationManifest(manifestPath, { - title, - author, - language, - readingProgression, - cover: cover && { - url: upath.relative(entryContextDir, cover.src), - name: cover.name, - }, - entries: manifestEntries, - modified: new Date().toISOString(), - }); + if (config.needToGenerateManifest) { + await generateManifest(config); } } diff --git a/src/server.ts b/src/server.ts index 8a466bd8..14ab8c1a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,13 +2,21 @@ import http from 'node:http'; import { fileURLToPath, pathToFileURL, URL } from 'node:url'; import handler from 'serve-handler'; import upath from 'upath'; +import * as vite from 'vite'; import { viewerRoot } from './const.js'; +import { CliFlags } from './input/config.js'; import { beforeExitHandlers, debug, findAvailablePort, isUrlString, } from './util.js'; +import { reloadConfig } from './vite/plugin-util.js'; +import { vsBrowserPlugin } from './vite/vite-plugin-browser.js'; +import { vsCopyAssetsPlugin } from './vite/vite-plugin-copy-assets.js'; +import { vsDevServerPlugin } from './vite/vite-plugin-dev-server.js'; +import { vsThemesPlugin } from './vite/vite-plugin-themes.js'; +import { vsViewerPlugin } from './vite/vite-plugin-viewer.js'; export type PageSize = { format: string } | { width: string; height: string }; @@ -216,3 +224,41 @@ async function launchServer(root: string): Promise { }); }); } + +export async function createViteConfig({ + cliFlags, + context, +}: { + cliFlags: CliFlags; + context: string; +}) { + const config = await reloadConfig({ cliFlags, context }); + + const viteConfig = { + clearScreen: false, + configFile: false, + appType: 'custom', + root: config.workspaceDir, + plugins: [ + vsDevServerPlugin({ config }), + vsViewerPlugin({ config }), + vsCopyAssetsPlugin({ config }), + vsBrowserPlugin({ config }), + vsThemesPlugin({ config }), + ], + } satisfies vite.InlineConfig; + return viteConfig; +} + +export async function createViteServer({ + cliFlags, + context, +}: { + cliFlags: CliFlags; + context: string; +}) { + const viteConfig = await createViteConfig({ cliFlags, context }); + const server = await vite.createServer(viteConfig); + + return server; +} diff --git a/src/vite/plugin-util.ts b/src/vite/plugin-util.ts new file mode 100644 index 00000000..4dce0789 --- /dev/null +++ b/src/vite/plugin-util.ts @@ -0,0 +1,25 @@ +import { + CliFlags, + loadVivliostyleConfig, + mergeConfig, + MergedConfig, +} from '../input/config.js'; + +const headStartTagRe = /]*>/i; +export const prependToHead = (html: string, content: string) => + html.replace(headStartTagRe, (match) => `${match}\n${content}`); + +export async function reloadConfig({ + cliFlags, + context, + prevConfig, +}: { + cliFlags: CliFlags; + context: string; + prevConfig?: MergedConfig; +}) { + const jsConfig = cliFlags.configPath + ? [await loadVivliostyleConfig(cliFlags.configPath)].flat()[0] + : undefined; + return await mergeConfig(cliFlags, jsConfig, context, prevConfig); +} diff --git a/src/vite/server.ts b/src/vite/server.ts new file mode 100644 index 00000000..6366b7b1 --- /dev/null +++ b/src/vite/server.ts @@ -0,0 +1,44 @@ +import * as vite from 'vite'; +import { CliFlags } from '../input/config.js'; +import { reloadConfig } from './plugin-util.js'; +import { vsBrowserPlugin } from './vite-plugin-browser.js'; +import { vsCopyAssetsPlugin } from './vite-plugin-copy-assets.js'; +import { vsDevServerPlugin } from './vite-plugin-dev-server.js'; +import { vsViewerPlugin } from './vite-plugin-viewer.js'; + +export async function createViteConfig({ + cliFlags, + context, +}: { + cliFlags: CliFlags; + context: string; +}) { + const config = await reloadConfig({ cliFlags, context }); + + const viteConfig = { + clearScreen: false, + configFile: false, + appType: 'custom', + root: config.workspaceDir, + plugins: [ + vsDevServerPlugin({ config }), + vsViewerPlugin({ config }), + vsCopyAssetsPlugin({ config }), + vsBrowserPlugin({ config }), + ], + } satisfies vite.InlineConfig; + return viteConfig; +} + +export async function createViteServer({ + cliFlags, + context, +}: { + cliFlags: CliFlags; + context: string; +}) { + const viteConfig = await createViteConfig({ cliFlags, context }); + const server = await vite.createServer(viteConfig); + + return server; +} diff --git a/src/vite/vite-plugin-browser.ts b/src/vite/vite-plugin-browser.ts new file mode 100644 index 00000000..46eaa3e4 --- /dev/null +++ b/src/vite/vite-plugin-browser.ts @@ -0,0 +1,135 @@ +import { fileURLToPath, pathToFileURL } from 'node:url'; +import upath from 'upath'; +import * as vite from 'vite'; +import { + checkBrowserAvailability, + downloadBrowser, + isPlaywrightExecutable, + launchBrowser, +} from '../browser.js'; +import { MergedConfig } from '../input/config.js'; +import { getViewerFullUrl } from '../server.js'; +import { debug, isUrlString, runExitHandlers } from '../util.js'; +import { viewerRootPath } from './vite-plugin-viewer.js'; + +async function openPreview( + { listenUrl, handleClose }: { listenUrl: string; handleClose: () => void }, + config: MergedConfig, +) { + const input = (config.manifestPath ?? + config.webbookEntryUrl ?? + config.epubOpfPath) as string; + const inputUrl = isUrlString(input) ? new URL(input) : pathToFileURL(input); + viewerRootPath; + const viewerUrl = new URL(`${viewerRootPath}/index.html`, listenUrl); + const sourceUrl = new URL(listenUrl); + sourceUrl.pathname = upath.join( + '/', + upath.relative(config.workspaceDir, fileURLToPath(inputUrl)), + ); + const viewerFullUrl = getViewerFullUrl( + { + size: config.size, + cropMarks: config.cropMarks, + bleed: config.bleed, + cropOffset: config.cropOffset, + css: config.css, + style: config.customStyle, + userStyle: config.customUserStyle, + singleDoc: config.singleDoc, + quick: config.quick, + viewerParam: config.viewerParam, + }, + { viewerUrl, sourceUrl }, + ); + + const { browserType, proxy, executableBrowser } = config; + debug(`Executing browser path: ${executableBrowser}`); + if (!checkBrowserAvailability(executableBrowser)) { + if (isPlaywrightExecutable(executableBrowser)) { + // The browser isn't downloaded first time starting CLI so try to download it + await downloadBrowser(browserType); + } else { + // executableBrowser seems to be specified explicitly + throw new Error( + `Cannot find the browser. Please check the executable browser path: ${executableBrowser}`, + ); + } + } + + const browser = await launchBrowser({ + browserType, + proxy, + executablePath: executableBrowser, + headless: false, + noSandbox: !config.sandbox, + disableWebSecurity: !config.viewer, + }); + const page = await browser.newPage({ + viewport: null, + ignoreHTTPSErrors: config.ignoreHttpsErrors, + }); + + page.on('close', handleClose); + + // Vivliostyle Viewer uses `i18nextLng` in localStorage for UI language + const locale = Intl.DateTimeFormat().resolvedOptions().locale; + await page.addInitScript( + `window.localStorage.setItem('i18nextLng', '${locale}');`, + ); + + // Prevent confirm dialog from being auto-dismissed + page.on('dialog', () => {}); + + await page.goto(viewerFullUrl); + + // Move focus from the address bar to the page + await page.bringToFront(); + // Focus to the URL input box if available + await page.locator('#vivliostyle-input-url').focus({ timeout: 0 }); + + return { + reload: () => page.reload(), + close: () => { + page.off('close', handleClose); + browser.close(); + }, + }; +} + +export function vsBrowserPlugin({ + config, +}: { + config: MergedConfig; +}): vite.Plugin { + let server: vite.ViteDevServer | undefined; + + return { + name: 'vivliostyle:browser', + configureServer(viteServer) { + server = viteServer; + + const _listen = viteServer.listen; + viteServer.listen = async (...args) => { + const server = await _listen(...args); + + // Terminate preview when the previewing page is closed + async function handleClose() { + await server?.close(); + runExitHandlers(); + } + + if (server.resolvedUrls?.local.length) { + openPreview( + { + listenUrl: server.resolvedUrls.local[0], + handleClose, + }, + config, + ); + } + return server; + }; + }, + }; +} diff --git a/src/vite/vite-plugin-copy-assets.ts b/src/vite/vite-plugin-copy-assets.ts new file mode 100644 index 00000000..f971a113 --- /dev/null +++ b/src/vite/vite-plugin-copy-assets.ts @@ -0,0 +1,89 @@ +import { NextHandleFunction } from 'connect'; +import picomatch from 'picomatch'; +import sirv, { RequestHandler } from 'sirv'; +import * as vite from 'vite'; +import { MergedConfig } from '../input/config.js'; +import { + getDefaultIgnorePatterns, + getIgnoreAssetPatterns, +} from '../processor/compile.js'; +import { reloadConfig } from './plugin-util.js'; + +export function vsCopyAssetsPlugin({ + config: _config, +}: { + config: MergedConfig; +}): vite.Plugin { + let config = _config; + let matcher: picomatch.Matcher; + let serve: RequestHandler; + + async function reload() { + config = await reloadConfig({ + cliFlags: config.cliFlags, + context: config.entryContextDir, + prevConfig: config, + }); + + const { + entryContextDir: cwd, + themesDir, + copyAsset, + outputs, + entries, + } = config; + const ignorePatterns = [ + ...copyAsset.excludes, + ...getIgnoreAssetPatterns({ outputs, entries, cwd }), + ]; + const weakIgnorePatterns = getDefaultIgnorePatterns({ themesDir, cwd }); + + // Step 1: Glob files with an extension in `fileExtension` + // Ignore files in node_modules directory, theme example files and files matched `excludes` + const fileExtensionMatcher = picomatch( + copyAsset.fileExtensions.map((ext) => `**/*.${ext}`), + { + ignore: [...ignorePatterns, ...weakIgnorePatterns], + }, + ); + // Step 2: Glob files matched with `includes` + // Ignore only files matched `excludes` + const includeMatcher = picomatch(copyAsset.includes, { + ignore: ignorePatterns, + }); + matcher = (test: string) => + fileExtensionMatcher(test) || includeMatcher(test); + serve = sirv(cwd, { dev: true, etag: false, extensions: [] }); + } + + const middleware = async function vivliostyleCopyAssetsMiddleware( + req, + res, + next, + ) { + if (matcher(req.url!)) { + return serve(req, res, next); + } + next(); + } satisfies NextHandleFunction; + + return { + name: 'vivliostyle:copy-assets', + async configureServer(viteServer) { + const { configPath } = config.cliFlags; + if (configPath) { + viteServer.watcher.add(configPath); + } + viteServer.watcher.on('change', async (pathname) => { + if (pathname === configPath) { + await reload(); + } + }); + + await reload(); + return () => { + viteServer.middlewares.use(middleware); + }; + }, + }; +} diff --git a/src/vite/vite-plugin-dev-server.ts b/src/vite/vite-plugin-dev-server.ts new file mode 100644 index 00000000..1ae91e56 --- /dev/null +++ b/src/vite/vite-plugin-dev-server.ts @@ -0,0 +1,227 @@ +import { NextHandleFunction } from 'connect'; +import { pathToFileURL } from 'node:url'; +import upath from 'upath'; +import * as vite from 'vite'; +import { + MergedConfig, + ParsedEntry, + WebPublicationManifestConfig, +} from '../input/config.js'; +import { generateManifest, transformManuscript } from '../processor/compile.js'; +import { prependToHead, reloadConfig } from './plugin-util.js'; + +const urlSplitRe = /^([^?#]*)([?#].*)?$/; + +// Ref: https://github.com/lukeed/sirv +function createEntriesRouteLookup(entries: ParsedEntry[], cwd: string) { + const extns = ['', 'html', 'htm']; + const toAssume = (uri: string) => { + let i = 0, + x, + len = uri.length - 1; + if (uri.charCodeAt(len) === 47) { + uri = uri.substring(0, len); + } + let arr = [], + tmp = `${uri}/index`; + for (; i < extns.length; i++) { + x = extns[i] ? `.${extns[i]}` : ''; + if (uri) arr.push(uri + x); + arr.push(tmp + x); + } + + return arr; + }; + const cache = entries.reduce>((acc, e) => { + acc[`/${upath.relative(cwd, e.target).normalize().replace(/\\+/g, '/')}`] = + e; + return acc; + }, {}); + return (uri: string) => { + let i = 0, + data, + arr = toAssume(uri); + for (; i < arr.length; i++) { + if ((data = cache[arr[i]])) return [data, arr[i]] as const; + } + }; +} + +export function vsDevServerPlugin({ + config: _config, +}: { + config: MergedConfig; +}): vite.Plugin { + let config = _config; + let server: vite.ViteDevServer | undefined; + let transformCache: Map = + new Map(); + let entriesLookup: ( + uri: string, + ) => readonly [ParsedEntry, string] | undefined; + const projectDeps = new Set(); + + async function reload(forceUpdate = false) { + const prevConfig = config; + config = await reloadConfig({ + cliFlags: config.cliFlags, + context: config.entryContextDir, + prevConfig, + }); + + transformCache.clear(); + const needToUpdateManifest = + forceUpdate || + // FIXME: More precise comparison + JSON.stringify(prevConfig) !== JSON.stringify(config); + if (config.needToGenerateManifest && needToUpdateManifest) { + await generateManifest(config); + } + + entriesLookup = createEntriesRouteLookup( + config.entries, + config.workspaceDir, + ); + if (config.cliFlags.configPath) { + projectDeps.add(config.cliFlags.configPath); + server?.watcher.add(config.cliFlags.configPath); + } + if (config.manifestPath) { + projectDeps.add(config.manifestPath); + server?.watcher.add(config.manifestPath); + } + } + + async function transform( + entry: ParsedEntry, + config: MergedConfig & WebPublicationManifestConfig, + ) { + let html = await transformManuscript(entry, config); + if (!html) { + return; + } + // Inject Vite client script to enable HMR + html = prependToHead( + html, + '', + ); + const etag = `W/"${Date.now()}"`; + transformCache.set(entry.target, { content: html, etag }); + if (entry.source) { + server?.watcher.add(entry.source); + } + return { content: html, etag }; + } + + async function invalidate(entry: ParsedEntry, config: MergedConfig) { + const cwd = pathToFileURL(config.workspaceDir); + const target = pathToFileURL(entry.target); + if (target.href.indexOf(cwd.href) !== 0) { + return; + } + transformCache.delete(entry.target); + config.entries + .filter((entry) => entry.rel === 'contents') + .forEach((entry) => { + transformCache.delete(entry.target); + }); + server?.ws.send({ + type: 'full-reload', + path: target.href.slice(cwd.href.length), + }); + } + + const middleware = async function vivliostyleDevServerMiddleware( + req, + res, + next, + ) { + if (!config.manifestPath) { + return next(); + } + const [_, pathname, qs] = decodeURI(req.url!).match(urlSplitRe) ?? []; + const match = pathname && entriesLookup?.(pathname); + if (!match) { + return next(); + } + const [entry, expected] = match; + // Enforce using the actual path to match the full-reload event of the Vite client + if (pathname !== expected) { + res.statusCode = 301; + res.setHeader('Location', `${expected}${qs || ''}`); + return res.end(); + } + const cached = transformCache.get(entry.target); + if (cached) { + if (req.headers['if-none-match'] === cached.etag) { + res.statusCode = 304; + return res.end(); + } else { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html;charset=utf-8'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Etag', cached.etag); + return res.end(cached.content); + } + } + + if (entry.rel === 'contents') { + // To transpile the table of contents, all dependent content must be transpiled in advance + await Promise.all( + config.entries.flatMap((e) => + config.manifestPath && e.rel !== 'contents' && e.rel !== 'cover' + ? transform(e, config) + : [], + ), + ); + } + const result = await transform(entry, config); + if (!result) { + return next(); + } + + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html;charset=utf-8'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Etag', result.etag); + return res.end(result.content); + } satisfies NextHandleFunction; + + return { + name: 'vivliostyle:dev-server', + enforce: 'pre', + + async configureServer(viteServer) { + server = viteServer; + + const handleUpdate = async (pathname: string) => { + if (!projectDeps.has(pathname)) { + return; + } + await reload(); + viteServer.ws.send({ + type: 'full-reload', + path: '*', + }); + }; + viteServer.watcher.on('add', handleUpdate); + viteServer.watcher.on('change', handleUpdate); + viteServer.watcher.on('unlink', handleUpdate); + + return () => { + viteServer.middlewares.use(middleware); + }; + }, + async buildStart() { + await reload(true); + }, + async handleHotUpdate(ctx) { + const entry = config.entries.find( + (e) => e.source === ctx.file || (!e.source && e.target === ctx.file), + ); + if (entry) { + await invalidate(entry, config); + } + }, + }; +} diff --git a/src/vite/vite-plugin-themes.ts b/src/vite/vite-plugin-themes.ts new file mode 100644 index 00000000..448dc3f7 --- /dev/null +++ b/src/vite/vite-plugin-themes.ts @@ -0,0 +1,51 @@ +import { pathToFileURL } from 'node:url'; +import * as vite from 'vite'; +import { MergedConfig } from '../input/config.js'; +import { prepareThemeDirectory } from '../processor/compile.js'; +import { reloadConfig } from './plugin-util.js'; + +export function vsThemesPlugin({ + config: _config, +}: { + config: MergedConfig; +}): vite.Plugin { + let config = _config; + let themesRootPath: string; + + async function reload() { + config = await reloadConfig({ + cliFlags: config.cliFlags, + context: config.entryContextDir, + prevConfig: config, + }); + const cwd = pathToFileURL(config.workspaceDir); + const themesDir = pathToFileURL(config.themesDir); + if (themesDir.href.indexOf(cwd.href) !== 0) { + return; + } + themesRootPath = themesDir.pathname.slice(cwd.pathname.length); + + await prepareThemeDirectory(config); + } + + return { + name: 'vivliostyle:themes', + enforce: 'pre', + + config() { + return { + optimizeDeps: { + exclude: ['@vivliostyle/viewer'], + }, + } satisfies vite.UserConfig; + }, + async buildStart() { + await reload(); + }, + resolveId(id) { + if (id.startsWith(themesRootPath)) { + return `/@vivliostyle:themes${id.slice(themesRootPath.length)}`; + } + }, + }; +} diff --git a/src/vite/vite-plugin-viewer.ts b/src/vite/vite-plugin-viewer.ts new file mode 100644 index 00000000..a3544abf --- /dev/null +++ b/src/vite/vite-plugin-viewer.ts @@ -0,0 +1,70 @@ +import { NextHandleFunction } from 'connect'; +import fs from 'node:fs'; +import sirv from 'sirv'; +import upath from 'upath'; +import * as vite from 'vite'; +import { viewerRoot } from '../const.js'; +import { MergedConfig } from '../input/config.js'; +import { prependToHead } from './plugin-util.js'; + +const viewerClientId = '@vivliostyle:viewer:client'; +const viewerClientRequestPath = `/${viewerClientId}`; +const viewerClientContent = /* js */ ` +if (import.meta.hot) { + import.meta.hot.on('vite:beforeFullReload', (e) => { + location.reload(); + }); +}`; +export const viewerRootPath = '/__vivliostyle-viewer'; + +export function vsViewerPlugin(_: { config: MergedConfig }): vite.Plugin { + const serveRootDir = upath.join(viewerRoot, 'lib'); + const serve = sirv(serveRootDir, { dev: false, etag: true }); + let cachedIndexHtml: string; + + const middleware = async function vivliostyleViewerMiddleware( + req, + res, + next, + ) { + const pathname = req.url!; + if (!pathname.startsWith(viewerRootPath)) { + return next(); + } + + req.url = pathname.slice(viewerRootPath.length); + if (req.url === '/' || req.url === '/index.html') { + cachedIndexHtml ??= prependToHead( + fs.readFileSync(upath.join(serveRootDir, 'index.html'), 'utf-8'), + ``, + ); + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html;charset=utf-8'); + res.setHeader('Cache-Control', 'no-cache'); + return res.end(cachedIndexHtml); + } else { + return serve(req, res, next); + } + } satisfies NextHandleFunction; + + return { + name: 'vivliostyle:viewer', + config() { + return { + optimizeDeps: { + exclude: ['@vivliostyle/viewer'], + }, + } satisfies vite.UserConfig; + }, + configureServer(viteServer) { + return () => { + viteServer.middlewares.use(middleware); + }; + }, + load(id) { + if (id === viewerClientRequestPath) { + return viewerClientContent; + } + }, + }; +} diff --git a/tests/command-util.ts b/tests/command-util.ts index 37ba7a73..e16aef32 100644 --- a/tests/command-util.ts +++ b/tests/command-util.ts @@ -21,22 +21,19 @@ export const getMergedConfig = async ( ...args, ]); const options = program.opts(); - const { vivliostyleConfig, vivliostyleConfigPath, cliFlags } = - await collectVivliostyleConfig({ - ...program.opts(), - input: program.args?.[0], - configPath: options.config, - targets: options.targets, - }); - const context = vivliostyleConfig - ? upath.dirname(vivliostyleConfigPath) + const { config, cliFlags } = await collectVivliostyleConfig({ + ...program.opts(), + input: program.args?.[0], + configPath: options.config, + targets: options.targets, + }); + const context = cliFlags.configPath + ? upath.dirname(cliFlags.configPath) : upath.join(rootPath, 'tests'); - const config = await Promise.all( - (vivliostyleConfig ?? [vivliostyleConfig]).map((entry) => - mergeConfig(cliFlags, entry, context), - ), + const mergedConfig = await Promise.all( + (config ?? [config]).map((entry) => mergeConfig(cliFlags, entry, context)), ); - return config.length > 1 ? config : config[0]; + return mergedConfig.length > 1 ? mergedConfig : mergedConfig[0]; }; export const maskConfig = (obj: any) => { diff --git a/tests/fixtures/cover/publication.json b/tests/fixtures/cover/publication.json new file mode 100644 index 00000000..c2d1b5c1 --- /dev/null +++ b/tests/fixtures/cover/publication.json @@ -0,0 +1,24 @@ +{ + "@context": ["https://schema.org", "https://www.w3.org/ns/pub-context"], + "type": "Book", + "conformsTo": "https://github.com/vivliostyle/vivliostyle-cli", + "name": "In-place cover conversion", + "dateModified": "2024-11-18T01:25:52.832Z", + "readingOrder": [ + { + "url": "inplace/.vs-1731893152825.cover.html", + "name": "in-place cover", + "rel": "cover", + "type": "LinkedResource" + } + ], + "resources": [ + { + "rel": "cover", + "url": "arch.jpg", + "name": "Cover image", + "encodingFormat": "image/jpeg" + } + ], + "links": [] +} diff --git a/tests/fixtures/toc/publication.json b/tests/fixtures/toc/publication.json new file mode 100644 index 00000000..0ebda3aa --- /dev/null +++ b/tests/fixtures/toc/publication.json @@ -0,0 +1,17 @@ +{ + "@context": ["https://schema.org", "https://www.w3.org/ns/pub-context"], + "type": "Book", + "conformsTo": "https://github.com/vivliostyle/vivliostyle-cli", + "name": "In-place toc conversion", + "dateModified": "2024-11-18T01:25:52.958Z", + "readingOrder": [ + { + "url": "inplace/.vs-1731893152951.index.html", + "name": "Table of Contents", + "rel": "contents", + "type": "LinkedResource" + } + ], + "resources": [], + "links": [] +} diff --git a/yarn.lock b/yarn.lock index c5c8387c..7d735763 100644 --- a/yarn.lock +++ b/yarn.lock @@ -683,6 +683,11 @@ "@pnpm/network.ca-file" "^1.0.1" config-chain "^1.1.11" +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.28" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" + integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== + "@release-it/conventional-changelog@^5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@release-it/conventional-changelog/-/conventional-changelog-5.1.1.tgz#5e3affbe8d1814fe47d89777e3375a8a90c073b5" @@ -898,6 +903,13 @@ resolved "https://registry.yarnpkg.com/@types/command-exists/-/command-exists-1.2.0.tgz#d97e0ed10097090e4ab0367ed425b0312fad86f3" integrity sha512-ugsxEJfsCuqMLSuCD4PIJkp5Uk2z6TCMRCgYVuhRo5cYQY3+1xXTQkSlPtkpGHuvWMjS2KTeVQXxkXRACMbM6A== +"@types/connect@^3.4.38": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + "@types/debug@^4.1.7": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" @@ -1103,6 +1115,11 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== +"@types/picomatch@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-3.0.1.tgz#d2bb932bb24c2f2f97f77c31c3c37ddd60c9a4a5" + integrity sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw== + "@types/readable-stream@^2.3.9": version "2.3.9" resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.9.tgz#40a8349e6ace3afd2dd1b6d8e9b02945de4566a9" @@ -5647,6 +5664,11 @@ mri@^1.2.0: resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== +mrmime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" + integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -7362,6 +7384,15 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +sirv@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.0.tgz#f8d90fc528f65dff04cb597a88609d4e8a4361ce" + integrity sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + slash@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" @@ -7868,6 +7899,11 @@ token-types@^4.1.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + tough-cookie@^4.1.2: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" @@ -8473,6 +8509,17 @@ vite@^5.0.0: optionalDependencies: fsevents "~2.3.3" +vite@^5.4.11: + version "5.4.11" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5" + integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + vitest@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.0.5.tgz#2f15a532704a7181528e399cc5b754c7f335fd62"