Path: blob/main/src/command/preview/preview-shiny.ts
3562 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 { kLocalhost } from "../../core/port-consts.ts";36import { normalizePath } from "../../core/path.ts";37import { previewMonitorResources } from "../../core/quarto.ts";38import { renderServices } from "../render/render-services.ts";39import { RenderFlags } from "../render/types.ts";40import { notebookContext } from "../../render/notebook/notebook-context.ts";41import { isIpynbOutput } from "../../config/format.ts";4243export interface PreviewShinyOptions extends RunOptions {44pandocArgs: string[];45watchInputs: boolean;46project?: ProjectContext;47}4849export async function previewShiny(options: PreviewShinyOptions) {50// monitor dev resources51previewMonitorResources();5253// render for preview54let rendering = false;55const renderingFile = isAbsolute(options.input)56? options.input57: join(Deno.cwd(), options.input);58const render = async (to?: string) => {59to = to || options.format;60const renderFlags: RenderFlags = { to, execute: true };61const services = renderServices(notebookContext());62try {63rendering = true;64const result = await renderForPreview(65options.input,66services,67renderFlags,68options.pandocArgs,69options.project,70);71return result;72} finally {73services.cleanup();74rendering = false;75}76};77const result = await render();7879// watch for changes and re-render / re-load as necessary80const changeHandler = createChangeHandler(81// result to kick off change handling82result,83// render for reload, but provide a reload filter that prevents84// rendering for files that shiny will auto-reload on and on85// the .html file that we generate86{87reloadClients: async () => {88await render();89},90},91// delegate to render92render,93// watch .qmd if requested94options.watchInputs,95// filter files that shiny will reload on96(file: string) => {97const ext = extname(file);98return ![".py", ".html", ".htm"].includes(ext);99},100(files: string[]) => {101if (102files.length === 1 && files[0] === renderingFile &&103extname(files[0]) === ".ipynb"104) {105return rendering;106}107return false;108},109);110111// if a render token was provided then run a control channel to fulfill render requests112if (renderToken()) {113runPreviewControlService(options, changeHandler.render);114}115116// serve w/ reload117return await serve({ ...options, render: false, reload: true });118}119120function runPreviewControlService(121options: PreviewShinyOptions,122renderHandler: (to?: string) => Promise<RenderForPreviewResult | undefined>,123) {124// helper to check whether a render request is compatible125// with the original render126const isCompatibleRequest = async (prevReq: PreviewRenderRequest) => {127return normalizePath(options.input) === normalizePath(prevReq.path) &&128await previewRenderRequestIsCompatible(129prevReq,130options.format,131options.project,132);133};134135const baseDir = dirname(options.input);136137const handlerOptions: HttpFileRequestOptions = {138baseDir,139140onRequest: async (req: Request) => {141if (isPreviewTerminateRequest(req)) {142exitWithCleanup(0);143} else if (isPreviewRenderRequest(req)) {144const prevReq = previewRenderRequest(req, true, baseDir);145if (prevReq && await isCompatibleRequest(prevReq)) {146renderHandler();147return httpContentResponse("rendered");148} else {149return previewUnableToRenderResponse();150}151} else {152return undefined;153}154},155};156157const handler = httpFileRequestHandler(handlerOptions);158159const port = findOpenPort();160161onCleanup(handleHttpRequests({ ...handlerOptions, handler }).stop);162info(`Preview service running (${port})`);163}164165166