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