alchemy 0.16.7 → 0.16.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,486 @@
1
+ import type { Context } from "../context.js";
2
+ import { Resource } from "../resource.js";
3
+ import { isSecret, type Secret } from "../secret.js";
4
+ import { createVercelApi, type VercelApi } from "./api.js";
5
+
6
+ type TargetEnvironment = "production" | "preview" | "development";
7
+
8
+ export type EnvironmentVariable = {
9
+ /**
10
+ * The key of the environment variable
11
+ */
12
+ key: string;
13
+
14
+ /**
15
+ * The target environment
16
+ */
17
+ target: TargetEnvironment[];
18
+
19
+ /**
20
+ * The Git branch
21
+ */
22
+ gitBranch?: string;
23
+
24
+ /**
25
+ * The type of environment variable.
26
+ *
27
+ * Defaults to `plain` when `value` is a `string`, and `encrypted` when `value` is a `Secret`.
28
+ */
29
+ type?: "encrypted" | "plain" | "sensitive";
30
+
31
+ /**
32
+ * The value of the environment variable
33
+ */
34
+ value: Secret | string;
35
+ } & (
36
+ | { type?: "plain" | "system"; value: string }
37
+ | { type?: "encrypted" | "sensitive"; value: Secret }
38
+ );
39
+
40
+ /**
41
+ * Properties for creating or updating a Project
42
+ */
43
+ export interface ProjectProps {
44
+ /**
45
+ * The desired name for the project
46
+ *
47
+ * Maximum length: `100`
48
+ * Example: `a-project-name`
49
+ */
50
+ name: string;
51
+
52
+ /**
53
+ * Opt-in to preview toolbar on the project level
54
+ */
55
+ enablePreviewFeedback?: boolean;
56
+
57
+ /**
58
+ * Opt-in to production toolbar on the project level
59
+ */
60
+ enableProductionFeedback?: boolean;
61
+
62
+ /**
63
+ * The build command for this project. When `null` is used this value will be automatically detected
64
+ */
65
+ buildCommand?: string;
66
+
67
+ commandForIgnoringBuildStep?: string;
68
+
69
+ /**
70
+ * The dev command for this project. When `null` is used this value will be automatically detected
71
+ */
72
+ devCommand?: string;
73
+
74
+ /**
75
+ * Collection of ENV Variables the Project will use
76
+ */
77
+ environmentVariables?: Array<EnvironmentVariable>;
78
+
79
+ /**
80
+ * The framework that is being used for this project. When `null` is used no framework is selected
81
+ */
82
+ framework?:
83
+ | "blitzjs"
84
+ | "nextjs"
85
+ | "gatsby"
86
+ | "remix"
87
+ | "react-router"
88
+ | "astro"
89
+ | "hexo"
90
+ | "eleventy"
91
+ | "docusaurus-2"
92
+ | "docusaurus"
93
+ | "preact"
94
+ | "solidstart-1"
95
+ | "solidstart"
96
+ | "dojo"
97
+ | "ember"
98
+ | "vue"
99
+ | "scully"
100
+ | "ionic-angular"
101
+ | "angular"
102
+ | "polymer"
103
+ | "svelte"
104
+ | "sveltekit"
105
+ | "sveltekit-1"
106
+ | "ionic-react"
107
+ | "create-react-app"
108
+ | "gridsome"
109
+ | "umijs"
110
+ | "sapper"
111
+ | "saber"
112
+ | "stencil"
113
+ | "nuxtjs"
114
+ | "redwoodjs"
115
+ | "hugo"
116
+ | "jekyll"
117
+ | "brunch"
118
+ | "middleman"
119
+ | "zola"
120
+ | "hydrogen"
121
+ | "vite"
122
+ | "vitepress"
123
+ | "vuepress"
124
+ | "parcel"
125
+ | "fasthtml"
126
+ | "sanity-v3"
127
+ | "sanity"
128
+ | "storybook"
129
+ | (string & {});
130
+
131
+ /**
132
+ * The Git Repository that will be connected to the project. When this is defined, any pushes to the specified connected Git Repository will be automatically deployed
133
+ */
134
+ gitRepository?: {
135
+ /**
136
+ * The Git Provider of the repository
137
+ */
138
+ type: "github" | "gitlab" | "bitbucket";
139
+
140
+ /**
141
+ * The name of the git repository. For example: `vercel/next.js`
142
+ */
143
+ repo: string;
144
+ };
145
+
146
+ /**
147
+ * The install command for this project. When `null` is used this value will be automatically detected
148
+ */
149
+ installCommand?: string;
150
+
151
+ /**
152
+ * The output directory of the build. When `null` is used this value will be automatically detected
153
+ */
154
+ outputDirectory?: string;
155
+
156
+ /**
157
+ * Specifies whether the source code and logs of the deployments for this project should be public or not
158
+ */
159
+ publicSource?: boolean;
160
+
161
+ /**
162
+ * The name of a directory or relative path to the source code of your project. When `null` is used it will default to the project root
163
+ */
164
+ rootDirectory?: string;
165
+
166
+ /**
167
+ * The region to deploy Serverless Functions in this project
168
+ */
169
+ serverlessFunctionRegion?: string;
170
+
171
+ /**
172
+ * Specifies whether Zero Config Failover is enabled for this project.
173
+ */
174
+ serverlessFunctionZeroConfigFailover?: boolean;
175
+
176
+ /**
177
+ * OpenID Connect JSON Web Token generation configuration.
178
+ */
179
+ oidcTokenConfig?: {
180
+ /**
181
+ * Whether or not to generate OpenID Connect JSON Web Tokens.
182
+ */
183
+ enabled: boolean;
184
+
185
+ /**
186
+ * team: `https://oidc.vercel.com/[team_slug]` global: `https://oidc.vercel.com`
187
+ */
188
+ issuerMode: "team" | "global";
189
+ };
190
+
191
+ /**
192
+ * Opt-in to skip deployments when there are no changes to the root directory and its dependencies
193
+ */
194
+ enableAffectedProjectsDeployments?: boolean;
195
+
196
+ /**
197
+ * Resource configuration
198
+ */
199
+ resourceConfig?: {
200
+ /**
201
+ * Whether fluid is enabled
202
+ */
203
+ fluid?: boolean;
204
+
205
+ /**
206
+ * Default regions for functions
207
+ */
208
+ functionDefaultRegions?: string[];
209
+
210
+ /**
211
+ * Default timeout for functions
212
+ */
213
+ functionDefaultTimeout?: number;
214
+
215
+ /**
216
+ * Default memory type for functions
217
+ */
218
+ functionDefaultMemoryType?: "standard_legacy" | "standard" | "performance";
219
+
220
+ /**
221
+ * Whether function zero config failover is enabled
222
+ */
223
+ functionZeroConfigFailover?: boolean;
224
+
225
+ /**
226
+ * Whether elastic concurrency is enabled
227
+ */
228
+ elasticConcurrencyEnabled?: boolean;
229
+
230
+ /**
231
+ * The build machine type
232
+ */
233
+ buildMachineType?: "enhanced" | "ultra";
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Output returned after Project creation/update
239
+ */
240
+ export interface Project extends Resource<"vercel::Project">, ProjectProps {
241
+ /**
242
+ * The ID of the project
243
+ */
244
+ id: string;
245
+
246
+ /**
247
+ * The account ID that the project belongs to
248
+ */
249
+ accountId: string;
250
+
251
+ /**
252
+ * The time at which the project was created
253
+ */
254
+ createdAt: number;
255
+
256
+ /**
257
+ * The time at which the project was last updated
258
+ */
259
+ updatedAt: number;
260
+
261
+ /**
262
+ * The latest deployment of the project
263
+ */
264
+ latestDeployment?: {
265
+ /**
266
+ * The ID of the deployment
267
+ */
268
+ id: string;
269
+
270
+ /**
271
+ * The URL of the deployment
272
+ */
273
+ url: string;
274
+ };
275
+ }
276
+
277
+ const decryptEnvironmentVariable = (envVar: EnvironmentVariable) => {
278
+ let { type, value } = envVar;
279
+
280
+ // Decrypt Secret & default `type`
281
+ if (isSecret(value)) {
282
+ value = value.unencrypted;
283
+ type ??= "encrypted";
284
+ } else {
285
+ // Otherwise, default to `plain`
286
+ type ??= "plain";
287
+ }
288
+
289
+ return { ...envVar, type, value };
290
+ };
291
+
292
+ /**
293
+ * Create and manage Vercel projects.
294
+ *
295
+ * @example
296
+ * // With accessToken
297
+ * const project = await Project("my-app", {
298
+ * accessToken: alchemy.secret(process.env.VERCEL_ACCESS_TOKEN),
299
+ * name: "my-app",
300
+ * framework: "astro",
301
+ * });
302
+ *
303
+ * @example
304
+ * // With GitHub
305
+ * const project = await Project("my-app", {
306
+ * name: "my-app",
307
+ * framework: "nextjs",
308
+ * gitRepository: {
309
+ * type: "github",
310
+ * repo: "username/my-app",
311
+ * },
312
+ * });
313
+ *
314
+ * @example
315
+ * // With plain text environment variable
316
+ * const project = await Project("my-app", {
317
+ * name: "my-app",
318
+ * environmentVariables: [
319
+ * {
320
+ * key: "PUBLIC_URL",
321
+ * target: ["production", "preview", "development"],
322
+ * value: "https://example.com",
323
+ * },
324
+ * ],
325
+ * });
326
+ *
327
+ * @example
328
+ * // With encrypted environment variable
329
+ * const project = await Project("my-app", {
330
+ * name: "my-app",
331
+ * environmentVariables: [
332
+ * {
333
+ * key: "DATABASE_URL",
334
+ * target: ["production", "preview"],
335
+ * value: alchemy.secret("DATABASE_URL"),
336
+ * },
337
+ * ],
338
+ * });
339
+ *
340
+ * @example
341
+ * // With sensitive environment variable
342
+ * const project = await Project("my-app", {
343
+ * name: "my-app",
344
+ * environmentVariables: [
345
+ * {
346
+ * key: "DATABASE_URL",
347
+ * target: ["production", "preview"],
348
+ * type: "sensitive",
349
+ * value: alchemy.secret("DATABASE_URL"),
350
+ * },
351
+ * ],
352
+ * });
353
+ *
354
+ * @example
355
+ * // With custom build settings
356
+ * const project = await Project("my-app", {
357
+ * name: "my-app",
358
+ * buildCommand: "npm run build",
359
+ * outputDirectory: "dist",
360
+ * installCommand: "npm install",
361
+ * devCommand: "npm run dev",
362
+ * });
363
+ */
364
+ export const Project = Resource(
365
+ "vercel::Project",
366
+ async function (
367
+ this: Context<Project>,
368
+ id: string,
369
+ { accessToken, ...props }: ProjectProps & { accessToken?: Secret },
370
+ ): Promise<Project> {
371
+ switch (this.phase) {
372
+ case "delete": {
373
+ const api = await createVercelApi({
374
+ baseUrl: "https://api.vercel.com/v9",
375
+ accessToken,
376
+ });
377
+
378
+ try {
379
+ if (this.output?.id) {
380
+ await api.delete(`/projects/${this.output.id}`);
381
+ }
382
+ } catch (error) {
383
+ console.error("Error deleting project:", error);
384
+ }
385
+
386
+ return this.destroy();
387
+ }
388
+
389
+ case "update": {
390
+ const api = await createVercelApi({
391
+ baseUrl: "https://api.vercel.com/v9",
392
+ accessToken,
393
+ });
394
+
395
+ if (!this.output?.id) {
396
+ throw new Error(
397
+ `Cannot update project ${id}: missing previous output.id`,
398
+ );
399
+ }
400
+
401
+ // 409 Conflict: Can't update name, so remove it from the props
402
+ // 400 Invalid Request: Should NOT have additional property `environmentVariables`
403
+ const { name, environmentVariables, ...rest } = props;
404
+ const response = await api.patch(`/projects/${this.output.id}`, rest);
405
+ const data = (await response.json()) as Project;
406
+
407
+ if (environmentVariables) {
408
+ const envApi = await createVercelApi({
409
+ baseUrl: "https://api.vercel.com/v10",
410
+ accessToken,
411
+ });
412
+
413
+ await updateEnvironmentVariables(envApi, this.output, props);
414
+ }
415
+
416
+ return this({
417
+ id: data.id,
418
+ accountId: data.accountId,
419
+ createdAt: data.createdAt,
420
+ updatedAt: data.updatedAt,
421
+ latestDeployment: data.latestDeployment,
422
+ ...props,
423
+ });
424
+ }
425
+
426
+ case "create": {
427
+ const api = await createVercelApi({
428
+ baseUrl: "https://api.vercel.com/v11",
429
+ accessToken,
430
+ });
431
+
432
+ const project = await createProject(api, props);
433
+
434
+ return this.create({ ...props, ...project });
435
+ }
436
+ }
437
+ },
438
+ );
439
+
440
+ export async function createProject(api: VercelApi, props: ProjectProps) {
441
+ const response = await api.post("/projects", {
442
+ ...props,
443
+ environmentVariables: props.environmentVariables?.map(
444
+ decryptEnvironmentVariable,
445
+ ),
446
+ });
447
+
448
+ const data = (await response.json()) as Project;
449
+
450
+ return data;
451
+ }
452
+
453
+ export async function updateEnvironmentVariables(
454
+ api: VercelApi,
455
+ output: Project,
456
+ props: ProjectProps,
457
+ ) {
458
+ if (!props.environmentVariables) {
459
+ return;
460
+ }
461
+
462
+ // Update explicitly set environment variables
463
+ await api.post(
464
+ `/projects/${output.id}/env?upsert=true`,
465
+ props.environmentVariables.map(decryptEnvironmentVariable),
466
+ );
467
+
468
+ // Find previous env vars that are not in the new list and delete them
469
+ const { envs } = (await api
470
+ .get(`/projects/${output.id}/env`)
471
+ .then((res) => res.json())) as {
472
+ envs: Array<{ id: string; key: string }>;
473
+ };
474
+
475
+ for (const previousEnv of envs) {
476
+ if (
477
+ props.environmentVariables.some(
478
+ (envVar) => envVar.key === previousEnv.key,
479
+ )
480
+ ) {
481
+ continue;
482
+ }
483
+
484
+ await api.delete(`/projects/${output.id}/env/${previousEnv.id}`);
485
+ }
486
+ }