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