Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/preview/preview-shiny.ts
3562 views
1
/*
2
* preview-shiny.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { info } from "../../deno_ral/log.ts";
8
9
import { dirname, extname, isAbsolute, join } from "../../deno_ral/path.ts";
10
11
import { RunOptions } from "../../execute/types.ts";
12
import { ProjectContext } from "../../project/types.ts";
13
import { serve } from "../serve/serve.ts";
14
import {
15
previewUnableToRenderResponse,
16
renderToken,
17
} from "../render/render-shared.ts";
18
import { HttpFileRequestOptions } from "../../core/http-types.ts";
19
import {
20
createChangeHandler,
21
isPreviewRenderRequest,
22
isPreviewTerminateRequest,
23
PreviewRenderRequest,
24
previewRenderRequest,
25
previewRenderRequestIsCompatible,
26
renderForPreview,
27
RenderForPreviewResult,
28
} from "./preview.ts";
29
import { exitWithCleanup, onCleanup } from "../../core/cleanup.ts";
30
import {
31
httpContentResponse,
32
httpFileRequestHandler,
33
} from "../../core/http.ts";
34
import { findOpenPort } from "../../core/port.ts";
35
import { handleHttpRequests } from "../../core/http-server.ts";
36
import { kLocalhost } from "../../core/port-consts.ts";
37
import { normalizePath } from "../../core/path.ts";
38
import { previewMonitorResources } from "../../core/quarto.ts";
39
import { renderServices } from "../render/render-services.ts";
40
import { RenderFlags } from "../render/types.ts";
41
import { notebookContext } from "../../render/notebook/notebook-context.ts";
42
import { isIpynbOutput } from "../../config/format.ts";
43
44
export interface PreviewShinyOptions extends RunOptions {
45
pandocArgs: string[];
46
watchInputs: boolean;
47
project?: ProjectContext;
48
}
49
50
export async function previewShiny(options: PreviewShinyOptions) {
51
// monitor dev resources
52
previewMonitorResources();
53
54
// render for preview
55
let rendering = false;
56
const renderingFile = isAbsolute(options.input)
57
? options.input
58
: join(Deno.cwd(), options.input);
59
const render = async (to?: string) => {
60
to = to || options.format;
61
const renderFlags: RenderFlags = { to, execute: true };
62
const services = renderServices(notebookContext());
63
try {
64
rendering = true;
65
const result = await renderForPreview(
66
options.input,
67
services,
68
renderFlags,
69
options.pandocArgs,
70
options.project,
71
);
72
return result;
73
} finally {
74
services.cleanup();
75
rendering = false;
76
}
77
};
78
const result = await render();
79
80
// watch for changes and re-render / re-load as necessary
81
const changeHandler = createChangeHandler(
82
// result to kick off change handling
83
result,
84
// render for reload, but provide a reload filter that prevents
85
// rendering for files that shiny will auto-reload on and on
86
// the .html file that we generate
87
{
88
reloadClients: async () => {
89
await render();
90
},
91
},
92
// delegate to render
93
render,
94
// watch .qmd if requested
95
options.watchInputs,
96
// filter files that shiny will reload on
97
(file: string) => {
98
const ext = extname(file);
99
return ![".py", ".html", ".htm"].includes(ext);
100
},
101
(files: string[]) => {
102
if (
103
files.length === 1 && files[0] === renderingFile &&
104
extname(files[0]) === ".ipynb"
105
) {
106
return rendering;
107
}
108
return false;
109
},
110
);
111
112
// if a render token was provided then run a control channel to fulfill render requests
113
if (renderToken()) {
114
runPreviewControlService(options, changeHandler.render);
115
}
116
117
// serve w/ reload
118
return await serve({ ...options, render: false, reload: true });
119
}
120
121
function runPreviewControlService(
122
options: PreviewShinyOptions,
123
renderHandler: (to?: string) => Promise<RenderForPreviewResult | undefined>,
124
) {
125
// helper to check whether a render request is compatible
126
// with the original render
127
const isCompatibleRequest = async (prevReq: PreviewRenderRequest) => {
128
return normalizePath(options.input) === normalizePath(prevReq.path) &&
129
await previewRenderRequestIsCompatible(
130
prevReq,
131
options.format,
132
options.project,
133
);
134
};
135
136
const baseDir = dirname(options.input);
137
138
const handlerOptions: HttpFileRequestOptions = {
139
baseDir,
140
141
onRequest: async (req: Request) => {
142
if (isPreviewTerminateRequest(req)) {
143
exitWithCleanup(0);
144
} else if (isPreviewRenderRequest(req)) {
145
const prevReq = previewRenderRequest(req, true, baseDir);
146
if (prevReq && await isCompatibleRequest(prevReq)) {
147
renderHandler();
148
return httpContentResponse("rendered");
149
} else {
150
return previewUnableToRenderResponse();
151
}
152
} else {
153
return undefined;
154
}
155
},
156
};
157
158
const handler = httpFileRequestHandler(handlerOptions);
159
160
const port = findOpenPort();
161
162
onCleanup(handleHttpRequests({ ...handlerOptions, handler }).stop);
163
info(`Preview service running (${port})`);
164
}
165
166