alchemy 0.63.1 → 0.65.0

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.
Files changed (235) hide show
  1. package/bin/alchemy.js +7664 -5857
  2. package/bin/commands/init.ts +130 -9
  3. package/bin/constants.ts +4 -0
  4. package/bin/services/cdp-manager/cdp-proxy.ts +69 -0
  5. package/bin/services/cdp-manager/server.ts +173 -0
  6. package/bin/services/execute-alchemy.ts +83 -4
  7. package/bin/types.ts +1 -0
  8. package/lib/alchemy.d.ts +4 -0
  9. package/lib/alchemy.d.ts.map +1 -1
  10. package/lib/alchemy.js +10 -4
  11. package/lib/alchemy.js.map +1 -1
  12. package/lib/apply.d.ts +3 -0
  13. package/lib/apply.d.ts.map +1 -1
  14. package/lib/apply.js +11 -1
  15. package/lib/apply.js.map +1 -1
  16. package/lib/cloudflare/bindings.d.ts +3 -1
  17. package/lib/cloudflare/bindings.d.ts.map +1 -1
  18. package/lib/cloudflare/bucket.d.ts +2 -1
  19. package/lib/cloudflare/bucket.d.ts.map +1 -1
  20. package/lib/cloudflare/bucket.js.map +1 -1
  21. package/lib/cloudflare/bundle/cloudflare-internal.d.ts +6 -0
  22. package/lib/cloudflare/bundle/cloudflare-internal.d.ts.map +1 -0
  23. package/lib/cloudflare/bundle/cloudflare-internal.js +39 -0
  24. package/lib/cloudflare/bundle/cloudflare-internal.js.map +1 -0
  25. package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts +2 -0
  26. package/lib/cloudflare/bundle/nodejs-builtin-modules.d.ts.map +1 -0
  27. package/lib/cloudflare/bundle/nodejs-builtin-modules.js +9 -0
  28. package/lib/cloudflare/bundle/nodejs-builtin-modules.js.map +1 -0
  29. package/lib/cloudflare/bundle/plugin-als-external.d.ts +6 -0
  30. package/lib/cloudflare/bundle/plugin-als-external.d.ts.map +1 -0
  31. package/lib/cloudflare/bundle/plugin-als-external.js +12 -0
  32. package/lib/cloudflare/bundle/plugin-als-external.js.map +1 -0
  33. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts +4 -1
  34. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts.map +1 -1
  35. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js +23 -23
  36. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js.map +1 -1
  37. package/lib/cloudflare/bundle/plugin-node-compat.d.ts +11 -0
  38. package/lib/cloudflare/bundle/plugin-node-compat.d.ts.map +1 -0
  39. package/lib/cloudflare/bundle/plugin-node-compat.js +93 -0
  40. package/lib/cloudflare/bundle/plugin-node-compat.js.map +1 -0
  41. package/lib/cloudflare/bundle/plugin-wasm.d.ts.map +1 -1
  42. package/lib/cloudflare/bundle/plugin-wasm.js +20 -11
  43. package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -1
  44. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  45. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  46. package/lib/cloudflare/d1-local-migrations.js.map +1 -1
  47. package/lib/cloudflare/durable-object-namespace.d.ts +1 -0
  48. package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -1
  49. package/lib/cloudflare/durable-object-namespace.js.map +1 -1
  50. package/lib/cloudflare/hyperdrive.d.ts +7 -2
  51. package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
  52. package/lib/cloudflare/hyperdrive.js +39 -57
  53. package/lib/cloudflare/hyperdrive.js.map +1 -1
  54. package/lib/cloudflare/index.d.ts +1 -0
  55. package/lib/cloudflare/index.d.ts.map +1 -1
  56. package/lib/cloudflare/index.js +1 -0
  57. package/lib/cloudflare/index.js.map +1 -1
  58. package/lib/cloudflare/next.d.ts +13 -0
  59. package/lib/cloudflare/next.d.ts.map +1 -0
  60. package/lib/cloudflare/next.js +91 -0
  61. package/lib/cloudflare/next.js.map +1 -0
  62. package/lib/cloudflare/website.d.ts.map +1 -1
  63. package/lib/cloudflare/website.js +80 -82
  64. package/lib/cloudflare/website.js.map +1 -1
  65. package/lib/cloudflare/worker-assets.d.ts +3 -3
  66. package/lib/cloudflare/worker-assets.d.ts.map +1 -1
  67. package/lib/cloudflare/worker-assets.js +22 -47
  68. package/lib/cloudflare/worker-assets.js.map +1 -1
  69. package/lib/cloudflare/worker-bundle.d.ts +6 -0
  70. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  71. package/lib/cloudflare/worker-bundle.js +34 -10
  72. package/lib/cloudflare/worker-bundle.js.map +1 -1
  73. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  74. package/lib/cloudflare/worker-metadata.js +1 -0
  75. package/lib/cloudflare/worker-metadata.js.map +1 -1
  76. package/lib/cloudflare/worker.d.ts.map +1 -1
  77. package/lib/cloudflare/worker.js +24 -6
  78. package/lib/cloudflare/worker.js.map +1 -1
  79. package/lib/cloudflare/wrangler.json.d.ts +4 -6
  80. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  81. package/lib/cloudflare/wrangler.json.js +20 -15
  82. package/lib/cloudflare/wrangler.json.js.map +1 -1
  83. package/lib/context.d.ts +3 -1
  84. package/lib/context.d.ts.map +1 -1
  85. package/lib/context.js +2 -2
  86. package/lib/context.js.map +1 -1
  87. package/lib/destroy.d.ts +8 -0
  88. package/lib/destroy.d.ts.map +1 -1
  89. package/lib/destroy.js +20 -4
  90. package/lib/destroy.js.map +1 -1
  91. package/lib/planetscale/api/client.gen.d.ts +1750 -0
  92. package/lib/planetscale/api/client.gen.d.ts.map +1 -0
  93. package/lib/planetscale/api/client.gen.js +2401 -0
  94. package/lib/planetscale/api/client.gen.js.map +1 -0
  95. package/lib/planetscale/api/index.d.ts +3 -0
  96. package/lib/planetscale/api/index.d.ts.map +1 -0
  97. package/lib/planetscale/api/index.js +4 -0
  98. package/lib/planetscale/api/index.js.map +1 -0
  99. package/lib/planetscale/api/types.gen.d.ts +12156 -0
  100. package/lib/planetscale/api/types.gen.d.ts.map +1 -0
  101. package/lib/planetscale/api/types.gen.js +2 -0
  102. package/lib/planetscale/api/types.gen.js.map +1 -0
  103. package/lib/planetscale/branch.d.ts +5 -8
  104. package/lib/planetscale/branch.d.ts.map +1 -1
  105. package/lib/planetscale/branch.js +122 -97
  106. package/lib/planetscale/branch.js.map +1 -1
  107. package/lib/planetscale/database.d.ts +24 -11
  108. package/lib/planetscale/database.d.ts.map +1 -1
  109. package/lib/planetscale/database.js +163 -118
  110. package/lib/planetscale/database.js.map +1 -1
  111. package/lib/planetscale/index.d.ts +2 -1
  112. package/lib/planetscale/index.d.ts.map +1 -1
  113. package/lib/planetscale/index.js +2 -1
  114. package/lib/planetscale/index.js.map +1 -1
  115. package/lib/planetscale/password.d.ts +7 -8
  116. package/lib/planetscale/password.d.ts.map +1 -1
  117. package/lib/planetscale/password.js +74 -57
  118. package/lib/planetscale/password.js.map +1 -1
  119. package/lib/planetscale/role.d.ts +126 -0
  120. package/lib/planetscale/role.d.ts.map +1 -0
  121. package/lib/planetscale/role.js +144 -0
  122. package/lib/planetscale/role.js.map +1 -0
  123. package/lib/planetscale/utils.d.ts +23 -7
  124. package/lib/planetscale/utils.d.ts.map +1 -1
  125. package/lib/planetscale/utils.js +169 -75
  126. package/lib/planetscale/utils.js.map +1 -1
  127. package/lib/scope.d.ts +5 -1
  128. package/lib/scope.d.ts.map +1 -1
  129. package/lib/scope.js +11 -3
  130. package/lib/scope.js.map +1 -1
  131. package/lib/secret.d.ts +0 -3
  132. package/lib/secret.d.ts.map +1 -1
  133. package/lib/secret.js +0 -2
  134. package/lib/secret.js.map +1 -1
  135. package/lib/state/cloudflare-state-store.js +3 -3
  136. package/lib/state/cloudflare-state-store.js.map +1 -1
  137. package/lib/test/bun.d.ts +2 -2
  138. package/lib/test/bun.d.ts.map +1 -1
  139. package/lib/test/vitest.d.ts +2 -2
  140. package/lib/test/vitest.d.ts.map +1 -1
  141. package/lib/test/vitest.js +4 -4
  142. package/lib/test/vitest.js.map +1 -1
  143. package/lib/util/content-type.js +2 -2
  144. package/lib/util/content-type.js.map +1 -1
  145. package/lib/util/dedent.d.ts +3 -14
  146. package/lib/util/dedent.d.ts.map +1 -1
  147. package/lib/util/dedent.js +33 -45
  148. package/lib/util/dedent.js.map +1 -1
  149. package/lib/util/memoize.d.ts.map +1 -1
  150. package/lib/util/memoize.js +11 -1
  151. package/lib/util/memoize.js.map +1 -1
  152. package/package.json +8 -3
  153. package/src/alchemy.ts +14 -6
  154. package/src/apply.ts +13 -1
  155. package/src/cloudflare/bindings.ts +3 -1
  156. package/src/cloudflare/bucket.ts +3 -1
  157. package/src/cloudflare/bundle/cloudflare-internal.ts +42 -0
  158. package/src/cloudflare/bundle/nodejs-builtin-modules.ts +14 -0
  159. package/src/cloudflare/bundle/plugin-als-external.ts +13 -0
  160. package/src/cloudflare/bundle/plugin-hybrid-node-compat.ts +29 -28
  161. package/src/cloudflare/bundle/plugin-node-compat.ts +114 -0
  162. package/src/cloudflare/bundle/plugin-wasm.ts +34 -14
  163. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  164. package/src/cloudflare/d1-local-migrations.ts +2 -2
  165. package/src/cloudflare/durable-object-namespace.ts +1 -0
  166. package/src/cloudflare/hyperdrive.ts +136 -95
  167. package/src/cloudflare/index.ts +1 -0
  168. package/src/cloudflare/next.ts +123 -0
  169. package/src/cloudflare/website.ts +88 -89
  170. package/src/cloudflare/worker-assets.ts +43 -105
  171. package/src/cloudflare/worker-bundle.ts +46 -9
  172. package/src/cloudflare/worker-metadata.ts +2 -0
  173. package/src/cloudflare/worker.ts +35 -7
  174. package/src/cloudflare/wrangler.json.ts +114 -113
  175. package/src/context.ts +5 -3
  176. package/src/destroy.ts +26 -8
  177. package/src/planetscale/api/client.gen.ts +2882 -0
  178. package/src/planetscale/api/index.ts +4 -0
  179. package/src/planetscale/api/types.gen.ts +13041 -0
  180. package/src/planetscale/branch.ts +170 -162
  181. package/src/planetscale/database.ts +263 -243
  182. package/src/planetscale/index.ts +2 -1
  183. package/src/planetscale/password.ts +89 -86
  184. package/src/planetscale/role.ts +278 -0
  185. package/src/planetscale/utils.ts +289 -128
  186. package/src/scope.ts +12 -3
  187. package/src/secret.ts +0 -4
  188. package/src/state/cloudflare-state-store.ts +4 -4
  189. package/src/test/bun.ts +2 -2
  190. package/src/test/vitest.ts +6 -6
  191. package/src/util/content-type.ts +2 -2
  192. package/src/util/dedent.ts +34 -50
  193. package/src/util/memoize.ts +13 -1
  194. package/templates/astro/alchemy.run.ts +1 -3
  195. package/templates/hono/alchemy.run.ts +0 -1
  196. package/templates/nextjs/README.md +85 -0
  197. package/templates/nextjs/_env +1 -0
  198. package/templates/nextjs/_env.example +1 -0
  199. package/templates/nextjs/_gitignore +46 -0
  200. package/templates/nextjs/alchemy.run.ts +16 -0
  201. package/templates/nextjs/eslint.config.mjs +16 -0
  202. package/templates/nextjs/next-env.d.ts +5 -0
  203. package/templates/nextjs/next.config.ts +10 -0
  204. package/templates/nextjs/open-next.config.ts +5 -0
  205. package/templates/nextjs/package.json +31 -0
  206. package/templates/nextjs/src/app/api/hello/route.ts +19 -0
  207. package/templates/nextjs/src/app/favicon.ico +0 -0
  208. package/templates/nextjs/src/app/globals.css +72 -0
  209. package/templates/nextjs/src/app/layout.tsx +19 -0
  210. package/templates/nextjs/src/app/page.tsx +55 -0
  211. package/templates/nextjs/tsconfig.json +33 -0
  212. package/templates/nextjs/types/env.d.ts +14 -0
  213. package/templates/nuxt/alchemy.run.ts +1 -3
  214. package/templates/react-router/alchemy.run.ts +1 -3
  215. package/templates/rwsdk/alchemy.run.ts +0 -2
  216. package/templates/sveltekit/alchemy.run.ts +1 -3
  217. package/templates/tanstack-start/alchemy.run.ts +1 -3
  218. package/templates/typescript/alchemy.run.ts +0 -1
  219. package/templates/vite/alchemy.run.ts +0 -1
  220. package/workers/cloudflare-state-store.js +58 -58
  221. package/lib/cloudflare/bundle/externals.d.ts +0 -3
  222. package/lib/cloudflare/bundle/externals.d.ts.map +0 -1
  223. package/lib/cloudflare/bundle/externals.js +0 -35
  224. package/lib/cloudflare/bundle/externals.js.map +0 -1
  225. package/lib/cloudflare/bundle/plugin-compat-warning.d.ts +0 -10
  226. package/lib/cloudflare/bundle/plugin-compat-warning.d.ts.map +0 -1
  227. package/lib/cloudflare/bundle/plugin-compat-warning.js +0 -67
  228. package/lib/cloudflare/bundle/plugin-compat-warning.js.map +0 -1
  229. package/lib/planetscale/api.d.ts +0 -53
  230. package/lib/planetscale/api.d.ts.map +0 -1
  231. package/lib/planetscale/api.js +0 -84
  232. package/lib/planetscale/api.js.map +0 -1
  233. package/src/cloudflare/bundle/externals.ts +0 -36
  234. package/src/cloudflare/bundle/plugin-compat-warning.ts +0 -94
  235. package/src/planetscale/api.ts +0 -115
