Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/publish/publish.ts
6442 views
1
/*
2
* publish.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import * as ld from "../core/lodash.ts";
8
9
import { existsSync, walkSync } from "../deno_ral/fs.ts";
10
11
import {
12
basename,
13
dirname,
14
extname,
15
isAbsolute,
16
join,
17
relative,
18
} from "../deno_ral/path.ts";
19
20
import {
21
InputMetadata,
22
PublishFiles,
23
PublishProvider,
24
} from "./provider-types.ts";
25
26
import { AccountToken } from "./provider-types.ts";
27
28
import { PublishOptions } from "./types.ts";
29
30
import { render } from "../command/render/render-shared.ts";
31
import { renderServices } from "../command/render/render-services.ts";
32
import { projectOutputDir } from "../project/project-shared.ts";
33
import { PublishRecord } from "../publish/types.ts";
34
import { ProjectContext } from "../project/types.ts";
35
import { renderProgress } from "../command/render/render-info.ts";
36
import { inspectConfig, isDocumentConfig } from "../inspect/inspect.ts";
37
import { kOutputFile, kTitle } from "../config/constants.ts";
38
import { inputFilesDir } from "../core/render.ts";
39
import {
40
writeProjectPublishDeployment,
41
writePublishDeployment,
42
} from "./config.ts";
43
import { websiteTitle } from "../project/types/website/website-config.ts";
44
import { gfmAutoIdentifier } from "../core/pandoc/pandoc-id.ts";
45
import { RenderResultFile } from "../command/render/types.ts";
46
import { isHtmlContent, isPdfContent } from "../core/mime.ts";
47
import { RenderFlags } from "../command/render/types.ts";
48
import { normalizePath } from "../core/path.ts";
49
import { notebookContext } from "../render/notebook/notebook-context.ts";
50
51
export const kSiteContent = "site";
52
export const kDocumentContent = "document";
53
54
export async function publishSite(
55
project: ProjectContext,
56
provider: PublishProvider,
57
account: AccountToken,
58
options: PublishOptions,
59
target?: PublishRecord,
60
) {
61
// create render function
62
const renderForPublish = async (
63
flags?: RenderFlags,
64
): Promise<PublishFiles> => {
65
let metadataByInput: Record<string, InputMetadata> = {};
66
67
if (options.render) {
68
renderProgress("Rendering for publish:\n");
69
const services = renderServices(notebookContext());
70
try {
71
const result = await render(project.dir, {
72
services,
73
flags,
74
setProjectDir: true,
75
});
76
77
metadataByInput = result.files.reduce(
78
// deno-lint-ignore no-explicit-any
79
(accumulatedResult: any, currentInput) => {
80
const key: string = currentInput.input as string;
81
accumulatedResult[key] = {
82
title: currentInput.format.metadata.title,
83
author: currentInput.format.metadata.author,
84
date: currentInput.format.metadata.date,
85
};
86
return accumulatedResult;
87
},
88
{},
89
);
90
91
result.context.cleanup();
92
if (result.error) {
93
throw result.error;
94
}
95
} finally {
96
services.cleanup();
97
}
98
}
99
// return PublishFiles
100
const outputDir = projectOutputDir(project);
101
const files: string[] = [];
102
for (const walk of walkSync(outputDir)) {
103
if (walk.isFile) {
104
files.push(relative(outputDir, walk.path));
105
}
106
}
107
return normalizePublishFiles({
108
baseDir: outputDir,
109
rootFile: "index.html",
110
files,
111
metadataByInput,
112
});
113
};
114
115
// publish
116
const siteTitle = websiteTitle(project.config) || basename(project.dir);
117
const siteSlug = gfmAutoIdentifier(siteTitle, false);
118
const [publishRecord, siteUrl] = await provider.publish(
119
account,
120
kSiteContent,
121
project.dir,
122
siteTitle,
123
siteSlug,
124
renderForPublish,
125
options,
126
target,
127
);
128
if (publishRecord) {
129
// write publish record if the id wasn't explicitly provided
130
if (options.id === undefined) {
131
writeProjectPublishDeployment(
132
project,
133
provider.name,
134
account,
135
publishRecord,
136
);
137
}
138
}
139
140
// return url
141
return siteUrl;
142
}
143
144
export async function publishDocument(
145
document: string,
146
provider: PublishProvider,
147
account: AccountToken,
148
options: PublishOptions,
149
target?: PublishRecord,
150
) {
151
// establish title
152
let title = basename(document, extname(document));
153
const fileConfig = await inspectConfig(document);
154
if (isDocumentConfig(fileConfig)) {
155
title = (Object.values(fileConfig.formats)[0].metadata[kTitle] ||
156
title) as string;
157
}
158
159
// create render function
160
const renderForPublish = async (
161
flags?: RenderFlags,
162
): Promise<PublishFiles> => {
163
const files: string[] = [];
164
if (options.render) {
165
renderProgress("Rendering for publish:\n");
166
const services = renderServices(notebookContext());
167
try {
168
const result = await render(document, {
169
services,
170
flags,
171
});
172
if (result.error) {
173
result.context.cleanup();
174
throw result.error;
175
}
176
177
// convert the result to be document relative (if the file was in a project
178
// then it will be project relative, which doesn't conform to the expectations
179
// of downstream code)
180
if (result.baseDir) {
181
result.baseDir = normalizePath(result.baseDir);
182
const docDir = normalizePath(dirname(document));
183
if (result.baseDir !== docDir) {
184
const docRelative = (file: string) => {
185
if (!isAbsolute(file)) {
186
file = join(result.baseDir!, file);
187
}
188
return relative(docDir, file);
189
};
190
result.files = result.files.map((resultFile) => {
191
return {
192
...resultFile,
193
file: docRelative(resultFile.file),
194
supporting: resultFile.supporting
195
? resultFile.supporting.map(docRelative)
196
: undefined,
197
resourceFiles: resultFile.resourceFiles.map(docRelative),
198
};
199
});
200
result.baseDir = docDir;
201
}
202
}
203
204
// populate files
205
const baseDir = result.baseDir || dirname(document);
206
const asRelative = (file: string) => {
207
if (isAbsolute(file)) {
208
return relative(baseDir, file);
209
} else {
210
return file;
211
}
212
};
213
214
// When publishing a document, try using an HTML or PDF
215
// document as the rootFile, if one isn't present, just take
216
// the first one
217
const findRootFile = (files: RenderResultFile[]) => {
218
const rootFile = files.find((renderResult) => {
219
return isHtmlContent(renderResult.file);
220
}) || files.find((renderResult) => {
221
return isPdfContent(renderResult.file);
222
}) || files[0];
223
224
if (rootFile) {
225
return asRelative(rootFile.file);
226
} else {
227
return undefined;
228
}
229
};
230
231
const rootFile: string | undefined = findRootFile(result.files);
232
for (const resultFile of result.files) {
233
const file = asRelative(resultFile.file);
234
files.push(file);
235
if (resultFile.supporting) {
236
files.push(
237
...resultFile.supporting
238
.map((sf) => {
239
if (isAbsolute(sf)) {
240
return relative(baseDir, sf);
241
} else {
242
return sf;
243
}
244
})
245
.map(asRelative),
246
);
247
}
248
files.push(...resultFile.resourceFiles.map(asRelative));
249
}
250
251
// If there is an output dir, we need to resolve this into a base dir
252
// that roots in the output directory. So use the project path +
253
// the folder from the base dir + the output dir
254
let finalBaseDir = baseDir;
255
if (result.outputDir && result.context) {
256
const relBasePath = relative(result.context.dir, baseDir);
257
finalBaseDir = join(
258
result.context.dir,
259
result.outputDir,
260
relBasePath,
261
);
262
}
263
result.context.cleanup();
264
265
return normalizePublishFiles({
266
baseDir: finalBaseDir,
267
rootFile: rootFile!,
268
files,
269
});
270
} finally {
271
services.cleanup();
272
}
273
} else {
274
// not rendering so we inspect
275
const baseDir = dirname(document);
276
if (isDocumentConfig(fileConfig)) {
277
// output files
278
let rootFile: string | undefined;
279
for (const format of Object.values(fileConfig.formats)) {
280
title = (format.metadata[kTitle] || title) as string;
281
const outputFile = format.pandoc[kOutputFile];
282
if (outputFile && existsSync(join(baseDir, outputFile))) {
283
files.push(outputFile);
284
if (!rootFile) {
285
rootFile = outputFile;
286
}
287
} else {
288
throw new Error(`Output file ${outputFile} does not exist.`);
289
}
290
}
291
// filesDir (if it exists)
292
const filesDir = inputFilesDir(document);
293
if (existsSync(filesDir)) {
294
files.push(filesDir);
295
}
296
// resources
297
files.push(...fileConfig.resources);
298
// return
299
return normalizePublishFiles({
300
baseDir,
301
rootFile: rootFile!,
302
files,
303
});
304
} else {
305
throw new Error(
306
`The specifed document (${document}) is not a valid quarto input file`,
307
);
308
}
309
}
310
};
311
312
// publish
313
const [publishRecord, siteUrl] = await provider.publish(
314
account,
315
kDocumentContent,
316
document,
317
title,
318
gfmAutoIdentifier(title, false),
319
renderForPublish,
320
options,
321
target,
322
);
323
if (publishRecord) {
324
// write publish record if the id wasn't explicitly provided
325
if (options.id === undefined) {
326
writePublishDeployment(document, provider.name, account, publishRecord);
327
}
328
}
329
330
// return url
331
return siteUrl;
332
}
333
334
function normalizePublishFiles(publishFiles: PublishFiles) {
335
publishFiles.files = publishFiles.files.reduce((files, file) => {
336
const filePath = join(publishFiles.baseDir, file);
337
if (Deno.statSync(filePath).isDirectory) {
338
for (const walk of walkSync(filePath)) {
339
if (walk.isFile) {
340
files.push(relative(publishFiles.baseDir, walk.path));
341
}
342
}
343
} else {
344
files.push(file);
345
}
346
return files;
347
}, new Array<string>());
348
publishFiles.files = ld.uniq(publishFiles.files) as string[];
349
return publishFiles;
350
}
351
352