Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/project/serve/serve.ts
6456 views
1
/*
2
* serve.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { info, warning } from "../../deno_ral/log.ts";
8
import { existsSync, safeRemoveSync } from "../../deno_ral/fs.ts";
9
import {
10
basename,
11
dirname,
12
extname,
13
join,
14
relative,
15
} from "../../deno_ral/path.ts";
16
import * as colors from "fmt/colors";
17
18
import * as ld from "../../core/lodash.ts";
19
20
import { DOMParser, initDenoDom } from "../../core/deno-dom.ts";
21
22
import { openUrl } from "../../core/shell.ts";
23
import {
24
contentType,
25
isDocxContent,
26
isHtmlContent,
27
isPdfContent,
28
isTextContent,
29
} from "../../core/mime.ts";
30
import { dirAndStem, isModifiedAfter } from "../../core/path.ts";
31
import { logError } from "../../core/log.ts";
32
33
import {
34
kProject404File,
35
kProjectType,
36
ProjectContext,
37
} from "../../project/types.ts";
38
import { resolvePreviewOptions } from "../../command/preview/preview.ts";
39
40
import {
41
isProjectInputFile,
42
projectExcludeDirs,
43
projectOutputDir,
44
projectPreviewServe,
45
} from "../../project/project-shared.ts";
46
import { projectContext } from "../../project/project-context.ts";
47
import { partitionedMarkdownForInput } from "../../project/project-config.ts";
48
49
import {
50
clearProjectIndex,
51
inputFileForOutputFile,
52
resolveInputTarget,
53
} from "../../project/project-index.ts";
54
55
import { websitePath } from "../../project/types/website/website-config.ts";
56
57
import { renderProject } from "../../command/render/project.ts";
58
import {
59
renderResultFinalOutput,
60
renderResultUrlPath,
61
} from "../../command/render/render.ts";
62
63
import {
64
httpContentResponse,
65
httpFileRequestHandler,
66
isBrowserPreviewable,
67
} from "../../core/http.ts";
68
import { HttpFileRequestOptions } from "../../core/http-types.ts";
69
import { ProjectWatcher, ServeOptions } from "./types.ts";
70
import { watchProject } from "./watch.ts";
71
import {
72
isPreviewRenderRequest,
73
isPreviewTerminateRequest,
74
previewRenderRequest,
75
previewRenderRequestIsCompatible,
76
} from "../../command/preview/preview.ts";
77
import {
78
previewUnableToRenderResponse,
79
printWatchingForChangesMessage,
80
render,
81
renderToken,
82
} from "../../command/render/render-shared.ts";
83
import { renderServices } from "../../command/render/render-services.ts";
84
import { renderProgress } from "../../command/render/render-info.ts";
85
import { resourceFilesFromFile } from "../../command/render/resources.ts";
86
import { projectType } from "../../project/types/project-types.ts";
87
import { htmlResourceResolverPostprocessor } from "../../project/types/website/website-resources.ts";
88
import { inputFilesDir } from "../../core/render.ts";
89
import { kResources, kTargetFormat } from "../../config/constants.ts";
90
import { resourcesFromMetadata } from "../../command/render/resources.ts";
91
import {
92
RenderFlags,
93
RenderOptions,
94
RenderResult,
95
} from "../../command/render/types.ts";
96
import {
97
kPdfJsInitialPath,
98
pdfJsBaseDir,
99
pdfJsFileHandler,
100
} from "../../core/pdfjs.ts";
101
import { isPdfOutput } from "../../config/format.ts";
102
import { bookOutputStem } from "../../project/types/book/book-shared.ts";
103
import { removePandocToArg } from "../../command/render/flags.ts";
104
import { isRStudioServer, isServerSession } from "../../core/platform.ts";
105
import { ServeRenderManager } from "./render.ts";
106
import { projectScratchPath } from "../project-scratch.ts";
107
import { previewMonitorResources } from "../../core/quarto.ts";
108
import { exitWithCleanup, onCleanup } from "../../core/cleanup.ts";
109
import { projectExtensionDirs } from "../../extension/extension.ts";
110
import { findOpenPort } from "../../core/port.ts";
111
import { kLocalhost } from "../../core/port-consts.ts";
112
import { ProjectServe } from "../../resources/types/zod/schema-types.ts";
113
import { handleHttpRequests } from "../../core/http-server.ts";
114
import { touch } from "../../core/file.ts";
115
import { staticResource } from "../../preview/preview-static.ts";
116
import { previewTextContent } from "../../preview/preview-text.ts";
117
import { kManuscriptType } from "../types/manuscript/manuscript-types.ts";
118
import {
119
previewURL,
120
printBrowsePreviewMessage,
121
} from "../../core/previewurl.ts";
122
import {
123
noPreviewServer,
124
PreviewServer,
125
runExternalPreviewServer,
126
} from "../../preview/preview-server.ts";
127
import { notebookContext } from "../../render/notebook/notebook-context.ts";
128
import { isWindows } from "../../deno_ral/platform.ts";
129
130
export const kRenderNone = "none";
131
export const kRenderDefault = "default";
132
133
export async function serveProject(
134
target: string | ProjectContext,
135
renderOptions: RenderOptions,
136
pandocArgs: string[],
137
options: ServeOptions,
138
noServe: boolean,
139
) {
140
let project: ProjectContext | undefined;
141
let flags = renderOptions.flags;
142
const nbContext = renderOptions.services.notebook;
143
if (typeof target === "string") {
144
if (target === ".") {
145
target = Deno.cwd();
146
}
147
project = await projectContext(
148
target,
149
nbContext,
150
renderOptions,
151
);
152
if (!project || !project?.config) {
153
throw new Error(`${target} is not a project`);
154
}
155
156
const isMdFormat = (
157
mdFormat: string,
158
format?: string | Record<string, unknown> | unknown,
159
) => {
160
if (!format) {
161
return false;
162
}
163
164
if (typeof format === "string") {
165
return format === mdFormat;
166
} else if (typeof format === "object") {
167
const formats = Object.keys(format);
168
if (formats.length > 0) {
169
const firstFormat = Object.keys(format)[0];
170
return firstFormat === mdFormat;
171
} else {
172
return false;
173
}
174
} else {
175
return false;
176
}
177
};
178
179
// Default project types can't be served
180
const projType = projectType(project?.config?.project?.[kProjectType]);
181
if (
182
projType.type === "default" &&
183
!isMdFormat("docusaurus-md", project?.config?.format) &&
184
!isMdFormat("hugo-md", project?.config?.format)
185
) {
186
const hasIndex = project.files.input.some((file) => {
187
let relPath = file;
188
if (project) {
189
relPath = relative(project.dir, file);
190
}
191
const [dir, stem] = dirAndStem(relPath);
192
return dir === "." && stem === "index";
193
});
194
195
if (!hasIndex && options.browser !== false) {
196
throw new Error(
197
`The project '${
198
project.config.project.title || ""
199
}' is a default type project which doesn't support project wide previewing unless there is an 'index' file present.\n\nPlease preview an individual file within this project instead.`,
200
);
201
}
202
}
203
} else {
204
project = target;
205
}
206
207
// acquire the preview lock
208
acquirePreviewLock(project);
209
210
// monitor the src dir
211
previewMonitorResources();
212
213
// clear the project index
214
clearProjectIndex(project.dir);
215
216
// set QUARTO_PROJECT_DIR
217
Deno.env.set("QUARTO_PROJECT_DIR", project.dir);
218
219
// resolve options
220
options = {
221
...options,
222
...(await resolvePreviewOptions(options, project)),
223
};
224
225
// are we rendering?
226
const renderBefore = options.render !== kRenderNone;
227
if (renderBefore) {
228
renderProgress("Rendering:");
229
} else {
230
renderProgress("Preparing to preview");
231
}
232
233
// get 'to' from --render
234
flags = {
235
...flags,
236
...(renderBefore && options.render !== kRenderDefault)
237
? { to: options.render }
238
: {},
239
};
240
241
// if there is no flags 'to' then set 'to' to the default format
242
if (flags.to === undefined) {
243
flags.to = kRenderDefault;
244
}
245
246
// are we targeting pdf output?
247
const pdfOutput = isPdfOutput(flags.to || "");
248
249
// Configure render services
250
const services = renderServices(nbContext);
251
252
// determines files to render and resourceFiles to monitor
253
// if we are in render 'none' mode then only render files whose output
254
// isn't up to date. for those files we aren't rendering, compute their
255
// resource files so we can watch them for changes
256
let files: string[] | undefined;
257
let resourceFiles: string[] = [];
258
if (!renderBefore) {
259
// if this is pdf output then we need to render all of the files
260
// so that the latex compiler can build the entire book
261
if (pdfOutput) {
262
files = project.files.input;
263
} else {
264
const srvFiles = await serveFiles(project);
265
files = srvFiles.files;
266
resourceFiles = srvFiles.resourceFiles;
267
}
268
}
269
270
let renderResult;
271
try {
272
renderResult = await renderProject(
273
project,
274
{
275
services,
276
progress: true,
277
useFreezer: !renderBefore,
278
flags,
279
pandocArgs,
280
previewServer: true,
281
},
282
files,
283
);
284
} finally {
285
services.cleanup();
286
}
287
288
// exit if there was an error
289
if (renderResult.error) {
290
throw renderResult.error;
291
}
292
293
// append resource files from render results
294
resourceFiles.push(...ld.uniq(
295
renderResult.files.flatMap((file) => file.resourceFiles),
296
) as string[]);
297
298
// scan for extension dirs
299
const extensionDirs = projectExtensionDirs(project);
300
301
// render manager for tracking need to re-render outputs
302
// (record any files we just rendered)
303
const renderManager = new ServeRenderManager();
304
renderManager.onRenderResult(
305
renderResult,
306
extensionDirs,
307
resourceFiles,
308
project,
309
);
310
311
// stop server function (will be reset if there is a serve action)
312
let stopServer = () => {};
313
314
// create project watcher. later we'll figure out if it should provide renderOutput
315
const watcher = await watchProject(
316
project,
317
extensionDirs,
318
resourceFiles,
319
{ ...renderOptions, flags },
320
pandocArgs,
321
options,
322
!pdfOutput, // we don't render on reload for pdf output
323
renderManager,
324
stopServer,
325
);
326
327
// print status
328
printWatchingForChangesMessage();
329
330
// are we serving? are we using a custom serve command?
331
const serve = noServe ? false : projectPreviewServe(project) || true;
332
const previewServer = serve === false
333
? await noPreviewServer()
334
: serve === true
335
? await internalPreviewServer(
336
project,
337
renderResult,
338
renderManager,
339
pdfOutput,
340
watcher,
341
extensionDirs,
342
resourceFiles,
343
flags,
344
pandocArgs,
345
options,
346
)
347
: await externalPreviewServer(
348
project,
349
serve,
350
options,
351
renderManager,
352
watcher,
353
extensionDirs,
354
resourceFiles,
355
flags,
356
pandocArgs,
357
);
358
359
// set stopServer hook
360
stopServer = previewServer.stop;
361
362
// start server (launch browser if a path is returned)
363
const path = await previewServer.start();
364
365
if (path !== undefined) {
366
printBrowsePreviewMessage(
367
options.host!,
368
options.port!,
369
path,
370
);
371
372
if (options.browser && !isServerSession()) {
373
await openUrl(previewURL(options.host!, options.port!, path));
374
}
375
}
376
377
// register the stopServer function as a cleanup handler
378
onCleanup(stopServer);
379
380
// if there is a touchPath then touch
381
if (options.touchPath) {
382
await touch(options.touchPath);
383
}
384
385
// run the server
386
await previewServer.serve();
387
}
388
389
function externalPreviewServer(
390
project: ProjectContext,
391
serve: ProjectServe,
392
options: ServeOptions,
393
renderManager: ServeRenderManager,
394
watcher: ProjectWatcher,
395
extensionDirs: string[],
396
resourceFiles: string[],
397
flags: RenderFlags,
398
pandocArgs: string[],
399
): Promise<PreviewServer> {
400
// run a control channel server for handling render requests
401
// if there was a renderToken() passed
402
let stop: () => void | undefined;
403
if (renderToken()) {
404
const outputDir = projectOutputDir(project);
405
const handlerOptions: HttpFileRequestOptions = {
406
// base dir
407
baseDir: outputDir,
408
409
// handle websocket upgrade and render requests
410
onRequest: previewControlChannelRequestHandler(
411
project,
412
renderManager,
413
watcher,
414
extensionDirs,
415
resourceFiles,
416
flags,
417
pandocArgs,
418
false,
419
),
420
};
421
422
const handler = httpFileRequestHandler(handlerOptions);
423
const port = findOpenPort();
424
({ stop } = handleHttpRequests({
425
port,
426
hostname: kLocalhost,
427
handler,
428
}));
429
// .abortController;
430
info(`Preview service running (${port})`);
431
}
432
433
// parse command line args and interpolate host and port
434
const cmd = serve.cmd.split(/[\t ]/).map((arg, index) => {
435
if (isWindows && index === 0 && arg === "npm") {
436
return "npm.cmd";
437
} else if (arg === "{host}") {
438
return options.host || kLocalhost;
439
} else if (arg === "{port}") {
440
return String(options.port);
441
} else {
442
return arg;
443
}
444
});
445
// add custom args
446
if (serve.args) {
447
cmd.push(...serve.args);
448
}
449
450
const readyPattern = new RegExp(serve.ready);
451
const server = runExternalPreviewServer({
452
cmd,
453
readyPattern,
454
env: serve.env as { [key: string]: string },
455
cwd: projectOutputDir(project),
456
});
457
458
return Promise.resolve({
459
start: async () => {
460
return server.start();
461
},
462
serve: async () => {
463
return server.serve();
464
},
465
stop: () => {
466
stop?.();
467
return server.stop();
468
},
469
});
470
}
471
472
async function internalPreviewServer(
473
project: ProjectContext,
474
renderResult: RenderResult,
475
renderManager: ServeRenderManager,
476
pdfOutput: boolean,
477
watcher: ProjectWatcher,
478
extensionDirs: string[],
479
resourceFiles: string[],
480
flags: RenderFlags,
481
pandocArgs: string[],
482
options: ServeOptions,
483
): Promise<PreviewServer> {
484
const projType = projectType(project?.config?.project?.[kProjectType]);
485
486
const outputDir = projectOutputDir(project);
487
488
const finalOutput = renderResultFinalOutput(renderResult);
489
490
// function that can return the current target pdf output file
491
const pdfOutputFile = (finalOutput && pdfOutput)
492
? (): string => {
493
const project = watcher.project();
494
if (projType.type == kManuscriptType) {
495
// For manuscripts, just use the final output as is
496
return finalOutput;
497
} else {
498
const outputFile = join(
499
dirname(finalOutput),
500
bookOutputStem(project.dir, project.config) + ".pdf",
501
);
502
return outputFile;
503
}
504
}
505
: undefined;
506
507
const handlerOptions: HttpFileRequestOptions = {
508
// base dir
509
baseDir: outputDir,
510
511
// print all urls
512
printUrls: "all",
513
514
// handle websocket upgrade and render requests
515
onRequest: previewControlChannelRequestHandler(
516
project,
517
renderManager,
518
watcher,
519
extensionDirs,
520
resourceFiles,
521
flags,
522
pandocArgs,
523
isBrowserPreviewable(finalOutput),
524
),
525
526
// handle html file requests w/ re-renders
527
onFile: async (file: string, req: Request) => {
528
// check for static response
529
const baseDir = projectOutputDir(project);
530
531
const staticResponse = await staticResource(baseDir, file);
532
if (staticResponse) {
533
const resolveBody = () => {
534
if (staticResponse.injectClient) {
535
const contents = new TextDecoder().decode(staticResponse.contents);
536
return staticResponse.injectClient(
537
contents,
538
watcher.clientHtml(req),
539
);
540
} else {
541
return staticResponse.contents;
542
}
543
};
544
const body = resolveBody();
545
const response = {
546
body,
547
contentType: staticResponse.contentType,
548
};
549
return response;
550
} else if (
551
isHtmlContent(file) || isPdfContent(file) || isDocxContent(file) ||
552
isTextContent(file)
553
) {
554
// find the input file associated with this output and render it
555
// if we can't find an input file for this .html file it may have
556
// been an input added after the server started running, to catch
557
// this case run a refresh on the watcher then try again
558
const serveDir = projectOutputDir(watcher.project());
559
const filePathRelative = relative(serveDir, file);
560
let inputFile = await inputFileForOutputFile(
561
watcher.project(),
562
filePathRelative,
563
);
564
565
if (!inputFile) {
566
// If we couldn't find a input file, check if this is a resource file.
567
// If so, we can bail out early, since we don't need to render it.
568
// This is great for performance, as refreshing the watcher can be slow.
569
const fileSourcePath = join(watcher.project().dir, filePathRelative);
570
const projectResourceFiles = watcher.project().files.resources;
571
if (projectResourceFiles?.includes(fileSourcePath)) {
572
return undefined;
573
}
574
}
575
576
if (!inputFile || !existsSync(inputFile.file)) {
577
// If we got here, we need to look harder for an input file.
578
inputFile = await inputFileForOutputFile(
579
await watcher.refreshProject(),
580
filePathRelative,
581
);
582
}
583
let result: RenderResult | undefined;
584
let renderError: Error | undefined;
585
if (inputFile) {
586
// render the file if we haven't already done a render for the current input state
587
if (
588
renderManager.fileRequiresReRender(
589
file,
590
inputFile.file,
591
extensionDirs,
592
resourceFiles,
593
watcher.project(),
594
)
595
) {
596
const renderFlags = { ...flags, quiet: true };
597
// remove 'to' argument to allow the file to be rendered in it's default format
598
// (only if we are in a project type e.g. websites that allows multiple formats)
599
const renderPandocArgs = projType.projectFormatsOnly
600
? pandocArgs
601
: removePandocToArg(pandocArgs);
602
if (!projType.projectFormatsOnly) {
603
delete renderFlags?.to;
604
}
605
// if to is 'all' then choose html
606
if (renderFlags?.to == "all") {
607
renderFlags.to = isHtmlContent(file) ? "html" : "pdf";
608
}
609
610
// When previewing, the project type can request that the format that produced
611
// the output that is being requested should always be used to render the file
612
if (projType.incrementalFormatPreviewing) {
613
renderFlags.to = inputFile.format.identifier[kTargetFormat];
614
delete renderFlags?.clean;
615
}
616
617
const services = renderServices(notebookContext());
618
try {
619
result = await renderManager.submitRender(() =>
620
renderProject(
621
watcher.project(),
622
{
623
services,
624
useFreezer: true,
625
devServerReload: true,
626
previewServer: true,
627
flags: renderFlags,
628
pandocArgs: renderPandocArgs,
629
},
630
[inputFile!.file],
631
)
632
);
633
if (result.error) {
634
renderManager.onRenderError(result.error);
635
renderError = result.error;
636
} else {
637
renderManager.onRenderResult(
638
result,
639
extensionDirs,
640
resourceFiles,
641
project!,
642
);
643
}
644
} catch (e) {
645
if (!(e instanceof Error)) throw e;
646
logError(e);
647
renderError = e;
648
} finally {
649
services.cleanup();
650
}
651
}
652
}
653
654
// read the output file
655
const fileContents = renderError
656
? renderErrorPage(renderError)
657
: Deno.readFileSync(file);
658
659
// inject watcher client for html
660
if (isHtmlContent(file) && inputFile) {
661
const projInputFile = join(
662
project!.dir,
663
relative(watcher.project().dir, inputFile.file),
664
);
665
return watcher.injectClient(
666
req,
667
fileContents,
668
projInputFile,
669
);
670
} else if (isTextContent(file) && inputFile) {
671
return previewTextContent(
672
file,
673
inputFile.file,
674
inputFile.format,
675
req,
676
watcher.injectClient,
677
);
678
} else {
679
return { contentType: contentType(file), body: fileContents };
680
}
681
} else {
682
return undefined;
683
}
684
},
685
686
// handle 404 by returing site custom 404 page
687
on404: (url: string, req: Request) => {
688
const print = !basename(url).startsWith("jupyter-");
689
let body = new TextEncoder().encode("Not Found");
690
const custom404 = join(outputDir, kProject404File);
691
if (existsSync(custom404)) {
692
let content404 = Deno.readTextFileSync(custom404);
693
// replace site-path references with / so they work in dev server mode
694
const sitePath = websitePath(project?.config);
695
if (sitePath !== "/" || isRStudioServer()) {
696
// if we are in rstudio server port proxied mode then replace
697
// including the port proxy
698
let replacePath = "/";
699
const referer = req.headers.get("referer");
700
if (isRStudioServer() && referer) {
701
const match = referer.match(/\/p\/.*?\//);
702
if (match) {
703
replacePath = match[0];
704
}
705
}
706
707
content404 = content404.replaceAll(
708
new RegExp('((?:content|ref|src)=")(' + sitePath + ")", "g"),
709
"$1" + replacePath,
710
);
711
}
712
body = new TextEncoder().encode(content404);
713
}
714
return {
715
print,
716
response: watcher.injectClient(req, body),
717
};
718
},
719
};
720
721
// if this is a pdf then we tweak the options to correctly handle pdfjs
722
if (finalOutput && pdfOutput) {
723
// change the baseDir to the pdfjs directory
724
handlerOptions.baseDir = pdfJsBaseDir();
725
726
// install custom handler for pdfjs
727
handlerOptions.onFile = pdfJsFileHandler(
728
pdfOutputFile!,
729
async (file: string, req: Request) => {
730
// inject watcher client for html
731
if (isHtmlContent(file)) {
732
const fileContents = await Deno.readFile(file);
733
return watcher.injectClient(req, fileContents);
734
} else {
735
return undefined;
736
}
737
},
738
);
739
}
740
741
// create the handler
742
const handler = httpFileRequestHandler(handlerOptions);
743
744
// if we are passed a browser path, resolve the output file if its an input
745
let browserPath = options.browserPath
746
? options.browserPath.replace(/^\//, "")
747
: undefined;
748
if (browserPath) {
749
const browserPathTarget = await resolveInputTarget(
750
project,
751
browserPath,
752
false,
753
);
754
if (browserPathTarget) {
755
browserPath = browserPathTarget.outputHref;
756
}
757
}
758
759
// compute browse url
760
const targetPath = browserPath
761
? browserPath
762
: pdfOutput
763
? kPdfJsInitialPath
764
: renderResultUrlPath(renderResult);
765
766
// print browse url and open browser if requested
767
const path = (targetPath && targetPath !== "index.html") ? targetPath : "";
768
769
// start listening
770
let stop: () => void | undefined;
771
772
return {
773
start: () => Promise.resolve(path),
774
serve: async () => {
775
const { server, stop: stopServer } = handleHttpRequests({
776
port: options.port!,
777
hostname: options.host,
778
handler,
779
});
780
stop = stopServer;
781
await server.finished;
782
},
783
stop: () => {
784
stop?.();
785
return Promise.resolve();
786
},
787
};
788
}
789
790
function previewControlChannelRequestHandler(
791
project: ProjectContext,
792
renderManager: ServeRenderManager,
793
watcher: ProjectWatcher,
794
extensionDirs: string[],
795
resourceFiles: string[],
796
flags: RenderFlags,
797
pandocArgs: string[],
798
requireActiveClient: boolean,
799
): (req: Request) => Promise<Response | undefined> {
800
return async (req: Request) => {
801
if (watcher.handle(req)) {
802
return await watcher.request(req);
803
} else if (isPreviewTerminateRequest(req)) {
804
exitWithCleanup(0);
805
} else if (isPreviewRenderRequest(req)) {
806
const prevReq = previewRenderRequest(
807
req,
808
requireActiveClient ? watcher.hasClients() : true,
809
project!.dir,
810
);
811
if (
812
prevReq &&
813
(await previewRenderRequestIsCompatible(prevReq, project, flags.to))
814
) {
815
if (isProjectInputFile(prevReq.path, project!)) {
816
const services = renderServices(notebookContext());
817
// if there is no specific format requested then 'all' needs
818
// to become 'html' so we don't render all formats
819
const to = flags.to === "all" ? (prevReq.format || "html") : flags.to;
820
renderManager.submitRender(() =>
821
render(prevReq.path, {
822
services,
823
flags: { ...flags, to },
824
pandocArgs,
825
previewServer: true,
826
})
827
).then((result) => {
828
if (result.error) {
829
result.context.cleanup();
830
renderManager.onRenderError(result.error);
831
} else {
832
// print output created
833
const finalOutput = renderResultFinalOutput(
834
result,
835
dirname(prevReq.path),
836
);
837
if (!finalOutput) {
838
throw new Error(
839
"No output created by quarto render " +
840
basename(prevReq.path),
841
);
842
}
843
844
renderManager.onRenderResult(
845
result,
846
extensionDirs,
847
resourceFiles,
848
watcher.project(),
849
);
850
result.context.cleanup();
851
852
info("Output created: " + finalOutput + "\n");
853
854
// notify user we are watching for reload
855
printWatchingForChangesMessage();
856
857
watcher.reloadClients(
858
true,
859
!isPdfContent(finalOutput)
860
? join(dirname(prevReq.path), finalOutput)
861
: undefined,
862
);
863
}
864
}).finally(() => {
865
services.cleanup();
866
});
867
return httpContentResponse("rendered");
868
// if this is a plain markdown file w/ an external preview server
869
// then just return success (it's already been saved as a
870
// precursor to the render)
871
} else if (
872
extname(prevReq.path) === ".md" && projectPreviewServe(project)
873
) {
874
return httpContentResponse("rendered");
875
} else {
876
return previewUnableToRenderResponse();
877
}
878
} else {
879
return previewUnableToRenderResponse();
880
}
881
} else {
882
return undefined;
883
}
884
};
885
}
886
887
// https://deno.com/blog/v1.23#remove-unstable-denosleepsync-api
888
function sleepSync(timeout: number) {
889
const sab = new SharedArrayBuffer(1024);
890
const int32 = new Int32Array(sab);
891
Atomics.wait(int32, 0, 0, timeout);
892
}
893
894
function acquirePreviewLock(project: ProjectContext) {
895
// get lockfile
896
const lockfile = previewLockFile(project);
897
898
// if there is a lockfile send a kill signal to the pid therin
899
if (existsSync(lockfile)) {
900
const pid = parseInt(Deno.readTextFileSync(lockfile)) || undefined;
901
if (pid) {
902
info(
903
colors.bold(colors.blue("Terminating existing preview server....")),
904
{ newline: false },
905
);
906
try {
907
Deno.kill(pid, "SIGTERM");
908
sleepSync(3000);
909
} catch {
910
//
911
} finally {
912
info(colors.bold(colors.blue("DONE\n")));
913
}
914
}
915
}
916
917
// write our pid to the lockfile
918
Deno.writeTextFileSync(lockfile, String(Deno.pid));
919
920
// remove the lockfile when we exit
921
onCleanup(() => releasePreviewLock(project));
922
}
923
924
function releasePreviewLock(project: ProjectContext) {
925
try {
926
safeRemoveSync(previewLockFile(project));
927
} catch {
928
//
929
}
930
}
931
932
function previewLockFile(project: ProjectContext) {
933
return projectScratchPath(project.dir, join("preview", "lock"));
934
}
935
936
function renderErrorPage(e: Error) {
937
const content = `
938
<!doctype html>
939
<html lang=en>
940
<head>
941
<meta charset=utf-8>
942
<title>Quarto Render Error</title>
943
<script id="quarto-render-error" type="text/plain">${e.message}</script>
944
</head>
945
<body>
946
</body>
947
</html>
948
`;
949
return new TextEncoder().encode(content);
950
}
951
952
async function serveFiles(
953
project: ProjectContext,
954
): Promise<{ files: string[]; resourceFiles: string[] }> {
955
const projType = projectType(project.config?.project?.[kProjectType]);
956
957
// one time denoDom init
958
await initDenoDom();
959
960
const files: string[] = [];
961
const resourceFiles: string[] = [];
962
for (let i = 0; i < project.files.input.length; i++) {
963
const inputFile = project.files.input[i];
964
const projRelative = relative(project.dir, inputFile);
965
const target = await resolveInputTarget(project, projRelative, false);
966
if (target) {
967
const outputFile = join(projectOutputDir(project), target?.outputHref);
968
if (
969
isModifiedAfter(inputFile, outputFile) ||
970
projType.previewSkipUnmodified === false // Project types can force not skipping the rendering of files
971
) {
972
// render this file
973
files.push(inputFile);
974
} else {
975
// we aren't rendering this file, so we need to compute it's resource files
976
// for monitoring during serve
977
978
// resource files referenced in html
979
const outputResources: string[] = [];
980
if (isHtmlContent(outputFile)) {
981
const htmlInput = Deno.readTextFileSync(outputFile);
982
const doc = new DOMParser().parseFromString(htmlInput, "text/html")!;
983
const resolver = htmlResourceResolverPostprocessor(
984
inputFile,
985
project,
986
);
987
outputResources.push(...(await resolver(doc)).resources);
988
}
989
990
// partition markdown and read globs
991
const partitioned = await partitionedMarkdownForInput(
992
project,
993
projRelative,
994
);
995
const globs: string[] = [];
996
if (partitioned?.yaml) {
997
const metadata = partitioned.yaml;
998
globs.push(...resourcesFromMetadata(metadata[kResources]));
999
}
1000
1001
// compute resource refs and add them
1002
resourceFiles.push(
1003
...(await resourceFilesFromFile(
1004
project.dir,
1005
projectExcludeDirs(project),
1006
projRelative,
1007
{ files: outputResources, globs },
1008
false, // selfContained,
1009
[join(dirname(projRelative), inputFilesDir(projRelative))],
1010
partitioned,
1011
)),
1012
);
1013
}
1014
} else {
1015
warning("Unabled to resolve output target for " + inputFile);
1016
}
1017
}
1018
1019
return { files, resourceFiles: ld.uniq(resourceFiles) as string[] };
1020
}
1021
1022