@@ -4,8 +4,16 @@ import { Resource } from "../resource.ts";
4
4
  import { Scope } from "../scope.ts";
5
5
  import { Secret } from "../secret.ts";
6
6
  import { logger } from "../util/logger.ts";
7
- import { handleApiError } from "./api-error.ts";
8
- import { createCloudflareApi, type CloudflareApiOptions } from "./api.ts";
7
+ import { CloudflareApiError, handleApiError } from "./api-error.ts";
8
+ import {
9
+ extractCloudflareResult,
10
+ type CloudflareApiErrorPayload,
11
+ } from "./api-response.ts";
12
+ import {
13
+ createCloudflareApi,
14
+ type CloudflareApi,
15
+ type CloudflareApiOptions,
16
+ } from "./api.ts";
9
17
 
10
18
  /**
11
19
  * Origin configuration for a PostgreSQL or MySQL database connection
@@ -163,6 +171,12 @@ export interface HyperdriveProps extends CloudflareApiOptions {
163
171
  */
164
172
  hyperdriveId?: string;
165
173
 
174
+ /**
175
+ * Whether to adopt an existing hyperdrive config
176
+ * @default false
177
+ */
178
+ adopt?: boolean;
179
+
166
180
  dev?: {
167
181
  /**
168
182
  * The database connection origin configuration for local development
@@ -176,46 +190,45 @@ export interface HyperdriveProps extends CloudflareApiOptions {
176
190
  * Output returned after Cloudflare Hyperdrive creation/update.
177
191
  * IMPORTANT: The interface name MUST match the exported resource name.
178
192
  */
179
- export interface Hyperdrive
180
- extends Resource<"cloudflare::Hyperdrive">,
181
- Omit<HyperdriveProps, "origin" | "dev"> {
182
- /**
183
- * The ID of the resource
184
- */
185
- id: string;
193
+ export type Hyperdrive = Resource<"cloudflare::Hyperdrive"> &
194
+ Omit<HyperdriveProps, "origin" | "dev"> & {
195
+ /**
196
+ * The ID of the resource
197
+ */
198
+ id: string;
186
199
 
187
- /**
188
- * Name of the Hyperdrive configuration
189
- */
190
- name: string;
200
+ /**
201
+ * Name of the Hyperdrive configuration
202
+ */
203
+ name: string;
191
204
 
192
- /**
193
- * The Cloudflare-generated UUID of the hyperdrive
194
- */
195
- hyperdriveId: string;
205
+ /**
206
+ * The Cloudflare-generated UUID of the hyperdrive
207
+ */
208
+ hyperdriveId: string;
196
209
 
197
- /**
198
- * Database connection origin configuration
199
- */
200
- origin: HyperdrivePublicOrigin | HyperdriveOriginWithAccess;
210
+ /**
211
+ * Database connection origin configuration
212
+ */
213
+ origin: HyperdrivePublicOrigin | HyperdriveOriginWithAccess;
201
214
 
202
- /**
203
- * Local development configuration
204
- * @internal
205
- */
206
- dev: {
207
215
  /**
208
- * The connection string to use for local development
216
+ * Local development configuration
217
+ * @internal
209
218
  */
210
- origin: Secret;
211
- };
219
+ dev: {
220
+ /**
221
+ * The connection string to use for local development
222
+ */
223
+ origin: Secret;
224
+ };
212
225
 
213
- /**
214
- * Resource type identifier for binding.
215
- * @internal
216
- */
217
- type: "hyperdrive";
218
- }
226
+ /**
227
+ * Resource type identifier for binding.
228
+ * @internal
229
+ */
230
+ type: "hyperdrive";
231
+ };
219
232
 
220
233
  /**
221
234
  * Represents a Cloudflare Hyperdrive configuration.
@@ -327,6 +340,7 @@ interface InternalHyperdriveProps extends CloudflareApiOptions {
327
340
  origin: Secret;
328
341
  force?: boolean;
329
342
  };
343
+ adopt?: boolean;
330
344
  }
331
345
 
332
346
  const _Hyperdrive = Resource(
@@ -337,7 +351,7 @@ const _Hyperdrive = Resource(
337
351
  props: InternalHyperdriveProps,
338
352
  ): Promise<Hyperdrive> {
339
353
  const hyperdriveId = props.hyperdriveId || this.output?.hyperdriveId;
340
-
354
+ const adopt = props.adopt || this.scope.adopt;
341
355
  const name =
342
356
  props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
343
357
 
@@ -354,13 +368,6 @@ const _Hyperdrive = Resource(
354
368
  });
355
369
  }
356
370
  const api = await createCloudflareApi(props);
357
- const configsPath = `/accounts/${api.accountId}/hyperdrive/configs`;
358
-
359
- // For create operations, we don't have a hyperdriveId yet
360
- // For update/delete operations, we need to use the hyperdriveId from props or output
361
- const configPath = hyperdriveId
362
- ? `${configsPath}/${hyperdriveId}`
363
- : `${configsPath}`;
364
371
 
365
372
  if (this.phase === "delete") {
366
373
  if (!hyperdriveId) {
@@ -369,7 +376,9 @@ const _Hyperdrive = Resource(
369
376
  }
370
377
 
371
378
  try {
372
- const deleteResponse = await api.delete(configPath);
379
+ const deleteResponse = await api.delete(
380
+ `/accounts/${api.accountId}/hyperdrive/configs/${hyperdriveId}`,
381
+ );
373
382
  // Only swallow 404 Not Found errors, all other errors should be handled
374
383
  if (!deleteResponse.ok && deleteResponse.status !== 404) {
375
384
  await handleApiError(deleteResponse, "delete", "hyperdrive", id);
@@ -381,75 +390,107 @@ const _Hyperdrive = Resource(
381
390
  return this.destroy();
382
391
  }
383
392
 
384
- let response: Response | undefined;
385
- let apiResource: any;
386
-
387
393
  // Prepare request body with unwrapped secrets
388
- const requestBody = prepareRequestBody(props);
389
-
390
- try {
391
- if (this.phase === "update" && hyperdriveId) {
392
- // Update existing hyperdrive
393
- response = await api.put(configPath, requestBody);
394
- } else {
395
- // Create new hyperdrive
396
- if (hyperdriveId) {
397
- // If we have a hyperdriveId but we're in create phase, it could be because
398
- // the resource exists but wasn't in state. Do a GET to check.
399
- const getResponse = await api.get(configPath);
400
- if (getResponse.status === 200) {
401
- // Hyperdrive exists, update it
402
- logger.log(
403
- `Hyperdrive '${id}' already exists. Updating existing resource.`,
394
+ const requestBody = prepareRequestBody({ ...props, name });
395
+ let result: HyperdriveResponse;
396
+
397
+ if (hyperdriveId) {
398
+ result = await extractCloudflareResult<HyperdriveResponse>(
399
+ `update hyperdrive config "${hyperdriveId}"`,
400
+ api.put(
401
+ `/accounts/${api.accountId}/hyperdrive/configs/${hyperdriveId}`,
402
+ requestBody,
403
+ ),
404
+ );
405
+ } else {
406
+ try {
407
+ result = await extractCloudflareResult<HyperdriveResponse>(
408
+ `create hyperdrive config "${name}"`,
409
+ api.post(
410
+ `/accounts/${api.accountId}/hyperdrive/configs`,
411
+ requestBody,
412
+ ),
413
+ );
414
+ } catch (error) {
415
+ if (
416
+ error instanceof CloudflareApiError &&
417
+ (error.errorData as CloudflareApiErrorPayload[]).some(
418
+ (e) => e.code === 2017, // 2017 is the error code for name conflict
419
+ )
420
+ ) {
421
+ if (!adopt) {
422
+ throw new Error(
423
+ `Hyperdrive config "${name}" already exists. Use adopt: true to adopt it.`,
424
+ { cause: error },
425
+ );
426
+ }
427
+ const existing = await findHyperdriveConfigByName(api, name);
428
+ if (!existing) {
429
+ throw new Error(
430
+ `Hyperdrive config "${name}" failed to create due to name conflict and could not be found for adoption.`,
431
+ { cause: error },
404
432
  );
405
- response = await api.put(configPath, requestBody);
406
- } else if (getResponse.status === 404) {
407
- // Hyperdrive doesn't exist, create new
408
- response = await api.post(configsPath, {
409
- ...requestBody,
410
- // Ensure name is set correctly if not already set
411
- name: props.name || id,
412
- });
413
- } else {
414
- // Unexpected error during GET check
415
- await handleApiError(getResponse, "get", "hyperdrive", id);
416
433
  }
434
+ result = await extractCloudflareResult<HyperdriveResponse>(
435
+ `adopt hyperdrive config "${name}"`,
436
+ api.put(
437
+ `/accounts/${api.accountId}/hyperdrive/configs/${existing.id}`,
438
+ requestBody,
439
+ ),
440
+ );
417
441
  } else {
418
- // No hyperdriveId, create new
419
- response = await api.post(configsPath, {
420
- ...requestBody,
421
- // Ensure name is set correctly if not already set
422
- name: props.name || id,
423
- });
442
+ throw error;
424
443
  }
425
444
  }
426
-
427
- if (!response?.ok) {
428
- const action = this.phase === "update" ? "update" : "create";
429
- await handleApiError(response!, action, "hyperdrive", id);
430
- }
431
-
432
- const data: { result: Record<string, any> } = await response!.json();
433
- apiResource = data.result;
434
- } catch (error) {
435
- logger.error(`Error ${this.phase} Hyperdrive '${id}':`, error);
436
- throw error;
437
445
  }
438
446
 
439
447
  // Construct the output object from API response and props
440
448
  return this({
441
449
  id,
442
- hyperdriveId: apiResource.id, // Store the Cloudflare-assigned UUID
443
- name: apiResource.name,
450
+ hyperdriveId: result.id, // Store the Cloudflare-assigned UUID
451
+ name: result.name,
444
452
  origin: props.origin,
445
- caching: apiResource.caching,
446
- mtls: apiResource.mtls,
453
+ caching: result.caching,
454
+ mtls: result.mtls,
447
455
  dev: props.dev,
448
456
  type: "hyperdrive",
449
457
  });
450
458
  },
451
459
  );
452
460
 
461
+ interface HyperdriveResponse {
462
+ id: string;
463
+ name: string;
464
+ origin: HyperdriveOrigin;
465
+ caching?: HyperdriveCaching;
466
+ created_on?: string;
467
+ modified_on?: string;
468
+ mtls?: HyperdriveMtls;
469
+ origin_connection_limit?: number;
470
+ }
471
+
472
+ async function findHyperdriveConfigByName(
473
+ api: CloudflareApi,
474
+ name: string,
475
+ page = 1,
476
+ ) {
477
+ const response = await api.get(
478
+ `/accounts/${api.accountId}/hyperdrive/configs?page=${page}`,
479
+ );
480
+ const data: {
481
+ result: HyperdriveResponse[];
482
+ result_info?: { total_pages?: number };
483
+ } = await response.json();
484
+ const found = data.result.find((config) => config.name === name);
485
+ if (found) {
486
+ return found;
487
+ }
488
+ if (data.result_info?.total_pages && page < data.result_info.total_pages) {
489
+ return await findHyperdriveConfigByName(api, name, page + 1);
490
+ }
491
+ return null;
492
+ }
493
+
453
494
  /**
454
495
  * Prepare the request body by unwrapping secret values
455
496
  */
@@ -33,6 +33,7 @@ export * from "./email-rule.ts";
33
33
  export * from "./hyperdrive.ts";
34
34
  export * from "./images.ts";
35
35
  export * from "./kv-namespace.ts";
36
+ export * from "./next.ts";
36
37
  export * from "./nuxt/nuxt.ts";
37
38
  export * from "./orange.ts";
38
39
  export * from "./permission-groups.ts";
@@ -0,0 +1,123 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { bold, cyan } from "picocolors";
4
+ import { getPackageManagerRunner } from "../util/detect-package-manager.ts";
5
+ import { exists } from "../util/exists.ts";
6
+ import ignore from "../util/ignore-matcher.ts";
7
+ import { logger } from "../util/logger.ts";
8
+ import type { Assets } from "./assets.ts";
9
+ import type { Bindings } from "./bindings.ts";
10
+ import { Website, type WebsiteProps } from "./website.ts";
11
+ import type { Worker } from "./worker.ts";
12
+
13
+ export interface NextjsProps<B extends Bindings> extends WebsiteProps<B> {}
14
+
15
+ export type Nextjs<B extends Bindings> = B extends { ASSETS: any }
16
+ ? never
17
+ : Worker<B & { ASSETS: Assets }>;
18
+
19
+ export async function Nextjs<const B extends Bindings>(
20
+ id: string,
21
+ props: NextjsProps<B> = {},
22
+ ): Promise<Nextjs<B>> {
23
+ const runner = await getPackageManagerRunner();
24
+ const isIgnored = await isFileIgnored("wrangler.jsonc", props.cwd);
25
+ if (!isIgnored) {
26
+ logger.warn(
27
+ [
28
+ bold(
29
+ `The ${cyan("wrangler.jsonc")} file for Next.js ${cyan(`"${id}"`)} is not ignored.`,
30
+ ),
31
+ "This may cause unexpected behavior. Please add the file to your .gitignore.",
32
+ ].join(" "),
33
+ );
34
+ }
35
+ return await Website(id, {
36
+ ...props,
37
+ entrypoint: props.entrypoint ?? ".open-next/worker.js",
38
+ wrangler: {
39
+ path: "wrangler.jsonc",
40
+ secrets: isIgnored,
41
+ ...(props.wrangler ?? {}),
42
+ },
43
+ build: normalizeCommand(props.build, {
44
+ command: `${runner} opennextjs-cloudflare build`,
45
+ env: {
46
+ NEXTJS_ENV: "production",
47
+ },
48
+ }),
49
+ dev: normalizeCommand(props.dev, {
50
+ command: `${runner} next dev`,
51
+ env: {
52
+ NEXTJS_ENV: "development",
53
+ },
54
+ }),
55
+
56
+ // OpenNext generates the files, but relies on us to bundle them.
57
+ noBundle: props.noBundle ?? false,
58
+
59
+ spa: false,
60
+ compatibilityFlags: [
61
+ "nodejs_compat",
62
+ "global_fetch_strictly_public",
63
+ ...(props.compatibilityFlags ?? []),
64
+ ],
65
+ assets: props.assets ?? ".open-next/assets",
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Check if a file is ignored by .gitignore.
71
+ *
72
+ * @param filename - The name of the file to check.
73
+ * @param cwd - The current working directory.
74
+ * @returns True if the file is ignored, false otherwise.
75
+ */
76
+ const isFileIgnored = async (filename: string, cwd?: string) => {
77
+ const matcher = ignore();
78
+
79
+ // Read .gitignore from current directory and walk up the directory tree
80
+ let currentDir = path.resolve(cwd ?? process.cwd());
81
+ const gitignorePatterns: string[] = [];
82
+
83
+ while (currentDir !== path.dirname(currentDir)) {
84
+ const gitignorePath = path.join(currentDir, ".gitignore");
85
+ try {
86
+ const gitignoreContent = await fs.readFile(gitignorePath, "utf8");
87
+ // unshift to prioritize local .gitignore
88
+ gitignorePatterns.unshift(
89
+ ...gitignoreContent
90
+ .split("\n")
91
+ .filter((line) => line.trim() && !line.startsWith("#")),
92
+ );
93
+ } catch {
94
+ // .gitignore doesn't exist in this directory, continue up
95
+ }
96
+ if (await exists(path.join(currentDir, ".git"))) {
97
+ break;
98
+ }
99
+ currentDir = path.dirname(currentDir);
100
+ }
101
+
102
+ matcher.add(gitignorePatterns);
103
+
104
+ return matcher.ignores(filename);
105
+ };
106
+
107
+ const normalizeCommand = (
108
+ input: WebsiteProps<any>["build"],
109
+ defaults: {
110
+ command: string;
111
+ env: Record<string, string>;
112
+ },
113
+ ): WebsiteProps<any>["build"] => {
114
+ return {
115
+ command:
116
+ typeof input === "string" ? input : (input?.command ?? defaults.command),
117
+ env: {
118
+ ...defaults.env,
119
+ ...(typeof input === "object" ? (input?.env ?? {}) : {}),
120
+ },
121
+ memoize: typeof input === "object" ? input?.memoize : undefined,
122
+ };
123
+ };
@@ -1,7 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { alchemy } from "../alchemy.ts";
5
4
  import { Exec } from "../os/index.ts";
6
5
  import { Scope } from "../scope.ts";
7
6
  import { isSecret } from "../secret.ts";
@@ -255,101 +254,101 @@ export async function Website<B extends Bindings>(
255
254
  entrypoint: path.relative(paths.cwd, paths.entrypoint),
256
255
  } as WorkerProps<B> & { name: string };
257
256
 
258
- return await alchemy.run(id, { parent: Scope.current }, async (scope) => {
259
- if (!workerProps.entrypoint) {
260
- await fs.mkdir(path.dirname(paths.entrypoint), { recursive: true });
261
- const content =
262
- script ??
263
- dedent`
264
- export default {
265
- async fetch(request, env) {
266
- return new Response("Not Found", { status: 404 });
267
- },
268
- };`;
269
- await fs.writeFile(paths.entrypoint, content);
270
- }
257
+ if (!workerProps.entrypoint) {
258
+ await fs.mkdir(path.dirname(paths.entrypoint), { recursive: true });
259
+ const content =
260
+ script ??
261
+ dedent`
262
+ export default {
263
+ async fetch(request, env) {
264
+ return new Response("Not Found", { status: 404 });
265
+ },
266
+ };`;
267
+ await fs.writeFile(paths.entrypoint, content);
268
+ }
271
269
 
272
- await writeMiniflareSymlink(paths.cwd);
270
+ await writeMiniflareSymlink(paths.cwd);
273
271
 
274
- await WranglerJson("wrangler.jsonc", {
275
- path: path.relative(paths.cwd, paths.wrangler.path),
276
- worker,
277
- assets: {
278
- binding: "ASSETS",
279
- directory: path.relative(paths.cwd, paths.assets),
280
- },
281
- main: path.relative(paths.cwd, paths.wrangler.main),
282
- secrets,
283
- transform: {
284
- wrangler: props.wrangler?.transform,
285
- },
286
- });
272
+ await WranglerJson({
273
+ path: path.relative(paths.cwd, paths.wrangler.path),
274
+ worker,
275
+ assets: {
276
+ binding: "ASSETS",
277
+ directory: path.relative(paths.cwd, paths.assets),
278
+ },
279
+ main: path.relative(paths.cwd, paths.wrangler.main),
280
+ secrets,
281
+ transform: {
282
+ wrangler: props.wrangler?.transform,
283
+ },
284
+ });
287
285
 
288
- if (build && !scope.local) {
289
- await Exec("build", {
290
- cwd: path.relative(process.cwd(), paths.cwd),
291
- command: build.command,
292
- env: {
293
- ...env,
294
- ...(typeof build === "object" ? build.env : {}),
295
- NODE_ENV: "production",
296
- },
297
- memoize: typeof build === "object" ? build.memoize : undefined,
298
- });
299
- }
286
+ const scope = Scope.current;
300
287
 
301
- let url: string | undefined;
302
- if (dev && scope.local) {
303
- url = await scope.spawn(name, {
304
- cmd: typeof dev === "string" ? dev : dev.command,
305
- cwd: paths.cwd,
306
- extract: (line) => {
307
- const URL_REGEX =
308
- /http:\/\/(localhost|0\.0\.0\.0|127\.0\.0\.1|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d+\/?/;
309
- const match = line
310
- .replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
311
- .match(URL_REGEX);
312
- if (match) {
313
- return match[0];
314
- }
315
- },
316
- env: {
317
- ...Object.fromEntries(
318
- Object.entries(env ?? {}).flatMap(([key, value]) => {
319
- if (isSecret(value)) {
320
- return [[key, value.unencrypted]];
321
- }
322
- if (typeof value === "string") {
323
- return [[key, value]];
324
- }
325
- return [];
326
- }),
327
- ),
328
- ...(typeof dev === "object" ? dev.env : {}),
329
- FORCE_COLOR: "1",
330
- ...process.env,
331
- // NOTE: we must set this to ensure the user does not accidentally set `NODE_ENV=production`
332
- // which breaks `vite dev` (it won't, for example, re-write `process.env.TSS_APP_BASE` in the `.js` client side bundle)
333
- NODE_ENV: "development",
334
- },
335
- });
336
- }
288
+ if (build && !scope.local) {
289
+ await Exec(`${id}-build`, {
290
+ cwd: path.relative(process.cwd(), paths.cwd),
291
+ command: build.command,
292
+ env: {
293
+ ...env,
294
+ ...(typeof build === "object" ? build.env : {}),
295
+ NODE_ENV: "production",
296
+ },
297
+ memoize: typeof build === "object" ? build.memoize : undefined,
298
+ });
299
+ }
337
300
 
338
- return (await Worker("worker", {
339
- ...worker,
340
- bindings: {
341
- ...worker.bindings,
342
- ...(!scope.local
343
- ? {
344
- ASSETS: await Assets("assets", {
345
- path: path.relative(process.cwd(), paths.assets),
346
- }),
301
+ let url: string | undefined;
302
+ if (dev && scope.local) {
303
+ url = await scope.spawn(name, {
304
+ cmd: typeof dev === "string" ? dev : dev.command,
305
+ cwd: paths.cwd,
306
+ extract: (line) => {
307
+ const URL_REGEX =
308
+ /http:\/\/(localhost|0\.0\.0\.0|127\.0\.0\.1|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d+\/?/;
309
+ const match = line
310
+ .replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
311
+ .match(URL_REGEX);
312
+ if (match) {
313
+ return match[0];
314
+ }
315
+ },
316
+ env: {
317
+ ...Object.fromEntries(
318
+ Object.entries(env ?? {}).flatMap(([key, value]) => {
319
+ if (isSecret(value)) {
320
+ return [[key, value.unencrypted]];
321
+ }
322
+ if (typeof value === "string") {
323
+ return [[key, value]];
347
324
  }
348
- : {}),
325
+ return [];
326
+ }),
327
+ ),
328
+ ...(typeof dev === "object" ? dev.env : {}),
329
+ FORCE_COLOR: "1",
330
+ ...process.env,
331
+ // NOTE: we must set this to ensure the user does not accidentally set `NODE_ENV=production`
332
+ // which breaks `vite dev` (it won't, for example, re-write `process.env.TSS_APP_BASE` in the `.js` client side bundle)
333
+ NODE_ENV: "development",
349
334
  },
350
- dev: url ? { url } : undefined,
351
- })) as Website<B>;
352
- });
335
+ });
336
+ }
337
+
338
+ return (await Worker(id, {
339
+ ...worker,
340
+ bindings: {
341
+ ...worker.bindings,
342
+ ...(!scope.local
343
+ ? {
344
+ ASSETS: await Assets("assets", {
345
+ path: path.relative(process.cwd(), paths.assets),
346
+ }),
347
+ }
348
+ : {}),
349
+ },
350
+ dev: url ? { url } : undefined,
351
+ })) as Website<B>;
353
352
  }
354
353
 
355
354
  async function writeMiniflareSymlink(cwd: string) {