Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/preview/preview.ts
6433 views
1
/*
2
* preview.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { debug, info, warning } from "../../deno_ral/log.ts";
8
import {
9
basename,
10
dirname,
11
isAbsolute,
12
join,
13
relative,
14
} from "../../deno_ral/path.ts";
15
import { existsSync } from "../../deno_ral/fs.ts";
16
17
import * as ld from "../../core/lodash.ts";
18
19
import { cssFileResourceReferences } from "../../core/css.ts";
20
import { logError } from "../../core/log.ts";
21
import { openUrl } from "../../core/shell.ts";
22
import {
23
httpContentResponse,
24
httpFileRequestHandler,
25
isBrowserPreviewable,
26
serveRedirect,
27
} from "../../core/http.ts";
28
import { HttpFileRequestOptions } from "../../core/http-types.ts";
29
import {
30
HttpDevServer,
31
httpDevServer,
32
HttpDevServerRenderMonitor,
33
} from "../../core/http-devserver.ts";
34
import { isHtmlContent, isPdfContent, isTextContent } from "../../core/mime.ts";
35
import { PromiseQueue } from "../../core/promise.ts";
36
import { inputFilesDir } from "../../core/render.ts";
37
38
import { kQuartoRenderCommand } from "../render/constants.ts";
39
40
import {
41
previewUnableToRenderResponse,
42
printWatchingForChangesMessage,
43
render,
44
renderToken,
45
} from "../render/render-shared.ts";
46
import {
47
renderServices,
48
withRenderServices,
49
} from "../render/render-services.ts";
50
import {
51
RenderFlags,
52
RenderResult,
53
RenderResultFile,
54
RenderServices,
55
} from "../render/types.ts";
56
import { renderFormats } from "../render/render-contexts.ts";
57
import { renderResultFinalOutput } from "../render/render.ts";
58
import { replacePandocArg } from "../render/flags.ts";
59
60
import { Format, isPandocFilter } from "../../config/types.ts";
61
import {
62
kPdfJsInitialPath,
63
pdfJsBaseDir,
64
pdfJsFileHandler,
65
} from "../../core/pdfjs.ts";
66
import {
67
kProjectWatchInputs,
68
ProjectContext,
69
ProjectPreview,
70
} from "../../project/types.ts";
71
import { projectOutputDir } from "../../project/project-shared.ts";
72
import { projectContext } from "../../project/project-context.ts";
73
import {
74
normalizePath,
75
pathWithForwardSlashes,
76
safeExistsSync,
77
} from "../../core/path.ts";
78
import {
79
isPositWorkbench,
80
isRStudio,
81
isServerSession,
82
isVSCodeServer,
83
vsCodeServerProxyUri,
84
} from "../../core/platform.ts";
85
import { isJupyterNotebook } from "../../core/jupyter/jupyter.ts";
86
import { watchForFileChanges } from "../../core/watch.ts";
87
import { previewMonitorResources } from "../../core/quarto.ts";
88
import { exitWithCleanup, onCleanup } from "../../core/cleanup.ts";
89
import {
90
extensionFilesFromDirs,
91
inputExtensionDirs,
92
} from "../../extension/extension.ts";
93
import { kOutputFile, kTargetFormat } from "../../config/constants.ts";
94
import { mergeConfigs } from "../../core/config.ts";
95
import { kLocalhost } from "../../core/port-consts.ts";
96
import { findOpenPort, waitForPort } from "../../core/port.ts";
97
import { inputFileForOutputFile } from "../../project/project-index.ts";
98
import { staticResource } from "../../preview/preview-static.ts";
99
import { previewTextContent } from "../../preview/preview-text.ts";
100
import {
101
previewURL,
102
printBrowsePreviewMessage,
103
rswURL,
104
} from "../../core/previewurl.ts";
105
import { notebookContext } from "../../render/notebook/notebook-context.ts";
106
import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts";
107
108
export async function resolvePreviewOptions(
109
options: ProjectPreview,
110
project?: ProjectContext,
111
): Promise<ProjectPreview> {
112
// start with project options if we have them
113
if (project?.config?.project.preview) {
114
options = mergeConfigs(project.config.project.preview, options);
115
}
116
// provide defaults
117
const resolved = mergeConfigs({
118
host: kLocalhost,
119
browser: true,
120
[kProjectWatchInputs]: !isRStudio(),
121
timeout: 0,
122
navigate: true,
123
}, options) as ProjectPreview;
124
125
// if a specific port is requested then wait for it up to 5 seconds
126
if (resolved.port) {
127
if (!await waitForPort({ port: resolved.port, hostname: resolved.host })) {
128
throw new Error(`Requested port ${options.port} is already in use.`);
129
}
130
} else {
131
resolved.port = findOpenPort();
132
}
133
134
return resolved;
135
}
136
137
interface PreviewOptions {
138
port?: number;
139
host?: string;
140
browser?: boolean;
141
[kProjectWatchInputs]?: boolean;
142
timeout?: number;
143
presentation: boolean;
144
}
145
146
export async function preview(
147
file: string,
148
flags: RenderFlags,
149
pandocArgs: string[],
150
options: PreviewOptions,
151
) {
152
const nbContext = notebookContext();
153
// see if this is project file
154
const project = (await projectContext(file, nbContext)) ||
155
(await singleFileProjectContext(file, nbContext));
156
onCleanup(() => {
157
project.cleanup();
158
});
159
160
// determine the target format if there isn't one in the command line args
161
// (current we force the use of an html or pdf based format)
162
const format = await previewFormat(file, project, flags.to, undefined);
163
setPreviewFormat(format, flags, pandocArgs);
164
165
// render for preview (create function we can pass to watcher then call it)
166
let isRendering = false;
167
const render = async (to?: string) => {
168
const renderFlags = { ...flags, to: to || flags.to };
169
const services = renderServices(nbContext);
170
try {
171
HttpDevServerRenderMonitor.onRenderStart();
172
isRendering = true;
173
const result = await renderForPreview(
174
file,
175
services,
176
renderFlags,
177
pandocArgs,
178
project,
179
);
180
HttpDevServerRenderMonitor.onRenderStop(true);
181
return result;
182
} catch (error) {
183
HttpDevServerRenderMonitor.onRenderStop(false);
184
throw error;
185
} finally {
186
isRendering = false;
187
services.cleanup();
188
}
189
};
190
const result = await render();
191
192
// resolve options (don't look at the project context b/c we
193
// don't want overlapping ports within the same project)
194
options = {
195
...options,
196
...(await resolvePreviewOptions(options)),
197
};
198
199
const ac = new AbortController();
200
// create listener and callback to stop the server
201
// const listener = Deno.listen({ port: options.port!, hostname: options.host });
202
const stopServer = () => ac.abort();
203
204
// create client reloader
205
const reloader = httpDevServer(
206
options.timeout!,
207
() => isRendering,
208
stopServer,
209
options.presentation || format === "revealjs",
210
);
211
212
// watch for changes and re-render / re-load as necessary
213
const changeHandler = createChangeHandler(
214
result,
215
reloader,
216
render,
217
options[kProjectWatchInputs]!,
218
);
219
220
// create file request handler (hook clients up to reloader, provide
221
// function to be used if a render request comes in)
222
const handler = isPdfContent(result.outputFile)
223
? pdfFileRequestHandler(
224
result.outputFile,
225
normalizePath(file),
226
flags,
227
result.format,
228
options.port!,
229
reloader,
230
changeHandler.render,
231
project,
232
)
233
: project
234
? projectHtmlFileRequestHandler(
235
project,
236
normalizePath(file),
237
flags,
238
result.format,
239
reloader,
240
changeHandler.render,
241
)
242
: htmlFileRequestHandler(
243
result.outputFile,
244
normalizePath(file),
245
flags,
246
result.format,
247
reloader,
248
changeHandler.render,
249
project,
250
);
251
252
// open browser if this is a browseable format
253
const initialPath = isPdfContent(result.outputFile)
254
? kPdfJsInitialPath
255
: project
256
? pathWithForwardSlashes(
257
relative(projectOutputDir(project), result.outputFile),
258
)
259
: "";
260
if (
261
options.browser &&
262
!isServerSession() &&
263
isBrowserPreviewable(result.outputFile)
264
) {
265
await openUrl(previewURL(options.host!, options.port!, initialPath));
266
}
267
268
// print status
269
await printBrowsePreviewMessage(options.host!, options.port!, initialPath);
270
271
// watch for src changes in dev mode
272
previewMonitorResources(stopServer);
273
274
// serve project
275
const server = Deno.serve(
276
{ signal: ac.signal, port: options.port!, hostname: options.host },
277
async (req: Request) => {
278
try {
279
return await handler(req);
280
} catch (err) {
281
if (err instanceof Error) {
282
warning(err.message);
283
}
284
throw err;
285
}
286
},
287
);
288
await server.finished;
289
}
290
291
export interface PreviewRenderRequest {
292
version: 1 | 2;
293
path: string;
294
format?: string;
295
}
296
297
export function isPreviewRenderRequest(req: Request) {
298
if (req.url.includes(kQuartoRenderCommand)) {
299
return true;
300
} else {
301
const token = renderToken();
302
if (token) {
303
return req.url.includes(token);
304
} else {
305
return false;
306
}
307
}
308
}
309
310
export function isPreviewTerminateRequest(req: Request) {
311
const kTerminateToken = "4231F431-58D3-4320-9713-994558E4CC45";
312
return req.url.includes(kTerminateToken);
313
}
314
315
export function previewRenderRequest(
316
req: Request,
317
hasClients: boolean,
318
baseDir?: string,
319
): PreviewRenderRequest | undefined {
320
// look for v1 rstudio format (requires baseDir b/c its a relative path)
321
const match = req.url.match(
322
new RegExp(`/${kQuartoRenderCommand}/(.*)$`),
323
);
324
if (match && baseDir) {
325
return {
326
version: 1,
327
path: join(baseDir, match[1]),
328
};
329
} else {
330
const token = renderToken();
331
if (token && req.url.includes(token)) {
332
const url = new URL(req.url);
333
const path = url.searchParams.get("path");
334
if (path) {
335
if (hasClients) {
336
return {
337
version: 2,
338
path,
339
format: url.searchParams.get("format") || undefined,
340
};
341
}
342
}
343
}
344
}
345
}
346
347
export async function previewRenderRequestIsCompatible(
348
request: PreviewRenderRequest,
349
project: ProjectContext,
350
format?: string,
351
) {
352
if (request.version === 1) {
353
return true; // rstudio manages its own request compatibility state
354
} else {
355
const reqFormat = await previewFormat(
356
request.path,
357
project,
358
request.format,
359
undefined,
360
);
361
return reqFormat === format;
362
}
363
}
364
365
// determine the format to preview
366
export async function previewFormat(
367
file: string,
368
project: ProjectContext,
369
format?: string,
370
formats?: Record<string, Format>,
371
) {
372
if (format) {
373
return format;
374
}
375
// const nbContext = notebookContext();
376
// project = project || (await singleFileProjectContext(file, nbContext));
377
formats = formats ||
378
await withRenderServices(
379
project.notebookContext,
380
(services: RenderServices) =>
381
renderFormats(file, services, "all", project),
382
);
383
format = Object.keys(formats)[0] || "html";
384
return format;
385
}
386
387
export function setPreviewFormat(
388
format: string,
389
flags: RenderFlags,
390
pandocArgs: string[],
391
) {
392
flags.to = format;
393
replacePandocArg(pandocArgs, "--to", format);
394
}
395
396
export function handleRenderResult(
397
file: string,
398
renderResult: RenderResult,
399
) {
400
// print output created
401
const finalOutput = renderResultFinalOutput(
402
renderResult,
403
dirname(file),
404
);
405
if (!finalOutput) {
406
throw new Error("No output created by quarto render " + basename(file));
407
}
408
info("Output created: " + finalOutput + "\n");
409
return finalOutput;
410
}
411
412
export interface RenderForPreviewResult {
413
file: string;
414
format: Format;
415
outputFile: string;
416
extensionFiles: string[];
417
resourceFiles: string[];
418
}
419
420
export async function renderForPreview(
421
file: string,
422
services: RenderServices,
423
flags: RenderFlags,
424
pandocArgs: string[],
425
project?: ProjectContext,
426
): Promise<RenderForPreviewResult> {
427
// Invalidate file cache for the file being rendered so changes are picked up.
428
// The project context persists across re-renders in preview mode, but the
429
// fileInformationCache contains file content that needs to be refreshed.
430
// TODO(#13955): Consider adding a dedicated invalidateForFile() method on ProjectContext
431
if (project?.fileInformationCache) {
432
project.fileInformationCache.delete(file);
433
}
434
435
// render
436
const renderResult = await render(file, {
437
services,
438
flags,
439
pandocArgs: pandocArgs,
440
previewServer: true,
441
setProjectDir: project !== undefined,
442
}, project);
443
if (renderResult.error) {
444
throw renderResult.error;
445
}
446
447
// print output created
448
const finalOutput = handleRenderResult(file, renderResult);
449
450
// notify user we are watching for reload
451
printWatchingForChangesMessage();
452
453
// determine files to watch for reload -- take the resource
454
// files detected during render, chase down additional references
455
// in css files, then filter out the _files dir
456
file = normalizePath(file);
457
const filesDir = join(dirname(file), inputFilesDir(file));
458
const resourceFiles = renderResult.files.reduce(
459
(resourceFiles: string[], file: RenderResultFile) => {
460
const resources = file.resourceFiles.concat(
461
cssFileResourceReferences(file.resourceFiles),
462
);
463
return resourceFiles.concat(
464
resources.filter((resFile) => !resFile.startsWith(filesDir)),
465
);
466
},
467
[],
468
);
469
470
// extension files
471
const extensionFiles = extensionFilesFromDirs(
472
inputExtensionDirs(file, project?.dir),
473
);
474
// shortcodes and filters (treat as extension files)
475
extensionFiles.push(...renderResult.files.reduce(
476
(extensionFiles: string[], file: RenderResultFile) => {
477
const shortcodes = file.format.render.shortcodes || [];
478
const filters = (file.format.pandoc.filters || []).map((filter) =>
479
isPandocFilter(filter) ? filter.path : filter
480
);
481
const ipynbFilters = file.format.execute["ipynb-filters"] || [];
482
[...shortcodes, ...filters.map((filter) => filter), ...ipynbFilters]
483
.forEach((extensionFile) => {
484
if (!isAbsolute(extensionFile)) {
485
const extensionFullPath = join(dirname(file.input), extensionFile);
486
if (existsSync(extensionFullPath)) {
487
extensionFiles.push(normalizePath(extensionFullPath));
488
}
489
}
490
});
491
return extensionFiles;
492
},
493
[],
494
));
495
496
return {
497
file,
498
format: renderResult.files[0].format,
499
outputFile: join(dirname(file), finalOutput),
500
extensionFiles,
501
resourceFiles,
502
};
503
}
504
505
export interface ChangeHandler {
506
render: () => Promise<RenderForPreviewResult | undefined>;
507
}
508
509
export function createChangeHandler(
510
result: RenderForPreviewResult,
511
reloader: { reloadClients: (reloadTarget?: string) => Promise<void> },
512
render: (to?: string) => Promise<RenderForPreviewResult | undefined>,
513
renderOnChange: boolean,
514
reloadFileFilter: (file: string) => boolean = () => true,
515
ignoreChanges?: (files: string[]) => boolean,
516
): ChangeHandler {
517
const renderQueue = new PromiseQueue<RenderForPreviewResult | undefined>();
518
let watcher: Watcher | undefined;
519
let lastResult = result;
520
521
// render handler
522
const renderHandler = async (to?: string) => {
523
try {
524
// if we have an alternate format then stop the watcher (as the alternate
525
// output format will be one of the watched resource files!)
526
if (to && watcher) {
527
watcher.stop();
528
}
529
// render
530
const result = await renderQueue.enqueue(async () => {
531
return render(to);
532
}, true);
533
if (result) {
534
sync(result);
535
}
536
537
return result;
538
} catch (e) {
539
if (e instanceof Error && e.message) {
540
// jupyter notebooks being edited in juptyerlab sometimes get an
541
// "Unexpected end of JSON input" error that remedies itself (so we ignore).
542
// this may be a result of an intermediate save result?
543
if (
544
isJupyterNotebook(result.file) &&
545
e.message.indexOf("Unexpected end of JSON input") !== -1
546
) {
547
return;
548
}
549
550
logError(e);
551
}
552
}
553
};
554
555
const sync = (result: RenderForPreviewResult) => {
556
const requiresSync = !watcher || resultRequiresSync(result, lastResult);
557
lastResult = result;
558
if (requiresSync) {
559
if (watcher) {
560
watcher.stop();
561
}
562
563
const watches: Watch[] = [];
564
if (renderOnChange) {
565
watches.push({
566
files: [result.file],
567
handler: ld.debounce(renderHandler, 50),
568
});
569
}
570
571
// re-render on extension change (as a mere reload won't reflect
572
// the changes as they do w/ e.g. css files)
573
watches.push({
574
files: result.extensionFiles,
575
handler: ld.debounce(renderHandler, 50),
576
});
577
578
// reload on output or resource changed (but wait for
579
// the render queue to finish, as sometimes pdfs are
580
// modified and even removed by pdflatex during render)
581
const reloadFiles = isPdfContent(result.outputFile)
582
? pdfReloadFiles(result)
583
: resultReloadFiles(result);
584
const reloadTarget = isPdfContent(result.outputFile)
585
? "/" + kPdfJsInitialPath
586
: "";
587
588
// https://github.com/quarto-dev/quarto-cli/issues/9547
589
// ... this fix means we'll never be able to support files
590
// fix question marks or octothorpes in their names
591
const removeUrlFragment = (file: string) =>
592
file.replace(/#.*$/, "").replace(/\?.*$/, "");
593
watches.push({
594
files: reloadFiles.filter(reloadFileFilter).map(removeUrlFragment),
595
handler: ld.debounce(async () => {
596
await renderQueue.enqueue(async () => {
597
await reloader.reloadClients(reloadTarget);
598
return undefined;
599
});
600
}, 50),
601
});
602
603
watcher = previewWatcher(watches, ignoreChanges);
604
watcher.start();
605
}
606
};
607
sync(result);
608
return {
609
render: renderHandler,
610
};
611
}
612
613
interface Watch {
614
files: string[];
615
handler: () => Promise<void>;
616
}
617
618
interface Watcher {
619
start: VoidFunction;
620
stop: VoidFunction;
621
}
622
623
function previewWatcher(
624
watches: Watch[],
625
ignoreChanges?: (files: string[]) => boolean,
626
): Watcher {
627
existsSync;
628
watches = watches.map((watch) => {
629
return {
630
...watch,
631
files: watch.files.filter((s) => existsSync(s)).map((file) => {
632
return normalizePath(file);
633
}),
634
};
635
});
636
const handlerForFile = (file: string) => {
637
const watch = watches.find((watch) => watch.files.includes(file));
638
return watch?.handler;
639
};
640
641
// create the watcher
642
const files = watches.flatMap((watch) => watch.files);
643
const fsWatcher = watchForFileChanges(files);
644
const watchForChanges = async () => {
645
for await (const event of fsWatcher) {
646
try {
647
if (
648
event.kind === "modify" &&
649
(!ignoreChanges || !ignoreChanges(event.paths))
650
) {
651
const handlers = new Set<() => Promise<void>>();
652
event.paths.forEach((path) => {
653
const handler = handlerForFile(path);
654
if (handler && !handlers.has(handler)) {
655
handlers.add(handler);
656
}
657
});
658
for (const handler of handlers) {
659
await handler();
660
}
661
}
662
} catch (e) {
663
logError(e);
664
}
665
}
666
};
667
668
return {
669
start: watchForChanges,
670
stop: () => fsWatcher.close(),
671
};
672
}
673
674
function projectHtmlFileRequestHandler(
675
context: ProjectContext,
676
inputFile: string,
677
flags: RenderFlags,
678
format: Format,
679
reloader: HttpDevServer,
680
renderHandler: (to?: string) => Promise<RenderForPreviewResult | undefined>,
681
) {
682
return httpFileRequestHandler(
683
htmlFileRequestHandlerOptions(
684
projectOutputDir(context),
685
"index.html",
686
inputFile,
687
flags,
688
format,
689
reloader,
690
renderHandler,
691
context,
692
),
693
);
694
}
695
696
function htmlFileRequestHandler(
697
htmlFile: string,
698
inputFile: string,
699
flags: RenderFlags,
700
format: Format,
701
reloader: HttpDevServer,
702
renderHandler: (to?: string) => Promise<RenderForPreviewResult | undefined>,
703
context: ProjectContext,
704
) {
705
return httpFileRequestHandler(
706
htmlFileRequestHandlerOptions(
707
dirname(htmlFile),
708
basename(htmlFile),
709
inputFile,
710
flags,
711
format,
712
reloader,
713
renderHandler,
714
context,
715
),
716
);
717
}
718
719
function htmlFileRequestHandlerOptions(
720
baseDir: string,
721
defaultFile: string,
722
inputFile: string,
723
flags: RenderFlags,
724
format: Format,
725
devserver: HttpDevServer,
726
renderHandler: (to?: string) => Promise<RenderForPreviewResult | undefined>,
727
project: ProjectContext,
728
): HttpFileRequestOptions {
729
// if we an alternate format on the fly we need to do a full re-render
730
// to get the correct state back. this flag will be set whenever
731
// we render an alternate format
732
let invalidateDevServerReRender = false;
733
return {
734
baseDir,
735
defaultFile,
736
printUrls: "404",
737
onRequest: async (req: Request) => {
738
if (devserver.handle(req)) {
739
return Promise.resolve(devserver.request(req));
740
} else if (isPreviewTerminateRequest(req)) {
741
exitWithCleanup(0);
742
} else if (req.url.endsWith("/quarto-render/")) {
743
// don't wait for the promise so the
744
// caller gets an immediate reply
745
renderHandler();
746
return Promise.resolve(httpContentResponse("rendered"));
747
} else if (isPreviewRenderRequest(req)) {
748
const outputFile = format.pandoc[kOutputFile];
749
const prevReq = previewRenderRequest(
750
req,
751
!isBrowserPreviewable(outputFile) || devserver.hasClients(),
752
);
753
if (
754
!invalidateDevServerReRender &&
755
prevReq &&
756
existsSync(prevReq.path) &&
757
normalizePath(prevReq.path) === normalizePath(inputFile) &&
758
await previewRenderRequestIsCompatible(prevReq, project, flags.to)
759
) {
760
// don't wait for the promise so the
761
// caller gets an immediate reply
762
renderHandler();
763
return Promise.resolve(httpContentResponse("rendered"));
764
} else {
765
return Promise.resolve(previewUnableToRenderResponse());
766
}
767
} else {
768
return Promise.resolve(undefined);
769
}
770
},
771
onFile: async (file: string, req: Request) => {
772
// check for static response
773
const staticResponse = await staticResource(baseDir, file);
774
if (staticResponse) {
775
const resolveBody = () => {
776
if (staticResponse.injectClient) {
777
const client = devserver.clientHtml(
778
req,
779
inputFile,
780
);
781
const contents = new TextDecoder().decode(
782
staticResponse.contents,
783
);
784
return staticResponse.injectClient(contents, client);
785
} else {
786
return staticResponse.contents;
787
}
788
};
789
const body = resolveBody();
790
791
return {
792
body,
793
contentType: staticResponse.contentType,
794
};
795
}
796
797
// the 'format' passed to this function is for the default
798
// render target, however this could be a request for another
799
// render target (e.g. a link in the 'More Formats' section)
800
// some of these formats might require rendering and/or may
801
// have extended preview behavior (e.g. preview-type: raw)
802
// in this case try to lookup the format and perform a render
803
let renderFormat = format;
804
if (project) {
805
const input = await inputFileForOutputFile(
806
project,
807
relative(baseDir, file),
808
);
809
if (input) {
810
renderFormat = input.format;
811
if (renderFormat !== format && fileRequiresRender(input.file, file)) {
812
invalidateDevServerReRender = true;
813
await renderHandler(renderFormat.identifier[kTargetFormat]);
814
}
815
}
816
}
817
818
// https://github.com/quarto-dev/quarto-cli/issues/5215
819
// return CORS requests as plain text so that OJS requests do
820
// not have formatting
821
if (
822
req.headers.get("sec-fetch-dest") === "empty" &&
823
req.headers.get("sec-fetch-mode") === "cors"
824
) {
825
return;
826
}
827
828
if (isHtmlContent(file)) {
829
// does the provide an alternate preview file?
830
if (renderFormat.formatPreviewFile) {
831
file = renderFormat.formatPreviewFile(file, renderFormat);
832
}
833
const fileContents = await Deno.readFile(file);
834
return devserver.injectClient(req, fileContents, inputFile);
835
} else if (isTextContent(file)) {
836
return previewTextContent(
837
file,
838
inputFile,
839
format,
840
req,
841
devserver.injectClient,
842
);
843
}
844
},
845
};
846
}
847
848
function fileRequiresRender(inputFile: string, outputFile: string) {
849
if (safeExistsSync(outputFile)) {
850
return (Deno.statSync(inputFile).mtime?.valueOf() || 0) >
851
(Deno.statSync(outputFile).mtime?.valueOf() || 0);
852
} else {
853
return true;
854
}
855
}
856
857
function resultReloadFiles(result: RenderForPreviewResult) {
858
return [result.outputFile].concat(result.resourceFiles);
859
}
860
861
function pdfFileRequestHandler(
862
pdfFile: string,
863
inputFile: string,
864
flags: RenderFlags,
865
format: Format,
866
port: number,
867
reloader: HttpDevServer,
868
renderHandler: () => Promise<RenderForPreviewResult | undefined>,
869
project: ProjectContext,
870
) {
871
// start w/ the html handler (as we still need it's http reload injection)
872
const pdfOptions = htmlFileRequestHandlerOptions(
873
dirname(pdfFile),
874
basename(pdfFile),
875
inputFile,
876
flags,
877
format,
878
reloader,
879
renderHandler,
880
project,
881
);
882
883
// pdf customizations
884
885
pdfOptions.baseDir = pdfJsBaseDir();
886
887
if (pdfOptions.onRequest) {
888
const onRequest = pdfOptions.onRequest;
889
pdfOptions.onRequest = async (req: Request) => {
890
if (new URL(req.url).pathname === "/") {
891
const url = isPositWorkbench()
892
? await rswURL(port, kPdfJsInitialPath)
893
: isVSCodeServer()
894
? vsCodeServerProxyUri()!.replace("{{port}}", `${port}`) +
895
kPdfJsInitialPath
896
: "/" + kPdfJsInitialPath;
897
return Promise.resolve(serveRedirect(url));
898
} else {
899
return Promise.resolve(onRequest(req));
900
}
901
};
902
}
903
904
pdfOptions.onFile = pdfJsFileHandler(() => pdfFile, pdfOptions.onFile);
905
906
return httpFileRequestHandler(pdfOptions);
907
}
908
909
function pdfReloadFiles(result: RenderForPreviewResult) {
910
return [result.outputFile];
911
}
912
913
function resultRequiresSync(
914
result: RenderForPreviewResult,
915
lastResult?: RenderForPreviewResult,
916
) {
917
if (!lastResult) {
918
return true;
919
}
920
return result.file !== lastResult.file ||
921
result.outputFile !== lastResult.outputFile ||
922
!ld.isEqual(result.extensionFiles, lastResult.extensionFiles) ||
923
!ld.isEqual(result.resourceFiles, lastResult.resourceFiles);
924
}
925
926