Path: blob/main/src/command/preview/preview-shiny.ts
6450 views
/*1* preview-shiny.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { info } from "../../deno_ral/log.ts";78import { dirname, extname, isAbsolute, join } from "../../deno_ral/path.ts";910import { RunOptions } from "../../execute/types.ts";11import { ProjectContext } from "../../project/types.ts";12import { serve } from "../serve/serve.ts";13import {14previewUnableToRenderResponse,15renderToken,16} from "../render/render-shared.ts";17import { HttpFileRequestOptions } from "../../core/http-types.ts";18import {19createChangeHandler,20isPreviewRenderRequest,21isPreviewTerminateRequest,22PreviewRenderRequest,23previewRenderRequest,24previewRenderRequestIsCompatible,25renderForPreview,26RenderForPreviewResult,27} from "./preview.ts";28import { exitWithCleanup, onCleanup } from "../../core/cleanup.ts";29import {30httpContentResponse,31httpFileRequestHandler,32} from "../../core/http.ts";33import { findOpenPort } from "../../core/port.ts";34import { handleHttpRequests } from "../../core/http-server.ts";35import { normalizePath } from "../../core/path.ts";36import { previewMonitorResources } from "../../core/quarto.ts";37import { renderServices } from "../render/render-services.ts";38import { RenderFlags } from "../render/types.ts";39import { notebookContext } from "../../render/notebook/notebook-context.ts";4041export interface PreviewShinyOptions extends RunOptions {42pandocArgs: string[];43watchInputs: boolean;44project: ProjectContext;45}4647export async function previewShiny(options: PreviewShinyOptions) {48// monitor dev resources49previewMonitorResources();5051// render for preview52let rendering = false;53const renderingFile = isAbsolute(options.input)54? options.input55: join(Deno.cwd(), options.input);56const render = async (to?: string) => {57to = to || options.format;58const renderFlags: RenderFlags = { to, execute: true };59const services = renderServices(notebookContext());60try {61rendering = true;62const result = await renderForPreview(63options.input,64services,65renderFlags,66options.pandocArgs,67options.project,68);69return result;70} finally {71services.cleanup();72rendering = false;73}74};75const result = await render();7677// watch for changes and re-render / re-load as necessary78const changeHandler = createChangeHandler(79// result to kick off change handling80result,81// render for reload, but provide a reload filter that prevents82// rendering for files that shiny will auto-reload on and on83// the .html file that we generate84{85reloadClients: async () => {86await render();87},88},89// delegate to render90render,91// watch .qmd if requested92options.watchInputs,93// filter files that shiny will reload on94(file: string) => {95const ext = extname(file);96return ![".py", ".html", ".htm"].includes(ext);97},98(files: string[]) => {99if (100files.length === 1 && files[0] === renderingFile &&101extname(files[0]) === ".ipynb"102) {103return rendering;104}105return false;106},107);108109// if a render token was provided then run a control channel to fulfill render requests110if (renderToken()) {111runPreviewControlService(options, changeHandler.render);112}113114// serve w/ reload115return await serve({ ...options, render: false, reload: true });116}117118function runPreviewControlService(119options: PreviewShinyOptions,120renderHandler: (to?: string) => Promise<RenderForPreviewResult | undefined>,121) {122// helper to check whether a render request is compatible123// with the original render124const isCompatibleRequest = async (prevReq: PreviewRenderRequest) => {125return normalizePath(options.input) === normalizePath(prevReq.path) &&126await previewRenderRequestIsCompatible(127prevReq,128options.project,129options.format,130);131};132133const baseDir = dirname(options.input);134135const handlerOptions: HttpFileRequestOptions = {136baseDir,137138onRequest: async (req: Request) => {139if (isPreviewTerminateRequest(req)) {140exitWithCleanup(0);141} else if (isPreviewRenderRequest(req)) {142const prevReq = previewRenderRequest(req, true, baseDir);143if (prevReq && await isCompatibleRequest(prevReq)) {144renderHandler();145return httpContentResponse("rendered");146} else {147return previewUnableToRenderResponse();148}149} else {150return undefined;151}152},153};154155const handler = httpFileRequestHandler(handlerOptions);156157const port = findOpenPort();158159onCleanup(handleHttpRequests({ ...handlerOptions, handler }).stop);160info(`Preview service running (${port})`);161}162163164