Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/publish/netlify/netlify.ts
6460 views
1
/*
2
* netlify.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
AccessToken,
9
Deploy,
10
NetlifyClient,
11
Site,
12
Ticket,
13
} from "./api/index.ts";
14
15
import {
16
AccountToken,
17
AccountTokenType,
18
PublishFiles,
19
PublishProvider,
20
} from "../provider-types.ts";
21
import { ApiError } from "../../publish/netlify/api/index.ts";
22
import { PublishOptions, PublishRecord } from "../types.ts";
23
import {
24
AuthorizationHandler,
25
authorizeAccessToken,
26
readAccessTokens,
27
writeAccessTokens,
28
} from "../common/account.ts";
29
import { quartoConfig } from "../../core/quarto.ts";
30
import { withRetry } from "../../core/retry.ts";
31
import { handlePublish, PublishHandler } from "../common/publish.ts";
32
import { authorizePrompt } from "../account.ts";
33
import { RenderFlags } from "../../command/render/types.ts";
34
35
export const kNetlify = "netlify";
36
const kNetlifyDescription = "Netlify";
37
38
export const kNetlifyAuthTokenVar = "NETLIFY_AUTH_TOKEN";
39
40
export const netlifyProvider: PublishProvider = {
41
name: kNetlify,
42
description: kNetlifyDescription,
43
requiresServer: false,
44
listOriginOnly: false,
45
accountTokens,
46
authorizeToken,
47
removeToken,
48
resolveTarget,
49
publish,
50
isUnauthorized,
51
isNotFound,
52
};
53
54
function accountTokens() {
55
const envTk = environmentAuthToken();
56
const accessTkns = accessTokens();
57
58
const accounts: AccountToken[] = [];
59
if (envTk) {
60
accounts.push({
61
type: AccountTokenType.Environment,
62
name: kNetlifyAuthTokenVar,
63
server: null,
64
token: envTk,
65
});
66
}
67
if (accessTkns) {
68
for (const accessTk of accessTkns) {
69
if (accessTk?.access_token) {
70
accounts.push({
71
type: AccountTokenType.Authorized,
72
name: accessTk.email!,
73
server: null,
74
token: accessTk?.access_token,
75
});
76
}
77
}
78
}
79
80
return Promise.resolve(accounts);
81
}
82
83
async function authorizeToken(_options: PublishOptions) {
84
if (await authorizePrompt(netlifyProvider)) {
85
const token = await authorizeNetlifyAccessToken();
86
if (token) {
87
return {
88
type: AccountTokenType.Authorized,
89
name: token.email!,
90
server: null,
91
token: token.access_token!,
92
};
93
}
94
} else {
95
return undefined;
96
}
97
}
98
99
function removeToken(token: AccountToken) {
100
writeAccessTokens(
101
netlifyProvider.name,
102
readAccessTokens<AccessToken>(netlifyProvider.name)?.filter(
103
(accessToken) => {
104
return accessToken.email !== token.name;
105
},
106
) || [],
107
);
108
}
109
110
function environmentAuthToken() {
111
return Deno.env.get(kNetlifyAuthTokenVar);
112
}
113
114
function accessTokens(): Array<AccessToken> | undefined {
115
return readAccessTokens<AccessToken>(kNetlify);
116
}
117
118
async function authorizeNetlifyAccessToken(): Promise<
119
AccessToken | undefined
120
> {
121
// create provider for authorization
122
const client = new NetlifyClient({});
123
const clientId = (await quartoConfig.dotenv())["NETLIFY_APP_CLIENT_ID"];
124
const handler: AuthorizationHandler<AccessToken, Ticket> = {
125
name: kNetlifyDescription,
126
createTicket: function (): Promise<Ticket> {
127
return client.ticket.createTicket({
128
clientId,
129
}) as unknown as Promise<Ticket>;
130
},
131
authorizationUrl: function (ticket: Ticket): string {
132
return `https://app.netlify.com/authorize?response_type=ticket&ticket=${ticket.id}`;
133
},
134
checkTicket: function (ticket: Ticket): Promise<Ticket> {
135
return client.ticket.showTicket({ ticketId: ticket.id! });
136
},
137
exchangeTicket: function (ticket: Ticket): Promise<AccessToken> {
138
return client.accessToken
139
.exchangeTicket({
140
ticketId: ticket.id!,
141
}) as unknown as Promise<AccessToken>;
142
},
143
144
compareTokens: (a: AccessToken, b: AccessToken) => a.email === b.email,
145
};
146
147
return authorizeAccessToken(handler);
148
}
149
150
async function resolveTarget(
151
account: AccountToken,
152
target: PublishRecord,
153
): Promise<PublishRecord | undefined> {
154
const client = new NetlifyClient({
155
TOKEN: account.token,
156
});
157
// get the site
158
const site = await client.site.getSite({ siteId: target.id });
159
160
// if it doesn't have asset optimization disabled then do that
161
// (but leave image optimization enabled)
162
if (updateProcessingSettings(site)) {
163
await client.site.updateSite({
164
siteId: target.id,
165
site: {
166
processing_settings: {
167
css: {
168
bundle: false,
169
minify: false,
170
},
171
html: {
172
pretty_urls: false,
173
},
174
js: {
175
bundle: false,
176
minify: false,
177
},
178
},
179
},
180
});
181
}
182
183
// return the target url
184
target.url = site?.ssl_url || site?.url || target.url;
185
return target;
186
}
187
188
function updateProcessingSettings(site: Site) {
189
return site.processing_settings?.skip !== true &&
190
(site.processing_settings?.css?.bundle ||
191
site.processing_settings?.css?.minify ||
192
site.processing_settings?.html?.pretty_urls ||
193
site.processing_settings?.js?.bundle ||
194
site.processing_settings?.js?.minify);
195
}
196
197
function publish(
198
account: AccountToken,
199
type: "document" | "site",
200
_input: string,
201
title: string,
202
slug: string,
203
render: (flags?: RenderFlags) => Promise<PublishFiles>,
204
_options: PublishOptions,
205
target?: PublishRecord,
206
): Promise<[PublishRecord, URL | undefined]> {
207
// create client
208
const client = new NetlifyClient({
209
TOKEN: account.token,
210
});
211
212
const handler: PublishHandler<Site, Deploy> = {
213
name: kNetlify,
214
createSite: async (_type: string, _title: string, _slug: string) => {
215
return withSslUrl(
216
await client.site.createSite({
217
site: {
218
force_ssl: true,
219
processing_settings: {
220
skip: true,
221
},
222
},
223
}) as unknown as Site,
224
);
225
},
226
createDeploy: async (siteId: string, files: Record<string, string>) => {
227
return withSslUrl(
228
await client.deploy.createSiteDeploy({
229
siteId,
230
deploy: {
231
files,
232
async: true,
233
},
234
}),
235
);
236
},
237
getDeploy: async (deployId: string) => {
238
return withSslUrl(
239
await client.deploy.getDeploy({
240
deployId,
241
}),
242
);
243
},
244
uploadDeployFile: async (
245
deployId: string,
246
path: string,
247
fileBody: Blob,
248
) => {
249
await withRetry(async () => {
250
await client.file.uploadDeployFile({
251
deployId,
252
path,
253
fileBody,
254
});
255
});
256
},
257
};
258
259
return handlePublish<Site, Deploy>(
260
handler,
261
type,
262
title,
263
slug,
264
render,
265
target,
266
);
267
}
268
269
function withSslUrl(obj: { ssl_url?: string; url?: string }) {
270
return {
271
...obj,
272
url: obj.ssl_url || obj.url,
273
};
274
}
275
276
function isUnauthorized(err: Error) {
277
return err instanceof ApiError && err.status === 401;
278
}
279
280
function isNotFound(err: Error) {
281
return err instanceof ApiError && err.status === 404;
282
}
283
284