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
3584 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
removeIfExists,
63
safeRemoveIfExists,
64
} from "../../core/path.ts";
65
import { handlerForScript } from "../../core/run/run.ts";
66
import { execProcess } from "../../core/process.ts";
67
import { parseShellRunCommand } from "../../core/run/shell.ts";
68
import { clearProjectIndex } from "../../project/project-index.ts";
69
import {
70
hasProjectOutputDir,
71
projectExcludeDirs,
72
projectFormatOutputDir,
73
projectOutputDir,
74
} from "../../project/project-shared.ts";
75
import { asArray } from "../../core/array.ts";
76
import { normalizePath } from "../../core/path.ts";
77
import { isSubdir } from "../../deno_ral/fs.ts";
78
import { Format } from "../../config/types.ts";
79
import { fileExecutionEngine } from "../../execute/engine.ts";
80
import { projectContextForDirectory } from "../../project/project-context.ts";
81
import { ProjectType } from "../../project/types/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
284
const realProjectDir = normalizePath(context.dir);
285
if (existsSync(projOutputDir)) {
286
const realOutputDir = normalizePath(projOutputDir);
287
if (
288
(realOutputDir !== realProjectDir) &&
289
realOutputDir.startsWith(realProjectDir)
290
) {
291
removeIfExists(realOutputDir);
292
}
293
}
294
// remove index
295
clearProjectIndex(realProjectDir);
296
}
297
298
// Create the environment that needs to be made available to the
299
// pre/post render scripts.
300
const prePostEnv = {
301
"QUARTO_PROJECT_OUTPUT_DIR": projOutputDir,
302
...(projectRenderConfig.behavior.renderAll
303
? { QUARTO_PROJECT_RENDER_ALL: "1" }
304
: {}),
305
};
306
307
// run pre-render step if we are rendering all files
308
if (preRenderScripts.length) {
309
// https://github.com/quarto-dev/quarto-cli/issues/10828
310
// some environments limit the length of environment variables.
311
// It's hard to know in advance what the limit is, so we will
312
// instead ask users to configure their environment with
313
// the names of the files we will write the list of files to.
314
315
const filesToRender = projectRenderConfig.filesToRender
316
.map((fileToRender) => fileToRender.path)
317
.map((file) => relative(projDir, file));
318
const env: Record<string, string> = {
319
...prePostEnv,
320
};
321
322
if (Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES")) {
323
Deno.writeTextFileSync(
324
Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES")!,
325
filesToRender.join("\n"),
326
);
327
} else {
328
env.QUARTO_PROJECT_INPUT_FILES = filesToRender.join("\n");
329
}
330
331
await runPreRender(
332
projDir,
333
preRenderScripts,
334
progress,
335
!!projectRenderConfig.options.flags?.quiet,
336
env,
337
);
338
339
// re-initialize project context
340
context = await projectContextForDirectory(
341
context.dir,
342
context.notebookContext,
343
projectRenderConfig.options,
344
);
345
346
// Validate that certain project properties haven't been mutated
347
noMutationValidations(projType, projOutputDir, projDir).some(
348
(validation) => {
349
if (!ld.isEqual(validation.newVal(context), validation.val)) {
350
throw new Error(
351
`Pre-render script resulted in a project change that is now allowed.\n${validation.msg}`,
352
);
353
}
354
},
355
);
356
357
// Recompute the project render list (filesToRender)
358
projectRenderConfig = await computeProjectRenderConfig({
359
context,
360
projType,
361
projOutputDir,
362
projDir,
363
options: pOptions,
364
files: pFiles,
365
});
366
}
367
368
// lookup the project type and call preRender
369
if (projType.preRender) {
370
await projType.preRender(context);
371
}
372
373
// set execute dir if requested
374
const executeDir = context.config?.project?.[kProjectExecuteDir];
375
if (
376
projectRenderConfig.options.flags?.executeDir === undefined &&
377
executeDir === "project"
378
) {
379
projectRenderConfig.options = {
380
...projectRenderConfig.options,
381
flags: {
382
...projectRenderConfig.options.flags,
383
executeDir: projDir,
384
},
385
};
386
}
387
388
// set executeDaemon to 0 for renders of the entire project
389
// or a list of more than 3 files (don't want to leave dozens of
390
// kernels in memory). we use 3 rather than 1 because w/ blogs
391
// and listings there may be addtional files added to the render list
392
if (
393
projectRenderConfig.filesToRender.length > 3 &&
394
projectRenderConfig.options.flags &&
395
projectRenderConfig.options.flags.executeDaemon === undefined
396
) {
397
projectRenderConfig.options.flags.executeDaemon = 0;
398
}
399
400
// projResults to return
401
const projResults: RenderResult = {
402
context,
403
baseDir: projDir,
404
outputDir: relative(projDir, projOutputDir),
405
files: [],
406
};
407
408
// determine the output dir
409
const outputDir = projResults.outputDir;
410
const outputDirAbsolute = outputDir ? join(projDir, outputDir) : undefined;
411
if (outputDirAbsolute) {
412
ensureDirSync(outputDirAbsolute);
413
}
414
415
// track the lib dir
416
const libDir = context.config?.project[kProjectLibDir];
417
418
// function to extract resource files from rendered file
419
const resourcesFrom = async (file: RenderedFile) => {
420
// resource files
421
const partitioned = await partitionedMarkdownForInput(
422
context,
423
file.input,
424
);
425
const excludeDirs = context ? projectExcludeDirs(context) : [];
426
427
const resourceFiles = resourceFilesFromRenderedFile(
428
projDir,
429
excludeDirs,
430
file,
431
partitioned,
432
);
433
return resourceFiles;
434
};
435
436
// render the files
437
const fileResults = await renderFiles(
438
projectRenderConfig.filesToRender,
439
projectRenderConfig.options,
440
context.notebookContext,
441
projectRenderConfig.alwaysExecuteFiles,
442
projType?.pandocRenderer
443
? projType.pandocRenderer(projectRenderConfig.options, context)
444
: undefined,
445
context,
446
);
447
448
const directoryRelocator = (destinationDir: string) => {
449
// move or copy dir
450
return (dir: string, copy = false) => {
451
const targetDir = join(destinationDir, dir);
452
const srcDir = join(projDir, dir);
453
// Dont' remove the directory unless there is a source
454
// directory that we can relocate
455
//
456
// If we intend for the directory relocated to be used to
457
// remove directories, we should instead make a function that
458
// does that explicitly, rather than as a side effect of a missing
459
// src Dir
460
if (!existsSync(srcDir)) {
461
return;
462
}
463
if (existsSync(targetDir)) {
464
try {
465
safeRemoveDirSync(targetDir, context.dir);
466
} catch (e) {
467
if (e instanceof UnsafeRemovalError) {
468
warning(
469
`Refusing to remove directory ${targetDir} since it is not a subdirectory of the main project directory.`,
470
);
471
warning(
472
`Quarto did not expect the path configuration being used in this project, and strange behavior may result.`,
473
);
474
}
475
}
476
}
477
ensureDirSync(dirname(targetDir));
478
if (copy) {
479
copyTo(srcDir, targetDir);
480
} else {
481
try {
482
Deno.renameSync(srcDir, targetDir);
483
} catch (_e) {
484
// if renaming failed, it could have happened
485
// because src and target are in different file systems.
486
// In that case, try to recursively copy from src
487
copyTo(srcDir, targetDir);
488
safeRemoveDirSync(srcDir, context.dir);
489
}
490
}
491
};
492
};
493
494
let moveOutputResult: Record<string, unknown> | undefined;
495
if (outputDirAbsolute) {
496
// track whether we need to keep the lib dir around
497
let keepLibsDir = false;
498
499
interface FileOperation {
500
key: string;
501
src: string;
502
performOperation: () => void;
503
}
504
const fileOperations: FileOperation[] = [];
505
506
// move/copy projResults to output_dir
507
for (let i = 0; i < fileResults.files.length; i++) {
508
const renderedFile = fileResults.files[i];
509
510
const formatOutputDir = projectFormatOutputDir(
511
renderedFile.format,
512
context,
513
projectType(context.config?.project.type),
514
);
515
516
const formatRelocateDir = directoryRelocator(formatOutputDir);
517
const moveFormatDir = formatRelocateDir;
518
const copyFormatDir = (dir: string) => formatRelocateDir(dir, true);
519
520
// move the renderedFile to the output dir
521
if (!renderedFile.isTransient) {
522
const outputFile = join(formatOutputDir, renderedFile.file);
523
ensureDirSync(dirname(outputFile));
524
safeMoveSync(join(projDir, renderedFile.file), outputFile);
525
}
526
527
// files dir
528
const keepFiles = !!renderedFile.format.execute[kKeepMd] ||
529
!!renderedFile.format.render[kKeepTex] ||
530
!!renderedFile.format.render[kKeepTyp];
531
keepLibsDir = keepLibsDir || keepFiles;
532
if (renderedFile.supporting) {
533
// lib-dir is handled separately for projects so filter it out of supporting
534
renderedFile.supporting = renderedFile.supporting.filter((file) =>
535
file !== libDir
536
);
537
// ensure that we don't have overlapping paths in supporting
538
renderedFile.supporting = renderedFile.supporting.filter((file) => {
539
return !renderedFile.supporting!.some((dir) =>
540
file.startsWith(dir) && file !== dir
541
);
542
});
543
if (keepFiles) {
544
renderedFile.supporting.forEach((file) => {
545
fileOperations.push({
546
key: `${file}|copy`,
547
src: file,
548
performOperation: () => {
549
copyFormatDir(file);
550
},
551
});
552
});
553
} else {
554
renderedFile.supporting.forEach((file) => {
555
fileOperations.push({
556
key: `${file}|move`,
557
src: file,
558
performOperation: () => {
559
moveFormatDir(file);
560
removeIfEmptyDir(dirname(file));
561
},
562
});
563
});
564
}
565
}
566
567
// remove empty files dir
568
if (!keepFiles) {
569
const filesDir = join(
570
projDir,
571
dirname(renderedFile.file),
572
inputFilesDir(renderedFile.file),
573
);
574
removeIfEmptyDir(filesDir);
575
}
576
577
// render file renderedFile
578
projResults.files.push({
579
isTransient: renderedFile.isTransient,
580
input: renderedFile.input,
581
markdown: renderedFile.markdown,
582
format: renderedFile.format,
583
file: renderedFile.file,
584
supporting: renderedFile.supporting,
585
resourceFiles: await resourcesFrom(renderedFile),
586
});
587
}
588
589
// Sort the operations in order from shallowest to deepest
590
// This means that parent directories will happen first (so for example
591
// foo_files will happen before foo_files/figure-html). This is
592
// desirable because if the order of operations is something like:
593
//
594
// foo_files/figure-html (move)
595
// foo_files (move)
596
//
597
// The second operation overwrites the folder foo_files with a copy that is
598
// missing the figure_html directory. (Render a document to JATS and HTML
599
// as an example case)
600
const uniqOps = ld.uniqBy(fileOperations, (op: FileOperation) => {
601
return op.key;
602
});
603
604
const sortedOperations = uniqOps.sort((a, b) => {
605
if (a.src === b.src) {
606
return 0;
607
}
608
if (isSubdir(a.src, b.src)) {
609
return -1;
610
}
611
return a.src.localeCompare(b.src);
612
});
613
614
// Before file move
615
if (projType.beforeMoveOutput) {
616
moveOutputResult = await projType.beforeMoveOutput(
617
context,
618
projResults.files,
619
);
620
}
621
622
sortedOperations.forEach((op) => {
623
op.performOperation();
624
});
625
626
// move or copy the lib dir if we have one (move one subdirectory at a time
627
// so that we can merge with what's already there)
628
if (libDir) {
629
const libDirFull = join(context.dir, libDir);
630
if (existsSync(libDirFull)) {
631
// if this is an incremental render or we are uzing the freezer, then
632
// copy lib dirs incrementally (don't replace the whole directory).
633
// otherwise, replace the whole thing so we get a clean start
634
const libsIncremental = !!(projectRenderConfig.behavior.incremental ||
635
projectRenderConfig.options.useFreezer);
636
637
// determine format lib dirs (for pruning)
638
const formatLibDirs = projType.formatLibDirs
639
? projType.formatLibDirs()
640
: [];
641
642
// lib dir to freezer
643
const freezeLibDir = (hidden: boolean) => {
644
copyToProjectFreezer(context, libDir, hidden, false);
645
pruneProjectFreezerDir(context, libDir, formatLibDirs, hidden);
646
pruneProjectFreezer(context, hidden);
647
};
648
649
// copy to hidden freezer
650
freezeLibDir(true);
651
652
// if we have a visible freezer then copy to it as well
653
if (existsSync(join(context.dir, kProjectFreezeDir))) {
654
freezeLibDir(false);
655
}
656
657
if (libsIncremental) {
658
for (const lib of Deno.readDirSync(libDirFull)) {
659
if (lib.isDirectory) {
660
const copyDir = join(libDir, lib.name);
661
const srcDir = join(projDir, copyDir);
662
const targetDir = join(outputDirAbsolute, copyDir);
663
copyMinimal(srcDir, targetDir);
664
if (!keepLibsDir) {
665
safeRemoveIfExists(srcDir);
666
}
667
}
668
}
669
if (!keepLibsDir) {
670
safeRemoveIfExists(libDirFull);
671
}
672
} else {
673
// move or copy dir
674
const relocateDir = directoryRelocator(outputDirAbsolute);
675
if (keepLibsDir) {
676
relocateDir(libDir, true);
677
} else {
678
relocateDir(libDir);
679
}
680
}
681
}
682
}
683
684
// determine the output files and filter them out of the resourceFiles
685
const outputFiles = projResults.files.map((result) =>
686
join(projDir, result.file)
687
);
688
projResults.files.forEach((file) => {
689
file.resourceFiles = file.resourceFiles.filter((resource) =>
690
!outputFiles.includes(resource)
691
);
692
});
693
694
// Expand the resources into the format aware targets
695
// srcPath -> Set<destinationPaths>
696
const resourceFilesToCopy: Record<string, Set<string>> = {};
697
698
const projectFormats: Record<string, Format> = {};
699
projResults.files.forEach((file) => {
700
if (
701
file.format.identifier[kTargetFormat] &&
702
projectFormats[file.format.identifier[kTargetFormat]] === undefined
703
) {
704
projectFormats[file.format.identifier[kTargetFormat]] = file.format;
705
}
706
});
707
708
const isSelfContainedOutput = (format: Format) => {
709
return projType.selfContainedOutput &&
710
projType.selfContainedOutput(format);
711
};
712
713
Object.values(projectFormats).forEach((format) => {
714
// Don't copy resource files if the project produces a self-contained output
715
if (isSelfContainedOutput(format)) {
716
return;
717
}
718
719
// Process the project resources
720
const formatOutputDir = projectFormatOutputDir(
721
format,
722
context,
723
projType,
724
);
725
context.files.resources?.forEach((resource) => {
726
resourceFilesToCopy[resource] = resourceFilesToCopy[resource] ||
727
new Set();
728
const relativePath = relative(context.dir, resource);
729
resourceFilesToCopy[resource].add(
730
join(formatOutputDir, relativePath),
731
);
732
});
733
});
734
735
// Process the resources provided by the files themselves
736
projResults.files.forEach((file) => {
737
// Don't copy resource files if the project produces a self-contained output
738
if (isSelfContainedOutput(file.format)) {
739
return;
740
}
741
742
const formatOutputDir = projectFormatOutputDir(
743
file.format,
744
context,
745
projType,
746
);
747
file.resourceFiles.forEach((file) => {
748
resourceFilesToCopy[file] = resourceFilesToCopy[file] || new Set();
749
const relativePath = relative(projDir, file);
750
resourceFilesToCopy[file].add(join(formatOutputDir, relativePath));
751
});
752
});
753
754
// Actually copy the resource files
755
Object.keys(resourceFilesToCopy).forEach((srcPath) => {
756
const destinationFiles = resourceFilesToCopy[srcPath];
757
destinationFiles.forEach((destPath: string) => {
758
if (existsSync(srcPath)) {
759
if (Deno.statSync(srcPath).isFile) {
760
copyResourceFile(context.dir, srcPath, destPath);
761
}
762
} else if (!existsSync(destPath)) {
763
warning(`File '${srcPath}' was not found.`);
764
}
765
});
766
});
767
} else {
768
for (const result of fileResults.files) {
769
const resourceFiles = await resourcesFrom(result);
770
projResults.files.push({
771
input: result.input,
772
markdown: result.markdown,
773
format: result.format,
774
file: result.file,
775
supporting: result.supporting,
776
resourceFiles,
777
});
778
}
779
}
780
781
// forward error to projResults
782
projResults.error = fileResults.error;
783
784
// call engine and project post-render
785
if (!projResults.error) {
786
// engine post-render
787
for (const file of projResults.files) {
788
const path = join(context.dir, file.input);
789
const engine = await fileExecutionEngine(
790
path,
791
projectRenderConfig.options.flags,
792
context,
793
);
794
if (engine?.postRender) {
795
await engine.postRender(file, projResults.context);
796
}
797
}
798
799
// compute output files
800
const outputFiles = projResults.files
801
.filter((x) => !x.isTransient)
802
.map((result) => {
803
const outputDir = projectFormatOutputDir(
804
result.format,
805
context,
806
projType,
807
);
808
const file = outputDir
809
? join(outputDir, result.file)
810
: join(projDir, result.file);
811
return {
812
file,
813
input: join(projDir, result.input),
814
format: result.format,
815
resources: result.resourceFiles,
816
supporting: result.supporting,
817
};
818
});
819
820
if (projType.postRender) {
821
await projType.postRender(
822
context,
823
projectRenderConfig.behavior.incremental,
824
outputFiles,
825
moveOutputResult,
826
);
827
}
828
829
// run post-render if this isn't incremental
830
if (postRenderScripts.length) {
831
// https://github.com/quarto-dev/quarto-cli/issues/10828
832
// some environments limit the length of environment variables.
833
// It's hard to know in advance what the limit is, so we will
834
// instead ask users to configure their environment with
835
// the names of the files we will write the list of files to.
836
837
const env: Record<string, string> = {
838
...prePostEnv,
839
};
840
841
if (Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES")) {
842
Deno.writeTextFileSync(
843
Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES")!,
844
outputFiles.map((outputFile) => relative(projDir, outputFile.file))
845
.join("\n"),
846
);
847
} else {
848
env.QUARTO_PROJECT_OUTPUT_FILES = outputFiles
849
.map((outputFile) => relative(projDir, outputFile.file))
850
.join("\n");
851
}
852
853
await runPostRender(
854
projDir,
855
postRenderScripts,
856
progress,
857
!!projectRenderConfig.options.flags?.quiet,
858
env,
859
);
860
}
861
}
862
863
// Mark any rendered files as supplemental if that
864
// is how they got into the render list
865
const supplements = projectRenderConfig.supplements;
866
projResults.files.forEach((file) => {
867
if (
868
supplements.files.find((supFile) => {
869
return supFile.path === join(projDir, file.input);
870
})
871
) {
872
file.supplemental = true;
873
}
874
});
875
876
// Also let the project know that the render has completed for
877
// any non supplemental files
878
const nonSupplementalFiles = projResults.files.filter((file) =>
879
!file.supplemental
880
).map((file) => file.file);
881
if (supplements.onRenderComplete) {
882
await supplements.onRenderComplete(
883
context,
884
nonSupplementalFiles,
885
projectRenderConfig.behavior.incremental,
886
);
887
}
888
889
// in addition to the cleanup above, if forceClean is set, we need to clean up the project scratch dir
890
// entirely. See options.forceClean in render-shared.ts
891
// .quarto is really a fiction created because of `--output-dir` being set on non-project
892
// renders
893
//
894
// cf https://github.com/quarto-dev/quarto-cli/issues/9745#issuecomment-2125951545
895
if (projectRenderConfig.options.forceClean) {
896
const scratchDir = join(projDir, kQuartoScratch);
897
if (existsSync(scratchDir)) {
898
safeRemoveSync(scratchDir, { recursive: true });
899
}
900
}
901
902
return projResults;
903
}
904
905
async function runPreRender(
906
projDir: string,
907
preRender: string[],
908
progress: boolean,
909
quiet: boolean,
910
env?: { [key: string]: string },
911
) {
912
await runScripts(projDir, preRender, progress, quiet, env);
913
}
914
915
async function runPostRender(
916
projDir: string,
917
postRender: string[],
918
progress: boolean,
919
quiet: boolean,
920
env?: { [key: string]: string },
921
) {
922
await runScripts(projDir, postRender, progress, quiet, env);
923
}
924
925
async function runScripts(
926
projDir: string,
927
scripts: string[],
928
progress: boolean,
929
quiet: boolean,
930
env?: { [key: string]: string },
931
) {
932
for (let i = 0; i < scripts.length; i++) {
933
const args = parseShellRunCommand(scripts[i]);
934
const script = args[0];
935
936
if (progress && !quiet) {
937
info(colors.bold(colors.blue(`${script}`)));
938
}
939
940
const handler = handlerForScript(script);
941
if (handler) {
942
if (env) {
943
env = {
944
...env,
945
};
946
} else {
947
env = {};
948
}
949
if (!env) throw new Error("should never get here");
950
const input = Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES");
951
const output = Deno.env.get("QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES");
952
if (input) {
953
env["QUARTO_USE_FILE_FOR_PROJECT_INPUT_FILES"] = input;
954
}
955
if (output) {
956
env["QUARTO_USE_FILE_FOR_PROJECT_OUTPUT_FILES"] = output;
957
}
958
959
const result = await handler.run(script, args.splice(1), undefined, {
960
cwd: projDir,
961
stdout: quiet ? "piped" : "inherit",
962
env,
963
});
964
if (!result.success) {
965
throw new Error();
966
}
967
} else {
968
const result = await execProcess({
969
cmd: args[0],
970
args: args.slice(1),
971
cwd: projDir,
972
stdout: quiet ? "piped" : "inherit",
973
env,
974
});
975
if (!result.success) {
976
throw new Error();
977
}
978
}
979
}
980
if (scripts.length > 0) {
981
info("");
982
}
983
}
984
985