Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/publish/cmd.ts
3562 views
1
/*
2
* cmd.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { existsSync } from "../../deno_ral/fs.ts";
8
9
import { Command } from "cliffy/command/mod.ts";
10
import { Select } from "cliffy/prompt/select.ts";
11
import { prompt } from "cliffy/prompt/mod.ts";
12
13
import { findProvider } from "../../publish/provider.ts";
14
15
import { AccountToken, PublishProvider } from "../../publish/provider-types.ts";
16
17
import { publishProviders } from "../../publish/provider.ts";
18
import { initYamlIntelligenceResourcesFromFilesystem } from "../../core/schema/utils.ts";
19
import {
20
initState,
21
setInitializer,
22
} from "../../core/lib/yaml-validation/state.ts";
23
import { projectContext } from "../../project/project-context.ts";
24
25
import {
26
projectIsManuscript,
27
projectIsWebsite,
28
} from "../../project/project-shared.ts";
29
30
import { PublishCommandOptions } from "./options.ts";
31
import { resolveDeployment } from "./deployment.ts";
32
import { AccountPrompt, manageAccounts, resolveAccount } from "./account.ts";
33
34
import { PublishOptions, PublishRecord } from "../../publish/types.ts";
35
import { isInteractiveTerminal, isServerSession } from "../../core/platform.ts";
36
import { runningInCI } from "../../core/ci-info.ts";
37
import { ProjectContext } from "../../project/types.ts";
38
import { openUrl } from "../../core/shell.ts";
39
import { publishDocument, publishSite } from "../../publish/publish.ts";
40
import { handleUnauthorized } from "../../publish/account.ts";
41
import { notebookContext } from "../../render/notebook/notebook-context.ts";
42
import { singleFileProjectContext } from "../../project/types/single-file/single-file.ts";
43
44
export const publishCommand =
45
// deno-lint-ignore no-explicit-any
46
new Command<any>()
47
.name("publish")
48
.description(
49
"Publish a document or project to a provider.\n\nAvailable providers include:\n\n" +
50
" - Quarto Pub (quarto-pub)\n" +
51
" - GitHub Pages (gh-pages)\n" +
52
" - Posit Connect (connect)\n" +
53
" - Netlify (netlify)\n" +
54
" - Confluence (confluence)\n" +
55
" - Hugging Face Spaces (huggingface)\n\n" +
56
"Accounts are configured interactively during publishing.\n" +
57
"Manage/remove accounts with: quarto publish accounts",
58
)
59
.arguments("[provider] [path]")
60
.option(
61
"--id <id:string>",
62
"Identifier of content to publish",
63
)
64
.option(
65
"--server <server:string>",
66
"Server to publish to",
67
)
68
.option(
69
"--token <token:string>",
70
"Access token for publising provider",
71
)
72
.option(
73
"--no-render",
74
"Do not render before publishing.",
75
)
76
.option(
77
"--no-prompt",
78
"Do not prompt to confirm publishing destination",
79
)
80
.option(
81
"--no-browser",
82
"Do not open a browser to the site after publishing",
83
)
84
.example(
85
"Publish project (prompt for provider)",
86
"quarto publish",
87
)
88
.example(
89
"Publish document (prompt for provider)",
90
"quarto publish document.qmd",
91
)
92
.example(
93
"Publish project to Hugging Face Spaces",
94
"quarto publish huggingface",
95
)
96
.example(
97
"Publish project to Netlify",
98
"quarto publish netlify",
99
)
100
.example(
101
"Publish with explicit target",
102
"quarto publish netlify --id DA36416-F950-4647-815C-01A24233E294",
103
)
104
.example(
105
"Publish project to GitHub Pages",
106
"quarto publish gh-pages",
107
)
108
.example(
109
"Publish project to Posit Connect",
110
"quarto publish connect",
111
)
112
.example(
113
"Publish with explicit credentials",
114
"quarto publish connect --server example.com --token 01A24233E294",
115
)
116
.example(
117
"Publish without confirmation prompt",
118
"quarto publish --no-prompt",
119
)
120
.example(
121
"Publish without rendering",
122
"quarto publish --no-render",
123
)
124
.example(
125
"Publish without opening browser",
126
"quarto publish --no-browser",
127
)
128
.example(
129
"Manage/remove publishing accounts",
130
"quarto publish accounts",
131
)
132
.action(
133
async (
134
options: PublishCommandOptions,
135
provider?: string,
136
path?: string,
137
) => {
138
// if provider is a path and no path is specified then swap
139
if (provider && !path && existsSync(provider)) {
140
path = provider;
141
provider = undefined;
142
}
143
144
// if provider is 'accounts' then invoke account management ui
145
if (provider === "accounts") {
146
await manageAccounts();
147
} else {
148
let providerInterface: PublishProvider | undefined;
149
if (provider) {
150
providerInterface = findProvider(provider);
151
if (!providerInterface) {
152
throw new Error(`Publishing source '${provider}' not found`);
153
}
154
}
155
await publishAction(options, providerInterface, path);
156
}
157
},
158
);
159
160
async function publishAction(
161
options: PublishCommandOptions,
162
provider?: PublishProvider,
163
path?: string,
164
) {
165
await initYamlIntelligence();
166
167
// coalesce options
168
const publishOptions = await createPublishOptions(options, provider, path);
169
170
// helper to publish (w/ account confirmation)
171
const doPublish = async (
172
publishProvider: PublishProvider,
173
accountPrompt: AccountPrompt,
174
publishTarget?: PublishRecord,
175
account?: AccountToken,
176
) => {
177
// enforce requiresRender
178
if (publishProvider.requiresRender && publishOptions.render === false) {
179
throw new Error(
180
`${publishProvider.description} requires rendering before publish.`,
181
);
182
}
183
184
// resolve account
185
account = (account && !publishOptions.prompt)
186
? account
187
: await resolveAccount(
188
publishProvider,
189
publishOptions.prompt ? accountPrompt : "never",
190
publishOptions,
191
account,
192
publishTarget,
193
);
194
195
if (account) {
196
// do the publish
197
await publish(
198
publishProvider,
199
account,
200
publishOptions,
201
publishTarget,
202
);
203
}
204
};
205
206
// see if cli options give us a deployment
207
const deployment = (provider && publishOptions.id)
208
? {
209
provider,
210
target: {
211
id: publishOptions.id,
212
},
213
}
214
: await resolveDeployment(
215
publishOptions,
216
provider?.name,
217
);
218
// update provider
219
provider = deployment?.provider || provider;
220
if (deployment) {
221
// existing deployment
222
await doPublish(
223
deployment.provider,
224
deployment.account ? "multiple" : "always",
225
deployment.target,
226
deployment.account,
227
);
228
} else if (publishOptions.prompt) {
229
// new deployment, determine provider if needed
230
const providers = publishProviders();
231
if (!provider) {
232
// select provider
233
const result = await prompt([{
234
indent: "",
235
name: "provider",
236
message: "Provider:",
237
options: providers
238
.filter((provider) => !provider.hidden)
239
.map((provider) => ({
240
name: provider.description,
241
value: provider.name,
242
})),
243
type: Select,
244
}]);
245
if (result.provider) {
246
provider = findProvider(result.provider);
247
}
248
}
249
if (provider) {
250
await doPublish(provider, "always");
251
}
252
} else {
253
throw new Error(
254
"No re-publishing target found (--no-prompt requires an existing 'publish' config to update)",
255
);
256
}
257
}
258
259
async function publish(
260
provider: PublishProvider,
261
account: AccountToken,
262
options: PublishOptions,
263
target?: PublishRecord,
264
): Promise<void> {
265
try {
266
const siteUrl = typeof (options.input) !== "string"
267
? await publishSite(
268
options.input,
269
provider,
270
account,
271
options,
272
target,
273
)
274
: await publishDocument(
275
options.input,
276
provider,
277
account,
278
options,
279
target,
280
);
281
282
// open browser if requested
283
if (siteUrl && options.browser) {
284
await openUrl(siteUrl.toString());
285
}
286
} catch (err) {
287
if (!(err instanceof Error)) {
288
// shouldn't ever happen
289
throw err;
290
}
291
// attempt to recover from unauthorized
292
if (!(provider.isUnauthorized(err) && options.prompt)) {
293
throw err;
294
}
295
if (await handleUnauthorized(provider, account)) {
296
const authorizedAccount = await provider.authorizeToken(
297
options,
298
target,
299
);
300
if (authorizedAccount) {
301
// recursve after re-authorization
302
return await publish(provider, authorizedAccount, options, target);
303
}
304
}
305
}
306
}
307
308
async function createPublishOptions(
309
options: PublishCommandOptions,
310
provider?: PublishProvider,
311
path?: string,
312
): Promise<PublishOptions> {
313
const nbContext = notebookContext();
314
// validate path exists
315
path = path || Deno.cwd();
316
if (!existsSync(path)) {
317
throw new Error(
318
`The specified path (${path}) does not exist so cannot be published.`,
319
);
320
}
321
// determine publish input
322
let input: ProjectContext | string | undefined;
323
324
if (provider && provider.resolveProjectPath) {
325
const resolvedPath = provider.resolveProjectPath(path);
326
try {
327
if (Deno.statSync(resolvedPath).isDirectory) {
328
path = resolvedPath;
329
}
330
} catch (_e) {
331
// ignore
332
}
333
}
334
335
// check for directory (either website or single-file project)
336
const project = (await projectContext(path, nbContext)) ||
337
(await singleFileProjectContext(path, nbContext));
338
if (Deno.statSync(path).isDirectory) {
339
if (projectIsWebsite(project)) {
340
input = project;
341
} else if (
342
projectIsManuscript(project) && project.files.input.length > 0
343
) {
344
input = project;
345
} else if (project.files.input.length === 1) {
346
input = project.files.input[0];
347
} else {
348
throw new Error(
349
`The specified path (${path}) is not a website, manuscript or book project so cannot be published.`,
350
);
351
}
352
} // single file path
353
else {
354
// if there is a project associated with this file then it can't be a website or book
355
if (project && projectIsWebsite(project)) {
356
throw new Error(
357
`The specified path (${path}) is within a website or book project so cannot be published individually`,
358
);
359
}
360
input = path;
361
}
362
363
const interactive = isInteractiveTerminal() && !runningInCI() && !options.id;
364
return {
365
input,
366
server: options.server || null,
367
token: options.token,
368
id: options.id,
369
render: !!options.render,
370
prompt: !!options.prompt && interactive,
371
browser: !!options.browser && interactive && !isServerSession(),
372
};
373
}
374
375
async function initYamlIntelligence() {
376
setInitializer(initYamlIntelligenceResourcesFromFilesystem);
377
await initState();
378
}
379
380