Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/render/project.ts
6429 views
1
/*
2
* project.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
ensureDirSync,
9
existsSync,
10
safeMoveSync,
11
safeRemoveDirSync,
12
safeRemoveSync,
13
UnsafeRemovalError,
14
} from "../../deno_ral/fs.ts";
15
import { dirname, isAbsolute, join, relative } from "../../deno_ral/path.ts";
16
import { info, warning } from "../../deno_ral/log.ts";
17
18
import * as colors from "fmt/colors";
19
20
import { copyMinimal, copyTo } from "../../core/copy.ts";
21
import * as ld from "../../core/lodash.ts";
22
23
import {
24
kKeepMd,
25
kKeepTex,
26
kKeepTyp,
27
kTargetFormat,
28
} from "../../config/constants.ts";
29
30
import {
31
kProjectExecuteDir,
32
kProjectLibDir,
33
kProjectPostRender,
34
kProjectPreRender,
35
kProjectType,
36
ProjectContext,
37
} from "../../project/types.ts";
38
import { kQuartoScratch } from "../../project/project-scratch.ts";
39
40
import { projectType } from "../../project/types/project-types.ts";
41
import { copyResourceFile } from "../../project/project-resources.ts";
42
import { ensureGitignore } from "../../project/project-gitignore.ts";
43
import { partitionedMarkdownForInput } from "../../project/project-config.ts";
44
45
import { renderFiles } from "./render-files.ts";
46
import {
47
RenderedFile,
48
RenderFile,
49
RenderOptions,
50
RenderResult,
51
} from "./types.ts";
52
import {
53
copyToProjectFreezer,
54
kProjectFreezeDir,
55
pruneProjectFreezer,
56
pruneProjectFreezerDir,
57
} from "./freeze.ts";
58
import { resourceFilesFromRenderedFile } from "./resources.ts";
59
import { inputFilesDir } from "../../core/render.ts";
60
import {
61
removeIfEmptyDir,
62
safeRemoveIfExists,
63
} from "../../core/path.ts";
64
import { handlerForScript } from "../../core/run/run.ts";
65
import { execProcess } from "../../core/process.ts";
66
import { parseShellRunCommand } from "../../core/run/shell.ts";
67
import { clearProjectIndex } from "../../project/project-index.ts";
68
import {
69
hasProjectOutputDir,
70
projectExcludeDirs,
71
projectFormatOutputDir,
72
projectOutputDir,
73
} from "../../project/project-shared.ts";
74
import { asArray } from "../../core/array.ts";
75
import { normalizePath } from "../../core/path.ts";
76
import { isSubdir } from "../../deno_ral/fs.ts";
77
import { Format } from "../../config/types.ts";
78
import { fileExecutionEngine } from "../../execute/engine.ts";
79
import { projectContextForDirectory } from "../../project/project-context.ts";
80
import { ProjectType } from "../../project/types/types.ts";
81
82
const noMutationValidations = (
83
projType: ProjectType,
84
projOutputDir: string,
85
projDir: string,
86
) => {
87
return [{
88
val: projType,
89
newVal: (context: ProjectContext) => {
90
return projectType(context.config?.project?.[kProjectType]);
91
},
92
msg: "The project type may not be mutated by the pre-render script",
93
}, {
94
val: projOutputDir,
95
newVal: (context: ProjectContext) => {
96
return projectOutputDir(context);
97
},
98
msg: "The project output-dir may not be mutated by the pre-render script",
99
}, {
100
val: projDir,
101
newVal: (context: ProjectContext) => {
102
return normalizePath(context.dir);
103
},
104
msg: "The project dir may not be mutated by the pre-render script",
105
}];
106
};
107
108
interface ProjectInputs {
109
projType: ProjectType;
110
projOutputDir: string;
111
projDir: string;
112
context: ProjectContext;
113
files: string[] | undefined;
114
options: RenderOptions;
115
}
116
117
interface ProjectRenderConfig {
118
behavior: {
119
incremental: boolean;
120
renderAll: boolean;
121
};
122
alwaysExecuteFiles: string[] | undefined;
123
filesToRender: RenderFile[];
124
options: RenderOptions;
125
supplements: {
126
files: RenderFile[];
127
onRenderComplete?: (
128
project: ProjectContext,
129
files: string[],
130
incremental: boolean,
131
) => Promise<void>;
132
};
133
}
134
135
const computeProjectRenderConfig = async (
136
inputs: ProjectInputs,
137
): Promise<ProjectRenderConfig> => {
138
// is this an incremental render?
139
const incremental = !!inputs.files;
140
141
// force execution for any incremental files (unless options.useFreezer is set)
142
let alwaysExecuteFiles = incremental && !inputs.options.useFreezer
143
? [...(inputs.files!)]
144
: undefined;
145
146
// file normaliation
147
const normalizeFiles = (targetFiles: string[]) => {
148
return targetFiles.map((file) => {
149
const target = isAbsolute(file) ? file : join(Deno.cwd(), file);
150
if (!existsSync(target)) {
151
throw new Error("Render target does not exist: " + file);
152
}
153
return normalizePath(target);
154
});
155
};
156
157
if (inputs.files) {
158
if (alwaysExecuteFiles) {
159
alwaysExecuteFiles = normalizeFiles(alwaysExecuteFiles);
160
inputs.files = normalizeFiles(inputs.files);
161
} else if (inputs.options.useFreezer) {
162
inputs.files = normalizeFiles(inputs.files);
163
}
164
}
165
166
// check with the project type to see if we should render all
167
// of the files in the project with the freezer enabled (required
168
// for projects that produce self-contained output from a
169
// collection of input files)
170
if (
171
inputs.files && alwaysExecuteFiles &&
172
inputs.projType.incrementalRenderAll &&
173
await inputs.projType.incrementalRenderAll(
174
inputs.context,
175
inputs.options,
176
inputs.files,
177
)
178
) {
179
inputs.files = inputs.context.files.input;
180
inputs.options = { ...inputs.options, useFreezer: true };
181
}
182
183
// some standard pre and post render script env vars
184
const renderAll = !inputs.files ||
185
(inputs.files.length === inputs.context.files.input.length);
186
187
// default for files if not specified
188
inputs.files = inputs.files || inputs.context.files.input;
189
const filesToRender: RenderFile[] = inputs.files.map((file) => {
190
return { path: file };
191
});
192
193
// See if the project type needs to add additional render files
194
// that should be rendered as a side effect of rendering the file(s)
195
// in the render list.
196
// We don't add supplemental files when this is a dev server reload
197
// to improve render performance
198
const projectSupplement = (filesToRender: RenderFile[]) => {
199
if (inputs.projType.supplementRender && !inputs.options.devServerReload) {
200
return inputs.projType.supplementRender(
201
inputs.context,
202
filesToRender,
203
incremental,
204
);
205
} else {
206
return { files: [] };
207
}
208
};
209
const supplements = projectSupplement(filesToRender);
210
filesToRender.push(...supplements.files);
211
212
return {
213
alwaysExecuteFiles,
214
filesToRender,
215
options: inputs.options,
216
supplements,
217
behavior: {
218
renderAll,
219
incremental,
220
},
221
};
222
};
223
224
const getProjectRenderScripts = async (
225
context: ProjectContext,
226
) => {
227
const preRenderScripts: string[] = [],
228
postRenderScripts: string[] = [];
229
if (context.config?.project?.[kProjectPreRender]) {
230
preRenderScripts.push(
231
...asArray(context.config?.project?.[kProjectPreRender]!),
232
);
233
}
234
if (context.config?.project?.[kProjectPostRender]) {
235
postRenderScripts.push(
236
...asArray(context.config?.project?.[kProjectPostRender]!),
237
);
238
}
239
return { preRenderScripts, postRenderScripts };
240
};
241
242
export async function renderProject(
243
context: ProjectContext,
244
pOptions: RenderOptions,
245
pFiles?: string[],
246
): Promise<RenderResult> {
247
const { preRenderScripts, postRenderScripts } = await getProjectRenderScripts(
248
context,
249
);
250
251
// lookup the project type
252
const projType = projectType(context.config?.project?.[kProjectType]);
253
254
const projOutputDir = projectOutputDir(context);
255
256
// get real path to the project
257
const projDir = normalizePath(context.dir);
258
259
let projectRenderConfig = await computeProjectRenderConfig({
260
context,
261
projType,
262
projOutputDir,
263
projDir,
264
options: pOptions,
265
files: pFiles,
266
});
267
268
// ensure we have the requisite entries in .gitignore
269
await ensureGitignore(context.dir);
270
271
// determine whether pre and post render steps should show progress
272
const progress = !!projectRenderConfig.options.progress ||
273
(projectRenderConfig.filesToRender.length > 1);
274
275
// if there is an output dir then remove it if clean is specified
276
if (
277
projectRenderConfig.behavior.renderAll && hasProjectOutputDir(context) &&
278
(projectRenderConfig.options.forceClean ||
279
(projectRenderConfig.options.flags?.clean == true) &&
280
(projType.cleanOutputDir === true))
281
) {
282
// output dir - use safeRemoveDirSync for boundary protection (#13892)
283
const realProjectDir = normalizePath(context.dir);
284
if (existsSync(projOutputDir)) {
285
const realOutputDir = normalizePath(projOutputDir);
286
try {
287
safeRemoveDirSync(realOutputDir, realProjectDir);
288
} catch (e) {
289
if (!(e instanceof UnsafeRemovalError)) {
290
throw e;
291
}
292
// Silently skip if output dir equals or is outside project dir
293
}
294
}
295
// remove index
296
clearProjectIndex(realProjectDir);
297
}
298
299
// Create the environment that needs to be made available to the
300
// pre/post render scripts.
301
const prePostEnv = {
302
"QUARTO_PROJECT_OUTPUT_DIR": projOutputDir,
303
...(projectRenderConfig.behavior.renderAll
304
? { QUARTO_PROJECT_RENDER_ALL: "1" }
305
: {}),
306
};
307
308
// run pre-render step if we are rendering all files
309
if (preRenderScripts.length) {
310
// https://github.com/quarto-dev/quarto-cli/issues/10828
311
// some environments limit the length of environment variables.
312
// It's hard to know in advance what the limit is, so we will
313
// instead ask users to configure their environment with
314
// the names of the files we will write the list of files to.
315
316
const filesToRender = projectRenderConfig.filesToRender
317
.map((fileToRender) => fileToRender.path)
318
.map((file) => relative(projDir, file));
319
const env: Record<string, string> = {
320
...prePostEnv,
321
};
322
323
if (Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES")) {
324
Deno.writeTextFileSync(
325
Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES")!,
326
filesToRender.join("\n"),
327
);
328
} else {
329
env.QUARTO_PROJECT_INPUT_FILES = filesToRender.join("\n");
330
}
331
332
await runPreRender(
333
projDir,
334
preRenderScripts,
335
progress,
336
!!projectRenderConfig.options.flags?.quiet,
337
env,
338
);
339
340
// re-initialize project context
341
context = await projectContextForDirectory(
342
context.dir,
343
context.notebookContext,
344
projectRenderConfig.options,
345
);
346
347
// Validate that certain project properties haven't been mutated
348
noMutationValidations(projType, projOutputDir, projDir).some(
349
(validation) => {
350
if (!ld.isEqual(validation.newVal(context), validation.val)) {
351
throw new Error(
352
`Pre-render script resulted in a project change that is now allowed.\n${validation.msg}`,
353
);
354
}
355
},
356
);
357
358
// Recompute the project render list (filesToRender)
359
projectRenderConfig = await computeProjectRenderConfig({
360
context,
361
projType,
362
projOutputDir,
363
projDir,
364
options: pOptions,
365
files: pFiles,
366
});
367
}
368
369
// lookup the project type and call preRender
370
if (projType.preRender) {
371
await projType.preRender(context);
372
}
373
374
// set execute dir if requested
375
const executeDir = context.config?.project?.[kProjectExecuteDir];
376
if (
377
projectRenderConfig.options.flags?.executeDir === undefined &&
378
executeDir === "project"
379
) {
380
projectRenderConfig.options = {
381
...projectRenderConfig.options,
382
flags: {
383
...projectRenderConfig.options.flags,
384
executeDir: projDir,
385
},
386
};
387
}
388
389
// set executeDaemon to 0 for renders of the entire project
390
// or a list of more than 3 files (don't want to leave dozens of
391
// kernels in memory). we use 3 rather than 1 because w/ blogs
392
// and listings there may be addtional files added to the render list
393
if (
394
projectRenderConfig.filesToRender.length > 3 &&
395
projectRenderConfig.options.flags &&
396
projectRenderConfig.options.flags.executeDaemon === undefined
397
) {
398
projectRenderConfig.options.flags.executeDaemon = 0;
399
}
400
401
// projResults to return
402
const projResults: RenderResult = {
403
context,
404
baseDir: projDir,
405
outputDir: relative(projDir, projOutputDir),
406
files: [],
407
};
408
409
// determine the output dir
410
const outputDir = projResults.outputDir;
411
const outputDirAbsolute = outputDir ? join(projDir, outputDir) : undefined;
412
if (outputDirAbsolute) {
413
ensureDirSync(outputDirAbsolute);
414
}
415
416
// track the lib dir
417
const libDir = context.config?.project[kProjectLibDir];
418
419
// function to extract resource files from rendered file
420
const resourcesFrom = async (file: RenderedFile) => {
421
// resource files
422
const partitioned = await partitionedMarkdownForInput(
423
context,
424
file.input,
425
);
426
const excludeDirs = context ? projectExcludeDirs(context) : [];
427
428
const resourceFiles = resourceFilesFromRenderedFile(
429
projDir,
430
excludeDirs,
431
file,
432
partitioned,
433
);
434
return resourceFiles;
435
};
436
437
// render the files
438
const fileResults = await renderFiles(
439
projectRenderConfig.filesToRender,
440
projectRenderConfig.options,
441
context.notebookContext,
442
projectRenderConfig.alwaysExecuteFiles,
443
projType?.pandocRenderer
444
? projType.pandocRenderer(projectRenderConfig.options, context)
445
: undefined,
446
context,
447
);
448
449
const directoryRelocator = (destinationDir: string) => {
450
// move or copy dir
451
return (dir: string, copy = false) => {
452
const targetDir = join(destinationDir, dir);
453
const srcDir = join(projDir, dir);
454
// Dont' remove the directory unless there is a source
455
// directory that we can relocate
456
//
457
// If we intend for the directory relocated to be used to
458
// remove directories, we should instead make a function that
459
// does that explicitly, rather than as a side effect of a missing
460
// src Dir
461
if (!existsSync(srcDir)) {
462
return;
463
}
464
if (existsSync(targetDir)) {
465
try {
466
safeRemoveDirSync(targetDir, context.dir);
467
} catch (e) {
468
if (e instanceof UnsafeRemovalError) {
469
warning(
470
`Refusing to remove directory ${targetDir} since it is not a subdirectory of the main project directory.`,
471
);
472
warning(
473
`Quarto did not expect the path configuration being used in this project, and strange behavior may result.`,
474
);
475
}
476
}
477
}
478
ensureDirSync(dirname(targetDir));
479
if (copy) {
480
copyTo(srcDir, targetDir);
481
} else {
482
try {
483
Deno.renameSync(srcDir, targetDir);
484
} catch (_e) {
485
// if renaming failed, it could have happened
486
// because src and target are in different file systems.
487
// In that case, try to recursively copy from src
488
copyTo(srcDir, targetDir);
489
safeRemoveDirSync(srcDir, context.dir);
490
}
491
}
492
};
493
};
494
495
let moveOutputResult: Record<string, unknown> | undefined;
496
if (outputDirAbsolute) {
497
// track whether we need to keep the lib dir around
498
let keepLibsDir = false;
499
500
interface FileOperation {
501
key: string;
502
src: string;
503
performOperation: () => void;
504
}
505
const fileOperations: FileOperation[] = [];
506
507
// move/copy projResults to output_dir
508
for (let i = 0; i < fileResults.files.length; i++) {
509
const renderedFile = fileResults.files[i];
510
511
const formatOutputDir = projectFormatOutputDir(
512
renderedFile.format,
513
context,
514
projectType(context.config?.project.type),
515
);
516
517
const formatRelocateDir = directoryRelocator(formatOutputDir);
518
const moveFormatDir = formatRelocateDir;
519
const copyFormatDir = (dir: string) => formatRelocateDir(dir, true);
520
521
// move the renderedFile to the output dir
522
if (!renderedFile.isTransient) {
523
const outputFile = join(formatOutputDir, renderedFile.file);
524
ensureDirSync(dirname(outputFile));
525
safeMoveSync(join(projDir, renderedFile.file), outputFile);
526
}
527
528
// files dir
529
const keepFiles = !!renderedFile.format.execute[kKeepMd] ||
530
!!renderedFile.format.render[kKeepTex] ||
531
!!renderedFile.format.render[kKeepTyp];
532
keepLibsDir = keepLibsDir || keepFiles;
533
if (renderedFile.supporting) {
534
// lib-dir is handled separately for projects so filter it out of supporting
535
renderedFile.supporting = renderedFile.supporting.filter((file) =>
536
file !== libDir
537
);
538
// ensure that we don't have overlapping paths in supporting
539
renderedFile.supporting = renderedFile.supporting.filter((file) => {
540
return !renderedFile.supporting!.some((dir) =>
541
file.startsWith(dir) && file !== dir
542
);
543
});
544
if (keepFiles) {
545
renderedFile.supporting.forEach((file) => {
546
fileOperations.push({
547
key: `${file}|copy`,
548
src: file,
549
performOperation: () => {
550
copyFormatDir(file);
551
},
552
});
553
});
554
} else {
555
renderedFile.supporting.forEach((file) => {
556
fileOperations.push({
557
key: `${file}|move`,
558
src: file,
559
performOperation: () => {
560
moveFormatDir(file);
561
removeIfEmptyDir(dirname(file));
562
},
563
});
564
});
565
}
566
}
567
568
// remove empty files dir
569
if (!keepFiles) {
570
const filesDir = join(
571
projDir,
572
dirname(renderedFile.file),
573
inputFilesDir(renderedFile.file),
574
);
575
removeIfEmptyDir(filesDir);
576
}
577
578
// render file renderedFile
579
projResults.files.push({
580
isTransient: renderedFile.isTransient,
581
input: renderedFile.input,
582
markdown: renderedFile.markdown,
583
format: renderedFile.format,
584
file: renderedFile.file,
585
supporting: renderedFile.supporting,
586
resourceFiles: await resourcesFrom(renderedFile),
587
});
588
}
589
590
// Sort the operations in order from shallowest to deepest
591
// This means that parent directories will happen first (so for example
592
// foo_files will happen before foo_files/figure-html). This is
593
// desirable because if the order of operations is something like:
594
//
595
// foo_files/figure-html (move)
596
// foo_files (move)
597
//
598
// The second operation overwrites the folder foo_files with a copy that is
599
// missing the figure_html directory. (Render a document to JATS and HTML
600
// as an example case)
601
const uniqOps = ld.uniqBy(fileOperations, (op: FileOperation) => {
602
return op.key;
603
}) as FileOperation[];
604
605
const sortedOperations = uniqOps.sort((a, b) => {
606
if (a.src === b.src) {
607
return 0;
608
}
609
if (isSubdir(a.src, b.src)) {
610
return -1;
611
}
612
return a.src.localeCompare(b.src);
613
});
614
615
// Before file move
616
if (projType.beforeMoveOutput) {
617
moveOutputResult = await projType.beforeMoveOutput(
618
context,
619
projResults.files,
620
);
621
}
622
623
sortedOperations.forEach((op) => {
624
op.performOperation();
625
});
626
627
// move or copy the lib dir if we have one (move one subdirectory at a time
628
// so that we can merge with what's already there)
629
if (libDir) {
630
const libDirFull = join(context.dir, libDir);
631
if (existsSync(libDirFull)) {
632
// if this is an incremental render or we are uzing the freezer, then
633
// copy lib dirs incrementally (don't replace the whole directory).
634
// otherwise, replace the whole thing so we get a clean start
635
const libsIncremental = !!(projectRenderConfig.behavior.incremental ||
636
projectRenderConfig.options.useFreezer);
637
638
// determine format lib dirs (for pruning)
639
const formatLibDirs = projType.formatLibDirs
640
? projType.formatLibDirs()
641
: [];
642
643
// lib dir to freezer
644
const freezeLibDir = (hidden: boolean) => {
645
copyToProjectFreezer(context, libDir, hidden, false);
646
pruneProjectFreezerDir(context, libDir, formatLibDirs, hidden);
647
pruneProjectFreezer(context, hidden);
648
};
649
650
// copy to hidden freezer
651
freezeLibDir(true);
652
653
// if we have a visible freezer then copy to it as well
654
if (existsSync(join(context.dir, kProjectFreezeDir))) {
655
freezeLibDir(false);
656
}
657
658
if (libsIncremental) {
659
for (const lib of Deno.readDirSync(libDirFull)) {
660
if (lib.isDirectory) {
661
const copyDir = join(libDir, lib.name);
662
const srcDir = join(projDir, copyDir);
663
const targetDir = join(outputDirAbsolute, copyDir);
664
copyMinimal(srcDir, targetDir);
665
if (!keepLibsDir) {
666
safeRemoveIfExists(srcDir);
667
}
668
}
669
}
670
if (!keepLibsDir) {
671
safeRemoveIfExists(libDirFull);
672
}
673
} else {
674
// move or copy dir
675
const relocateDir = directoryRelocator(outputDirAbsolute);
676
if (keepLibsDir) {
677
relocateDir(libDir, true);
678
} else {
679
relocateDir(libDir);
680
}
681
}
682
}
683
}
684
685
// determine the output files and filter them out of the resourceFiles
686
const outputFiles = projResults.files.map((result) =>
687
join(projDir, result.file)
688
);
689
projResults.files.forEach((file) => {
690
file.resourceFiles = file.resourceFiles.filter((resource) =>
691
!outputFiles.includes(resource)
692
);
693
});
694
695
// Expand the resources into the format aware targets
696
// srcPath -> Set<destinationPaths>
697
const resourceFilesToCopy: Record<string, Set<string>> = {};
698
699
const projectFormats: Record<string, Format> = {};
700
projResults.files.forEach((file) => {
701
if (
702
file.format.identifier[kTargetFormat] &&
703
projectFormats[file.format.identifier[kTargetFormat]] === undefined
704
) {
705
projectFormats[file.format.identifier[kTargetFormat]] = file.format;
706
}
707
});
708
709
const isSelfContainedOutput = (format: Format) => {
710
return projType.selfContainedOutput &&
711
projType.selfContainedOutput(format);
712
};
713
714
Object.values(projectFormats).forEach((format) => {
715
// Don't copy resource files if the project produces a self-contained output
716
if (isSelfContainedOutput(format)) {
717
return;
718
}
719
720
// Process the project resources
721
const formatOutputDir = projectFormatOutputDir(
722
format,
723
context,
724
projType,
725
);
726
context.files.resources?.forEach((resource) => {
727
resourceFilesToCopy[resource] = resourceFilesToCopy[resource] ||
728
new Set();
729
const relativePath = relative(context.dir, resource);
730
resourceFilesToCopy[resource].add(
731
join(formatOutputDir, relativePath),
732
);
733
});
734
});
735
736
// Process the resources provided by the files themselves
737
projResults.files.forEach((file) => {
738
// Don't copy resource files if the project produces a self-contained output
739
if (isSelfContainedOutput(file.format)) {
740
return;
741
}
742
743
const formatOutputDir = projectFormatOutputDir(
744
file.format,
745
context,
746
projType,
747
);
748
file.resourceFiles.forEach((file) => {
749
resourceFilesToCopy[file] = resourceFilesToCopy[file] || new Set();
750
const relativePath = relative(projDir, file);
751
resourceFilesToCopy[file].add(join(formatOutputDir, relativePath));
752
});
753
});
754
755
// Actually copy the resource files
756
Object.keys(resourceFilesToCopy).forEach((srcPath) => {
757
const destinationFiles = resourceFilesToCopy[srcPath];
758
destinationFiles.forEach((destPath: string) => {
759
if (existsSync(srcPath)) {
760
if (Deno.statSync(srcPath).isFile) {
761
copyResourceFile(context.dir, srcPath, destPath);
762
}
763
} else if (!existsSync(destPath)) {
764
warning(`File '${srcPath}' was not found.`);
765
}
766
});
767
});
768
} else {
769
for (const result of fileResults.files) {
770
const resourceFiles = await resourcesFrom(result);
771
projResults.files.push({
772
input: result.input,
773
markdown: result.markdown,
774
format: result.format,
775
file: result.file,
776
supporting: result.supporting,
777
resourceFiles,
778
});
779
}
780
}
781
782
// forward error to projResults
783
projResults.error = fileResults.error;
784
785
// call engine and project post-render
786
if (!projResults.error) {
787
// engine post-render
788
for (const file of projResults.files) {
789
const path = join(context.dir, file.input);
790
const engine = await fileExecutionEngine(
791
path,
792
projectRenderConfig.options.flags,
793
context,
794
);
795
if (engine?.postRender) {
796
await engine.postRender(file);
797
}
798
}
799
800
// compute output files
801
const outputFiles = projResults.files
802
.filter((x) => !x.isTransient)
803
.map((result) => {
804
const outputDir = projectFormatOutputDir(
805
result.format,
806
context,
807
projType,
808
);
809
const file = outputDir
810
? join(outputDir, result.file)
811
: join(projDir, result.file);
812
return {
813
file,
814
input: join(projDir, result.input),
815
format: result.format,
816
resources: result.resourceFiles,
817
supporting: result.supporting,
818
};
819
});
820
821
if (projType.postRender) {
822
await projType.postRender(
823
context,
824
projectRenderConfig.behavior.incremental,
825
outputFiles,
826
moveOutputResult,
827
);
828
}
829
830
// run post-render if this isn't incremental
831
if (postRenderScripts.length) {
832
// https://github.com/quarto-dev/quarto-cli/issues/10828
833
// some environments limit the length of environment variables.
834
// It's hard to know in advance what the limit is, so we will
835
// instead ask users to configure their environment with
836
// the names of the files we will write the list of files to.
837
838
const env: Record<string, string> = {
839
...prePostEnv,
840
};
841
842
if (Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES")) {
843
Deno.writeTextFileSync(
844
Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES")!,
845
outputFiles.map((outputFile) => relative(projDir, outputFile.file))
846
.join("\n"),
847
);
848
} else {
849
env.QUARTO_PROJECT_OUTPUT_FILES = outputFiles
850
.map((outputFile) => relative(projDir, outputFile.file))
851
.join("\n");
852
}
853
854
await runPostRender(
855
projDir,
856
postRenderScripts,
857
progress,
858
!!projectRenderConfig.options.flags?.quiet,
859
env,
860
);
861
}
862
}
863
864
// Mark any rendered files as supplemental if that
865
// is how they got into the render list
866
const supplements = projectRenderConfig.supplements;
867
projResults.files.forEach((file) => {
868
if (
869
supplements.files.find((supFile) => {
870
return supFile.path === join(projDir, file.input);
871
})
872
) {
873
file.supplemental = true;
874
}
875
});
876
877
// Also let the project know that the render has completed for
878
// any non supplemental files
879
const nonSupplementalFiles = projResults.files.filter((file) =>
880
!file.supplemental
881
).map((file) => file.file);
882
if (supplements.onRenderComplete) {
883
await supplements.onRenderComplete(
884
context,
885
nonSupplementalFiles,
886
projectRenderConfig.behavior.incremental,
887
);
888
}
889
890
// Clean up synthetic project created for --output-dir
891
// When --output-dir is used without a project file, we create a temporary
892
// project context with a .quarto directory (see render-shared.ts).
893
// After rendering completes, we must remove this directory to avoid leaving
894
// debris in non-project directories (#9745).
895
//
896
// Critical ordering for Windows: Close file handles BEFORE removing directory
897
// to avoid "The process cannot access the file because it is being used by
898
// another process" (os error 32) (#13625).
899
if (projectRenderConfig.options.forceClean) {
900
// 1. Close all file handles (KV database, temp context, etc.)
901
context.cleanup();
902
903
// 2. Remove the temporary .quarto directory
904
const scratchDir = join(projDir, kQuartoScratch);
905
if (existsSync(scratchDir)) {
906
safeRemoveSync(scratchDir, { recursive: true });
907
}
908
}
909
910
return projResults;
911
}
912
913
async function runPreRender(
914
projDir: string,
915
preRender: string[],
916
progress: boolean,
917
quiet: boolean,
918
env?: { [key: string]: string },
919
) {
920
await runScripts(projDir, preRender, progress, quiet, env);
921
}
922
923
async function runPostRender(
924
projDir: string,
925
postRender: string[],
926
progress: boolean,
927
quiet: boolean,
928
env?: { [key: string]: string },
929
) {
930
await runScripts(projDir, postRender, progress, quiet, env);
931
}
932
933
async function runScripts(
934
projDir: string,
935
scripts: string[],
936
progress: boolean,
937
quiet: boolean,
938
env?: { [key: string]: string },
939
) {
940
for (let i = 0; i < scripts.length; i++) {
941
const args = parseShellRunCommand(scripts[i]);
942
const script = args[0];
943
944
if (progress && !quiet) {
945
info(colors.bold(colors.blue(`${script}`)));
946
}
947
948
const handler = handlerForScript(script);
949
if (handler) {
950
if (env) {
951
env = {
952
...env,
953
};
954
} else {
955
env = {};
956
}
957
if (!env) throw new Error("should never get here");
958
const input = Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES");
959
const output = Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES");
960
if (input) {
961
env["QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES"] = input;
962
}
963
if (output) {
964
env["QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES"] = output;
965
}
966
967
const result = await handler.run(script, args.splice(1), undefined, {
968
cwd: projDir,
969
stdout: quiet ? "piped" : "inherit",
970
env,
971
});
972
if (!result.success) {
973
throw new Error();
974
}
975
} else {
976
const result = await execProcess({
977
cmd: args[0],
978
args: args.slice(1),
979
cwd: projDir,
980
stdout: quiet ? "piped" : "inherit",
981
env,
982
});
983
if (!result.success) {
984
throw new Error();
985
}
986
}
987
}
988
if (scripts.length > 0) {
989
info("");
990
}
991
}
992
993