Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/render/freeze.ts
3584 views
1
/*
2
* freeze.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
basename,
9
dirname,
10
extname,
11
isAbsolute,
12
join,
13
relative,
14
} from "../../deno_ral/path.ts";
15
import {
16
ensureDirSync,
17
EOL,
18
existsSync,
19
format,
20
LF,
21
} from "../../deno_ral/fs.ts";
22
23
import { inputFilesDir } from "../../core/render.ts";
24
import { TempContext } from "../../core/temp.ts";
25
import { md5HashSync } from "../../core/hash.ts";
26
import {
27
normalizePath,
28
removeIfEmptyDir,
29
removeIfExists,
30
safeRemoveIfExists,
31
} from "../../core/path.ts";
32
33
import {
34
kIncludeAfterBody,
35
kIncludeBeforeBody,
36
kIncludeInHeader,
37
} from "../../config/constants.ts";
38
39
import { ExecuteResult } from "../../execute/types.ts";
40
41
import { kProjectLibDir, ProjectContext } from "../../project/types.ts";
42
import { projectScratchPath } from "../../project/project-scratch.ts";
43
import { copyMinimal, copyTo } from "../../core/copy.ts";
44
import { warning } from "../../deno_ral/log.ts";
45
46
export const kProjectFreezeDir = "_freeze";
47
export const kOldFreezeExecuteResults = "execute";
48
export const kFreezeExecuteResults = "execute-results";
49
50
export function freezeExecuteResult(
51
input: string,
52
output: string,
53
result: ExecuteResult,
54
) {
55
// resolve includes within executeResult
56
// nb: Beware to not modify the original result object
57
const innerResult = {
58
...result,
59
includes: result.includes ? { ...result.includes } : undefined,
60
} as ExecuteResult;
61
const resolveIncludes = (
62
name: "include-in-header" | "include-before-body" | "include-after-body",
63
) => {
64
if (innerResult.includes) {
65
if (innerResult.includes[name]) {
66
innerResult.includes[name] = innerResult.includes[name]!.map((file) =>
67
// Storing file content using LF line ending
68
format(Deno.readTextFileSync(file), LF)
69
);
70
}
71
}
72
};
73
resolveIncludes(kIncludeInHeader);
74
resolveIncludes(kIncludeBeforeBody);
75
resolveIncludes(kIncludeAfterBody);
76
77
// make the supporting dirs relative to the input file dir
78
innerResult.supporting = innerResult.supporting.map((file) => {
79
if (isAbsolute(file)) {
80
return relative(normalizePath(dirname(input)), file);
81
} else {
82
return file;
83
}
84
});
85
86
// save both the result and a hash of the input file
87
const hash = freezeInputHash(input);
88
89
// write the freeze json
90
const freezeJsonFile = freezeResultFile(input, output, true);
91
Deno.writeTextFileSync(
92
freezeJsonFile,
93
JSON.stringify({ hash, result: innerResult }, undefined, 2),
94
);
95
96
// return the file
97
return freezeJsonFile;
98
}
99
100
export function defrostExecuteResult(
101
source: string,
102
output: string,
103
temp: TempContext,
104
force = false,
105
) {
106
const resultFile = freezeResultFile(source, output);
107
if (existsSync(resultFile)) {
108
// parse result
109
let hash: string;
110
let result: ExecuteResult;
111
const contents = Deno.readTextFileSync(resultFile);
112
try {
113
const inp = JSON.parse(contents) as {
114
hash: string;
115
result: ExecuteResult;
116
};
117
hash = inp.hash;
118
result = inp.result;
119
} catch (_e) {
120
if (
121
contents.match("<<<<<<<") && contents.match(">>>>>>>") &&
122
contents.match("=======")
123
) {
124
warning(
125
`Error parsing ${resultFile}; it looks possibly like a git merge conflict.`,
126
);
127
} else {
128
warning(`Error parsing ${resultFile}; it may be corrupt.`);
129
}
130
return;
131
}
132
133
// use frozen version for force or equivalent source hash
134
if (force || hash === freezeInputHash(source)) {
135
// full path to supporting
136
result.supporting = result.supporting.map((file) =>
137
join(normalizePath(dirname(source)), file)
138
);
139
140
// convert includes to files
141
const resolveIncludes = (
142
name:
143
| "include-in-header"
144
| "include-before-body"
145
| "include-after-body",
146
) => {
147
if (result.includes) {
148
if (result.includes[name]) {
149
result.includes[name] = result.includes[name]!.map((content) => {
150
const includeFile = temp.createFile();
151
// Restoring content in file using the OS line ending character
152
content = format(content, EOL);
153
Deno.writeTextFileSync(includeFile, content);
154
return includeFile;
155
});
156
}
157
}
158
};
159
resolveIncludes(kIncludeInHeader);
160
resolveIncludes(kIncludeBeforeBody);
161
resolveIncludes(kIncludeAfterBody);
162
163
return result;
164
}
165
}
166
}
167
168
export function projectFreezerDir(dir: string, hidden: boolean) {
169
const freezeDir = hidden
170
? projectScratchPath(dir, kProjectFreezeDir)
171
: join(dir, kProjectFreezeDir);
172
ensureDirSync(freezeDir);
173
return normalizePath(freezeDir);
174
}
175
176
export function copyToProjectFreezer(
177
project: ProjectContext,
178
file: string,
179
hidden: boolean,
180
incremental: boolean,
181
) {
182
const freezerDir = projectFreezerDir(project.dir, hidden);
183
const srcFilesDir = join(project.dir, file);
184
const destFilesDir = join(freezerDir, asFreezerDir(file));
185
if (incremental) {
186
for (const dir of Deno.readDirSync(srcFilesDir)) {
187
if (dir.name === kFreezeExecuteResults) {
188
const resultsDir = join(srcFilesDir, dir.name);
189
const destResultsDir = join(destFilesDir, kFreezeExecuteResults);
190
ensureDirSync(destResultsDir);
191
for (const json of Deno.readDirSync(resultsDir)) {
192
if (json.isFile) {
193
copyTo(
194
join(resultsDir, json.name),
195
join(destResultsDir, json.name),
196
);
197
}
198
}
199
} else {
200
copyMinimal(
201
join(srcFilesDir, dir.name),
202
join(destFilesDir, dir.name),
203
);
204
}
205
}
206
} else {
207
copyMinimal(srcFilesDir, destFilesDir);
208
}
209
}
210
211
export function copyFromProjectFreezer(
212
project: ProjectContext,
213
file: string,
214
hidden: boolean,
215
) {
216
const freezerDir = projectFreezerDir(project.dir, hidden);
217
const srcFilesDir = join(
218
freezerDir,
219
asFreezerDir(file),
220
);
221
const destFilesDir = join(project.dir, file);
222
if (existsSync(srcFilesDir)) {
223
copyMinimal(srcFilesDir, destFilesDir);
224
}
225
}
226
227
export function pruneProjectFreezerDir(
228
project: ProjectContext,
229
dir: string,
230
files: string[],
231
hidden: boolean,
232
) {
233
const freezerDir = projectFreezerDir(project.dir, hidden);
234
// on some network drives removeSync w/ recursive: true doesn't seem to work
235
// (see https://github.com/quarto-dev/quarto-cli/issues/188)
236
// TODO: this prevents the error but we will want to eventually
237
// find a way to do force this
238
files.map((file) => {
239
const filePath = join(freezerDir, dir, file);
240
safeRemoveIfExists(filePath);
241
});
242
removeIfEmptyDir(join(freezerDir, dir));
243
}
244
245
export function pruneProjectFreezer(project: ProjectContext, hidden: boolean) {
246
const freezerDir = projectFreezerDir(project.dir, hidden);
247
const libDir = project.config?.project[kProjectLibDir];
248
if (libDir) {
249
let remove = true;
250
for (const entry of Deno.readDirSync(freezerDir)) {
251
if (entry.isFile || entry.name !== libDir) {
252
remove = false;
253
break;
254
}
255
}
256
if (remove) {
257
removeIfExists(freezerDir);
258
}
259
} else {
260
removeIfEmptyDir(freezerDir);
261
}
262
}
263
264
export function freezerFreezeFile(project: ProjectContext, freezeFile: string) {
265
const filesDir = asFreezerDir(dirname(dirname(freezeFile)));
266
return join(
267
project.dir,
268
kProjectFreezeDir,
269
filesDir,
270
kFreezeExecuteResults,
271
basename(freezeFile),
272
);
273
}
274
275
export function freezerFigsDir(
276
project: ProjectContext,
277
filesDir: string,
278
figsDir: string,
279
) {
280
return join(
281
project.dir,
282
kProjectFreezeDir,
283
asFreezerDir(filesDir),
284
figsDir,
285
);
286
}
287
288
export function freezeResultFile(
289
input: string,
290
output: string,
291
ensureDir = false,
292
) {
293
const filesDir = join(dirname(input), inputFilesDir(input));
294
const freezeDir = join(filesDir, kFreezeExecuteResults);
295
if (ensureDir) {
296
ensureDirSync(freezeDir);
297
}
298
299
return join(freezeDir, extname(output).slice(1) + ".json");
300
}
301
302
export function removeFreezeResults(filesDir: string) {
303
const freezeDir = join(filesDir, kFreezeExecuteResults);
304
removeIfExists(freezeDir);
305
const oldFreezeDir = join(filesDir, kOldFreezeExecuteResults);
306
removeIfExists(oldFreezeDir);
307
if (existsSync(filesDir)) {
308
removeIfEmptyDir(filesDir);
309
}
310
}
311
312
function freezeInputHash(input: string) {
313
// Calculate the hash on a content with LF line ending to avoid
314
// different hash on different OS (#3599)
315
return md5HashSync(format(Deno.readTextFileSync(input), LF));
316
}
317
318
// don't use _files suffix in freezer
319
function asFreezerDir(dir: string) {
320
return dir.replace(/_files$/, "");
321
}
322
323