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