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