Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/preview/cmd.ts
3562 views
1
/*
2
* cmd.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { existsSync } from "../../deno_ral/fs.ts";
8
import { dirname, extname, join, relative } from "../../deno_ral/path.ts";
9
10
import * as colors from "fmt/colors";
11
12
import { Command } from "cliffy/command/mod.ts";
13
14
import { kLocalhost } from "../../core/port-consts.ts";
15
import { waitForPort } from "../../core/port.ts";
16
import { fixupPandocArgs, parseRenderFlags } from "../render/flags.ts";
17
import {
18
handleRenderResult,
19
preview,
20
previewFormat,
21
setPreviewFormat,
22
} from "./preview.ts";
23
import {
24
kRenderDefault,
25
kRenderNone,
26
serveProject,
27
} from "../../project/serve/serve.ts";
28
29
import {
30
initState,
31
setInitializer,
32
} from "../../core/lib/yaml-validation/state.ts";
33
import { initYamlIntelligenceResourcesFromFilesystem } from "../../core/schema/utils.ts";
34
import { kProjectWatchInputs, ProjectContext } from "../../project/types.ts";
35
import { projectContext } from "../../project/project-context.ts";
36
import {
37
projectIsServeable,
38
projectPreviewServe,
39
} from "../../project/project-shared.ts";
40
41
import { isHtmlOutput } from "../../config/format.ts";
42
import { renderProject } from "../render/project.ts";
43
import { renderServices } from "../render/render-services.ts";
44
import { parseFormatString } from "../../core/pandoc/pandoc-formats.ts";
45
import { normalizePath } from "../../core/path.ts";
46
import { kCliffyImplicitCwd } from "../../config/constants.ts";
47
import { warning } from "../../deno_ral/log.ts";
48
import { renderFormats } from "../render/render-contexts.ts";
49
import { Format } from "../../config/types.ts";
50
import { isServerShiny, isServerShinyPython } from "../../core/render.ts";
51
import { previewShiny } from "./preview-shiny.ts";
52
import { serve } from "../serve/serve.ts";
53
import { fileExecutionEngine } from "../../execute/engine.ts";
54
import { notebookContext } from "../../render/notebook/notebook-context.ts";
55
import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts";
56
import { exitWithCleanup } from "../../core/cleanup.ts";
57
58
export const previewCommand = new Command()
59
.name("preview")
60
.stopEarly()
61
.option(
62
"--port [port:number]",
63
"Suggested port to listen on (defaults to random value between 3000 and 8000).\n" +
64
"If the port is not available then a random port between 3000 and 8000 will be selected.",
65
)
66
.option(
67
"--host [host:string]",
68
"Hostname to bind to (defaults to 127.0.0.1)",
69
)
70
.option(
71
"--render [format:string]",
72
"Render to the specified format(s) before previewing",
73
{
74
default: kRenderNone,
75
},
76
)
77
.option(
78
"--no-serve",
79
"Don't run a local preview web server (just monitor and re-render input files)",
80
)
81
.option(
82
"--no-navigate",
83
"Don't navigate the browser automatically when outputs are updated.",
84
)
85
.option(
86
"--no-browser",
87
"Don't open a browser to preview the site.",
88
)
89
.option(
90
"--no-watch-inputs",
91
"Do not re-render input files when they change.",
92
)
93
.option(
94
"--timeout",
95
"Time (in seconds) after which to exit if there are no active clients.",
96
)
97
.arguments("[file:string] [...args:string]")
98
.description(
99
"Render and preview a document or website project.\n\nAutomatically reloads the browser when " +
100
"input files or document resources (e.g. CSS) change.\n\n" +
101
"For website preview, the most recent execution results of computational documents are used to render\n" +
102
"the site (this is to optimize startup time). If you want to perform a full render prior to\n" +
103
'previewing pass the --render option with "all" or a comma-separated list of formats to render.\n\n' +
104
"For document preview, input file changes will result in a re-render (pass --no-watch to prevent).\n\n" +
105
"You can also include arbitrary command line arguments to be forwarded to " +
106
colors.bold("quarto render") + ".",
107
)
108
.example(
109
"Preview document",
110
"quarto preview doc.qmd",
111
)
112
.example(
113
"Preview document with render command line args",
114
"quarto preview doc.qmd --toc",
115
)
116
.example(
117
"Preview document (don't watch for input changes)",
118
"quarto preview doc.qmd --no-watch-inputs",
119
)
120
.example(
121
"Preview website with most recent execution results",
122
"quarto preview",
123
)
124
.example(
125
"Previewing website using a specific port",
126
"quarto preview --port 4444",
127
)
128
.example(
129
"Preview website (don't open a browser)",
130
"quarto preview --no-browser",
131
)
132
.example(
133
"Fully render all website/book formats then preview",
134
"quarto preview --render all",
135
)
136
.example(
137
"Fully render the html format then preview",
138
"quarto preview --render html",
139
)
140
// deno-lint-ignore no-explicit-any
141
.action(async (options: any, file?: string, ...args: string[]) => {
142
// one-time initialization of yaml validation modules
143
setInitializer(initYamlIntelligenceResourcesFromFilesystem);
144
await initState();
145
146
// if input is missing but there exists an args parameter which is a .qmd or .ipynb file,
147
// issue a warning.
148
if (!file || file === kCliffyImplicitCwd) {
149
file = Deno.cwd();
150
const firstArg = args.find((arg) =>
151
arg.endsWith(".qmd") || arg.endsWith(".ipynb")
152
);
153
if (firstArg) {
154
warning(
155
"`quarto preview` invoked with no input file specified (the parameter order matters).\nQuarto will preview the current directory by default.\n" +
156
`Did you mean to run \`quarto preview ${firstArg} ${
157
args.filter((arg) => arg !== firstArg).join(" ")
158
}\`?\n` +
159
"Use `quarto preview --help` for more information.",
160
);
161
}
162
}
163
164
file = file || Deno.cwd();
165
if (!existsSync(file)) {
166
throw new Error(`${file} not found`);
167
}
168
169
// show help if requested
170
if (args.length > 0 && args[0] === "--help") {
171
previewCommand.showHelp();
172
return;
173
}
174
175
// pull out our command line args
176
const portPos = args.indexOf("--port");
177
if (portPos !== -1) {
178
options.port = parseInt(args[portPos + 1]);
179
args.splice(portPos, 2);
180
}
181
const hostPos = args.indexOf("--host");
182
if (hostPos !== -1) {
183
options.host = String(args[hostPos + 1]);
184
args.splice(hostPos, 2);
185
}
186
const renderPos = args.indexOf("--render");
187
if (renderPos !== -1) {
188
options.render = String(args[renderPos + 1]);
189
args.splice(renderPos, 2);
190
}
191
const presentationPos = args.indexOf("--presentation");
192
if (presentationPos !== -1) {
193
options.presentation = true;
194
args.splice(presentationPos, 1);
195
} else {
196
options.presentation = false;
197
}
198
const browserPathPos = args.indexOf("--browser-path");
199
if (browserPathPos !== -1) {
200
options.browserPath = String(args[browserPathPos + 1]);
201
args.splice(browserPathPos, 2);
202
}
203
const noServePos = args.indexOf("--no-serve");
204
if (noServePos !== -1) {
205
options.noServe = true;
206
args.splice(noServePos, 1);
207
}
208
const noBrowsePos = args.indexOf("--no-browse");
209
if (noBrowsePos !== -1) {
210
options.browse = false;
211
args.splice(noBrowsePos, 1);
212
}
213
const noBrowserPos = args.indexOf("--no-browser");
214
if (noBrowserPos !== -1) {
215
options.browser = false;
216
args.splice(noBrowserPos, 1);
217
}
218
const navigatePos = args.indexOf("--navigate");
219
if (navigatePos !== -1) {
220
options.navigate = true;
221
args.splice(navigatePos, 1);
222
}
223
const noNavigatePos = args.indexOf("--no-navigate");
224
if (noNavigatePos !== -1) {
225
options.navigate = false;
226
args.splice(noNavigatePos, 1);
227
}
228
const watchInputsPos = args.indexOf("--watch-inputs");
229
if (watchInputsPos !== -1) {
230
options.watchInputs = true;
231
args.splice(watchInputsPos, 1);
232
}
233
const noWatchInputsPos = args.indexOf("--no-watch-inputs");
234
if (noWatchInputsPos !== -1) {
235
options.watchInputs = false;
236
args.splice(noWatchInputsPos, 1);
237
}
238
const timeoutPos = args.indexOf("--timeout");
239
if (timeoutPos !== -1) {
240
options.timeout = parseInt(args[timeoutPos + 1]);
241
args.splice(timeoutPos, 2);
242
}
243
244
// alias for --no-watch-inputs (used by older versions of quarto r package)
245
const noWatchPos = args.indexOf("--no-watch");
246
if (noWatchPos !== -1) {
247
options.watchInputs = false;
248
args.splice(noWatchPos, 1);
249
}
250
// alias for --no-watch-inputs (used by older versions of rstudio)
251
const noRenderPos = args.indexOf("--no-render");
252
if (noRenderPos !== -1) {
253
options.watchInputs = false;
254
args.splice(noRenderPos, 1);
255
}
256
257
if (options.port) {
258
// try to bind to requested port (error if its in use)
259
const port = parseInt(options.port);
260
if (await waitForPort({ port, hostname: kLocalhost })) {
261
options.port = port;
262
} else {
263
throw new Error(`Requested port ${options.port} is already in use.`);
264
}
265
}
266
267
// extract pandoc flag values we know/care about, then fixup args as
268
// necessary (remove our flags that pandoc doesn't know about)
269
const flags = await parseRenderFlags(args);
270
args = fixupPandocArgs(args, flags);
271
272
// if this is a single-file preview within a 'serveable' project
273
// without a specific render directive then render the file
274
// and convert the render to a project one
275
let touchPath: string | undefined;
276
let projectTarget: string | ProjectContext = file;
277
if (Deno.statSync(file).isFile) {
278
// get project and preview format
279
const nbContext = notebookContext();
280
const project = (await projectContext(dirname(file), nbContext)) ||
281
(await singleFileProjectContext(file, nbContext));
282
const formats = await (async () => {
283
const services = renderServices(nbContext);
284
try {
285
return await renderFormats(
286
file!,
287
services,
288
undefined,
289
project,
290
);
291
} finally {
292
services.cleanup();
293
}
294
})();
295
const format = await previewFormat(file, flags.to, formats, project);
296
297
// see if this is server: shiny document and if it is then forward to previewShiny
298
if (isHtmlOutput(parseFormatString(format).baseFormat)) {
299
const renderFormat = formats[format] as Format | undefined;
300
if (renderFormat && isServerShiny(renderFormat)) {
301
const engine = await fileExecutionEngine(file, flags, project);
302
setPreviewFormat(format, flags, args);
303
if (isServerShinyPython(renderFormat, engine?.name)) {
304
const result = await previewShiny({
305
input: file,
306
render: !!options.render,
307
port: typeof (options.port) === "string"
308
? parseInt(options.port)
309
: options.port,
310
host: options.host,
311
browser: options.browser,
312
projectDir: project?.dir,
313
tempDir: Deno.makeTempDirSync(),
314
format,
315
pandocArgs: args,
316
watchInputs: options.watchInputs!,
317
});
318
exitWithCleanup(result.code);
319
throw new Error(); // unreachable
320
} else {
321
const result = await serve({
322
input: file,
323
render: !!options.render,
324
port: typeof (options.port) === "string"
325
? parseInt(options.port)
326
: options.port,
327
host: options.host,
328
format: format,
329
browser: options.browser,
330
projectDir: project?.dir,
331
tempDir: Deno.makeTempDirSync(),
332
});
333
exitWithCleanup(result.code);
334
throw new Error(); // unreachable
335
}
336
}
337
}
338
339
if (project && projectIsServeable(project)) {
340
// special case: plain markdown file w/ an external previewer that is NOT
341
// in the project input list -- in this case allow things to proceed
342
// without a render
343
const filePath = normalizePath(file);
344
if (!project.files.input.includes(filePath)) {
345
if (extname(file) === ".md" && projectPreviewServe(project)) {
346
setPreviewFormat(format, flags, args);
347
touchPath = filePath;
348
options.browserPath = "";
349
file = project.dir;
350
projectTarget = project;
351
}
352
} else {
353
if (
354
isHtmlOutput(parseFormatString(format).baseFormat, true) ||
355
projectPreviewServe(project)
356
) {
357
setPreviewFormat(format, flags, args);
358
const services = renderServices(notebookContext());
359
try {
360
const renderResult = await renderProject(project, {
361
services,
362
progress: false,
363
useFreezer: false,
364
flags,
365
pandocArgs: args,
366
previewServer: true,
367
}, [file]);
368
if (renderResult.error) {
369
throw renderResult.error;
370
}
371
handleRenderResult(file, renderResult);
372
if (projectPreviewServe(project) && renderResult.baseDir) {
373
touchPath = join(
374
renderResult.baseDir,
375
renderResult.files[0].file,
376
);
377
}
378
} finally {
379
services.cleanup();
380
}
381
// re-write various targets to redirect to project preview
382
if (projectPreviewServe(project)) {
383
options.browserPath = "";
384
} else {
385
options.browserPath = relative(project.dir, file);
386
}
387
file = project.dir;
388
projectTarget = project;
389
}
390
}
391
}
392
}
393
394
// see if we are serving a project or a file
395
if (Deno.statSync(file).isDirectory) {
396
// project preview
397
const renderOptions = {
398
services: renderServices(notebookContext()),
399
flags,
400
};
401
await serveProject(projectTarget, renderOptions, args, {
402
port: options.port,
403
host: options.host,
404
browser: (options.browser === false || options.browse === false)
405
? false
406
: undefined,
407
[kProjectWatchInputs]: options.watchInputs,
408
timeout: options.timeout,
409
render: options.render,
410
touchPath,
411
browserPath: options.browserPath,
412
navigate: options.navigate,
413
}, options.noServe === true);
414
} else {
415
// single file preview
416
if (
417
options.render !== kRenderNone &&
418
options.render !== kRenderDefault &&
419
args.indexOf("--to") === -1
420
) {
421
args.push("--to", options.render);
422
}
423
424
await preview(relative(Deno.cwd(), file), flags, args, {
425
port: options.port,
426
host: options.host,
427
browser: (options.browser === false || options.browse === false)
428
? false
429
: undefined,
430
[kProjectWatchInputs]: options.watchInputs,
431
timeout: options.timeout,
432
presentation: options.presentation,
433
});
434
}
435
});
436
437