alchemy 0.68.0 → 0.69.1

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 (181) hide show
  1. package/bin/alchemy.js +32 -33
  2. package/bin/commands/init.ts +41 -55
  3. package/lib/aws/control/properties.d.ts +64 -0
  4. package/lib/aws/control/properties.d.ts.map +1 -1
  5. package/lib/aws/control/properties.js +554 -39
  6. package/lib/aws/control/properties.js.map +1 -1
  7. package/lib/aws/control/resource.d.ts.map +1 -1
  8. package/lib/aws/control/resource.js +33 -0
  9. package/lib/aws/control/resource.js.map +1 -1
  10. package/lib/aws/control/types.d.ts +5451 -1587
  11. package/lib/aws/control/types.d.ts.map +1 -1
  12. package/lib/aws/control/types.js.map +1 -1
  13. package/lib/aws/control/update-types.d.ts +12913 -0
  14. package/lib/aws/control/update-types.d.ts.map +1 -0
  15. package/lib/aws/control/update-types.js +12916 -0
  16. package/lib/aws/control/update-types.js.map +1 -0
  17. package/lib/cloudflare/api.d.ts +1 -1
  18. package/lib/cloudflare/api.d.ts.map +1 -1
  19. package/lib/cloudflare/api.js +9 -10
  20. package/lib/cloudflare/api.js.map +1 -1
  21. package/lib/cloudflare/astro/astro.d.ts.map +1 -1
  22. package/lib/cloudflare/astro/astro.js +3 -3
  23. package/lib/cloudflare/astro/astro.js.map +1 -1
  24. package/lib/cloudflare/bucket-object.d.ts +86 -0
  25. package/lib/cloudflare/bucket-object.d.ts.map +1 -0
  26. package/lib/cloudflare/bucket-object.js +67 -0
  27. package/lib/cloudflare/bucket-object.js.map +1 -0
  28. package/lib/cloudflare/bucket.d.ts +36 -6
  29. package/lib/cloudflare/bucket.d.ts.map +1 -1
  30. package/lib/cloudflare/bucket.js +154 -18
  31. package/lib/cloudflare/bucket.js.map +1 -1
  32. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  33. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  34. package/lib/cloudflare/container.d.ts.map +1 -1
  35. package/lib/cloudflare/container.js +2 -2
  36. package/lib/cloudflare/container.js.map +1 -1
  37. package/lib/cloudflare/index.d.ts +1 -0
  38. package/lib/cloudflare/index.d.ts.map +1 -1
  39. package/lib/cloudflare/index.js +1 -0
  40. package/lib/cloudflare/index.js.map +1 -1
  41. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  42. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +44 -12
  43. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  44. package/lib/cloudflare/next.js.map +1 -1
  45. package/lib/cloudflare/nuxt/nuxt.d.ts.map +1 -1
  46. package/lib/cloudflare/nuxt/nuxt.js +3 -3
  47. package/lib/cloudflare/nuxt/nuxt.js.map +1 -1
  48. package/lib/cloudflare/tanstack-start/plugin.d.ts +2 -15
  49. package/lib/cloudflare/tanstack-start/plugin.d.ts.map +1 -1
  50. package/lib/cloudflare/tanstack-start/plugin.js +5 -25
  51. package/lib/cloudflare/tanstack-start/plugin.js.map +1 -1
  52. package/lib/cloudflare/tanstack-start/tanstack-start.d.ts.map +1 -1
  53. package/lib/cloudflare/tanstack-start/tanstack-start.js +15 -4
  54. package/lib/cloudflare/tanstack-start/tanstack-start.js.map +1 -1
  55. package/lib/cloudflare/vite/vite.d.ts.map +1 -1
  56. package/lib/cloudflare/vite/vite.js +3 -3
  57. package/lib/cloudflare/vite/vite.js.map +1 -1
  58. package/lib/cloudflare/website.d.ts +7 -1
  59. package/lib/cloudflare/website.d.ts.map +1 -1
  60. package/lib/cloudflare/website.js +20 -1
  61. package/lib/cloudflare/website.js.map +1 -1
  62. package/lib/cloudflare/worker-metadata.d.ts +4 -1
  63. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  64. package/lib/cloudflare/worker-metadata.js +2 -0
  65. package/lib/cloudflare/worker-metadata.js.map +1 -1
  66. package/lib/cloudflare/worker.d.ts +6 -0
  67. package/lib/cloudflare/worker.d.ts.map +1 -1
  68. package/lib/cloudflare/worker.js +1 -0
  69. package/lib/cloudflare/worker.js.map +1 -1
  70. package/lib/cloudflare/wrangler.json.d.ts +34 -9
  71. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  72. package/lib/cloudflare/wrangler.json.js +12 -1
  73. package/lib/cloudflare/wrangler.json.js.map +1 -1
  74. package/lib/docker/api.d.ts +22 -1
  75. package/lib/docker/api.d.ts.map +1 -1
  76. package/lib/docker/api.js +64 -2
  77. package/lib/docker/api.js.map +1 -1
  78. package/lib/docker/container.d.ts +88 -0
  79. package/lib/docker/container.d.ts.map +1 -1
  80. package/lib/docker/container.js +36 -0
  81. package/lib/docker/container.js.map +1 -1
  82. package/lib/llms.cloudflare.txt +2 -2
  83. package/lib/neon/branch.d.ts +19 -3
  84. package/lib/neon/branch.d.ts.map +1 -1
  85. package/lib/neon/branch.js +4 -4
  86. package/lib/neon/branch.js.map +1 -1
  87. package/lib/neon/project.d.ts +17 -1
  88. package/lib/neon/project.d.ts.map +1 -1
  89. package/lib/neon/project.js +4 -4
  90. package/lib/neon/project.js.map +1 -1
  91. package/lib/serde.d.ts +3 -0
  92. package/lib/serde.d.ts.map +1 -1
  93. package/lib/serde.js +40 -0
  94. package/lib/serde.js.map +1 -1
  95. package/lib/test/bun.js.map +1 -1
  96. package/package.json +1 -1
  97. package/src/aws/control/properties.ts +554 -39
  98. package/src/aws/control/resource.ts +58 -0
  99. package/src/aws/control/types.ts +85281 -84914
  100. package/src/aws/control/update-types.ts +12917 -0
  101. package/src/cloudflare/api.ts +9 -10
  102. package/src/cloudflare/astro/astro.ts +8 -3
  103. package/src/cloudflare/bucket-object.ts +127 -0
  104. package/src/cloudflare/bucket.ts +218 -18
  105. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  106. package/src/cloudflare/container.ts +4 -5
  107. package/src/cloudflare/index.ts +1 -0
  108. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +57 -13
  109. package/src/cloudflare/next.ts +2 -2
  110. package/src/cloudflare/nuxt/nuxt.ts +8 -3
  111. package/src/cloudflare/tanstack-start/plugin.ts +7 -28
  112. package/src/cloudflare/tanstack-start/tanstack-start.ts +15 -4
  113. package/src/cloudflare/vite/vite.ts +8 -3
  114. package/src/cloudflare/website.ts +29 -2
  115. package/src/cloudflare/worker-metadata.ts +6 -1
  116. package/src/cloudflare/worker.ts +7 -0
  117. package/src/cloudflare/wrangler.json.ts +47 -18
  118. package/src/docker/api.ts +86 -2
  119. package/src/docker/container.ts +97 -0
  120. package/src/llms.cloudflare.txt +2 -2
  121. package/src/neon/branch.ts +6 -5
  122. package/src/neon/project.ts +4 -4
  123. package/src/serde.ts +50 -0
  124. package/src/test/bun.ts +1 -1
  125. package/templates/rwsdk/.cursor/rules/alchemy_cloudflare.mdc +2 -2
  126. package/templates/tanstack-start/.cta.json +13 -0
  127. package/templates/tanstack-start/README.md +267 -49
  128. package/templates/tanstack-start/_gitignore +9 -18
  129. package/templates/tanstack-start/alchemy.run.ts +2 -2
  130. package/templates/tanstack-start/package.json +24 -19
  131. package/templates/tanstack-start/public/favicon.ico +0 -0
  132. package/templates/tanstack-start/public/logo192.png +0 -0
  133. package/templates/tanstack-start/public/logo512.png +0 -0
  134. package/templates/tanstack-start/public/manifest.json +25 -0
  135. package/templates/tanstack-start/public/robots.txt +3 -0
  136. package/templates/tanstack-start/src/components/Header.tsx +21 -0
  137. package/templates/tanstack-start/src/logo.svg +12 -0
  138. package/templates/tanstack-start/src/router.tsx +7 -16
  139. package/templates/tanstack-start/src/routes/__root.tsx +13 -114
  140. package/templates/tanstack-start/src/routes/api.demo-names.ts +15 -0
  141. package/templates/tanstack-start/src/routes/demo.start.api-request.tsx +44 -0
  142. package/templates/tanstack-start/src/routes/demo.start.server-funcs.tsx +97 -0
  143. package/templates/tanstack-start/src/routes/index.tsx +31 -4
  144. package/templates/tanstack-start/src/styles.css +15 -0
  145. package/templates/tanstack-start/tsconfig.json +19 -22
  146. package/templates/tanstack-start/types/env.d.ts +2 -2
  147. package/templates/tanstack-start/vite.config.ts +16 -25
  148. package/workers/tunnel-proxy.js +1 -1
  149. package/templates/tanstack-start/_prettierignore +0 -4
  150. package/templates/tanstack-start/public/android-chrome-192x192.png +0 -0
  151. package/templates/tanstack-start/public/android-chrome-512x512.png +0 -0
  152. package/templates/tanstack-start/public/apple-touch-icon.png +0 -0
  153. package/templates/tanstack-start/public/favicon-16x16.png +0 -0
  154. package/templates/tanstack-start/public/favicon-32x32.png +0 -0
  155. package/templates/tanstack-start/public/favicon.png +0 -0
  156. package/templates/tanstack-start/public/site.webmanifest +0 -19
  157. package/templates/tanstack-start/src/components/DefaultCatchBoundary.tsx +0 -53
  158. package/templates/tanstack-start/src/components/NotFound.tsx +0 -25
  159. package/templates/tanstack-start/src/components/PostError.tsx +0 -5
  160. package/templates/tanstack-start/src/components/UserError.tsx +0 -5
  161. package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-a.tsx +0 -10
  162. package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-b.tsx +0 -10
  163. package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout.tsx +0 -34
  164. package/templates/tanstack-start/src/routes/_pathlessLayout.tsx +0 -16
  165. package/templates/tanstack-start/src/routes/api/users.$userId.ts +0 -28
  166. package/templates/tanstack-start/src/routes/api/users.ts +0 -59
  167. package/templates/tanstack-start/src/routes/customScript[.]js.ts +0 -10
  168. package/templates/tanstack-start/src/routes/deferred.tsx +0 -62
  169. package/templates/tanstack-start/src/routes/posts.$postId.tsx +0 -34
  170. package/templates/tanstack-start/src/routes/posts.index.tsx +0 -8
  171. package/templates/tanstack-start/src/routes/posts.tsx +0 -38
  172. package/templates/tanstack-start/src/routes/posts_.$postId.deep.tsx +0 -29
  173. package/templates/tanstack-start/src/routes/redirect.tsx +0 -9
  174. package/templates/tanstack-start/src/routes/users.$userId.tsx +0 -44
  175. package/templates/tanstack-start/src/routes/users.index.tsx +0 -18
  176. package/templates/tanstack-start/src/routes/users.tsx +0 -49
  177. package/templates/tanstack-start/src/styles/app.css +0 -12
  178. package/templates/tanstack-start/src/utils/loggingMiddleware.tsx +0 -41
  179. package/templates/tanstack-start/src/utils/posts.tsx +0 -40
  180. package/templates/tanstack-start/src/utils/seo.ts +0 -33
  181. package/templates/tanstack-start/src/utils/users.tsx +0 -5
@@ -31,10 +31,7 @@ export async function createMiniflareWorkerProxy(options: {
31
31
  });
32
32
  server.on("request", async (req, res) => {
33
33
  try {
34
- const request = toMiniflareRequest(req);
35
- const name = options.getWorkerName(request);
36
- const worker = await options.miniflare.getWorker(name);
37
- const response = await worker.fetch(request);
34
+ const response = await handleFetch(req);
38
35
  writeMiniflareResponseToNode(response, res);
39
36
  } catch (error) {
40
37
  console.error(error);
@@ -45,20 +42,67 @@ export async function createMiniflareWorkerProxy(options: {
45
42
  }
46
43
  });
47
44
 
48
- const toMiniflareRequest = (req: http.IncomingMessage): miniflare.Request => {
45
+ const handleFetch = async (
46
+ req: http.IncomingMessage,
47
+ ): Promise<miniflare.Response> => {
48
+ const { request, url } = toMiniflareRequest(req);
49
+ const name = options.getWorkerName(request);
50
+ const worker = await options.miniflare.getWorker(name);
51
+
52
+ // Handle scheduled events
53
+ // https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/#background
54
+ // https://github.com/cloudflare/workers-sdk/blob/7d53b9ab6b370944b7934ad51ebef43160c3c775/packages/wrangler/templates/middleware/middleware-scheduled.ts#L6
55
+ if (url.pathname === "/__scheduled") {
56
+ await worker.scheduled({
57
+ scheduledTime: new Date(),
58
+ cron: url.searchParams.get("cron") ?? undefined,
59
+ });
60
+ return new miniflare.Response("Ran scheduled function");
61
+ }
62
+
63
+ const result = await worker
64
+ .fetch(request)
65
+ .then((response) => ({ success: true, response }) as const)
66
+ .catch((error) => ({ success: false, error }) as const);
67
+
68
+ // If you open the `/__scheduled` page in a browser, the browser will automatically make a request to `/favicon.ico`.
69
+ // For scheduled Workers _without_ a fetch handler, this will result in an unhelpful error that we don't need to log.
70
+ // To avoid this, inject a 404 response to favicon.ico loads on the `/__scheduled` page
71
+ if (
72
+ request.headers.get("referer")?.endsWith("/__scheduled") &&
73
+ url.pathname === "/favicon.ico" &&
74
+ !result.success
75
+ ) {
76
+ return new miniflare.Response(null, { status: 404 });
77
+ }
78
+
79
+ if (!result.success) {
80
+ throw result.error;
81
+ }
82
+
83
+ return result.response;
84
+ };
85
+
86
+ const toMiniflareRequest = (
87
+ req: http.IncomingMessage,
88
+ ): { request: miniflare.Request; url: URL } => {
49
89
  const info = parseIncomingMessage(req);
50
90
  options.transformRequest?.(info);
51
- return new miniflare.Request(info.url, {
52
- method: info.method,
53
- headers: info.headers,
54
- body: info.body,
55
- redirect: info.redirect,
56
- duplex: info.duplex,
57
- });
91
+ return {
92
+ request: new miniflare.Request(info.url, {
93
+ method: info.method,
94
+ headers: info.headers,
95
+ body: info.body,
96
+ redirect: info.redirect,
97
+ duplex: info.duplex,
98
+ }),
99
+ url: info.url,
100
+ };
58
101
  };
59
102
 
60
103
  const createServerWebSocket = async (req: http.IncomingMessage) => {
61
- const name = options.getWorkerName(toMiniflareRequest(req));
104
+ const { request } = toMiniflareRequest(req);
105
+ const name = options.getWorkerName(request);
62
106
  const target = await options.miniflare.unsafeGetDirectURL(name);
63
107
  const url = new URL(req.url ?? "/", target);
64
108
  url.protocol = url.protocol.replace("http", "ws");
@@ -113,7 +113,7 @@ const normalizeCommand = (
113
113
  command: string;
114
114
  env: Record<string, string>;
115
115
  },
116
- ): WebsiteProps<any>["build"] => {
116
+ ) => {
117
117
  return {
118
118
  command:
119
119
  typeof input === "string" ? input : (input?.command ?? defaults.command),
@@ -122,5 +122,5 @@ const normalizeCommand = (
122
122
  ...(typeof input === "object" ? (input?.env ?? {}) : {}),
123
123
  },
124
124
  memoize: typeof input === "object" ? input?.memoize : undefined,
125
- };
125
+ } satisfies WebsiteProps<any>["build"];
126
126
  };
@@ -2,7 +2,12 @@ import path from "node:path";
2
2
  import { getPackageManagerRunner } from "../../util/detect-package-manager.ts";
3
3
  import type { Assets } from "../assets.ts";
4
4
  import type { Bindings } from "../bindings.ts";
5
- import { Website, type WebsiteProps } from "../website.ts";
5
+ import {
6
+ spreadBuildProps,
7
+ spreadDevProps,
8
+ Website,
9
+ type WebsiteProps,
10
+ } from "../website.ts";
6
11
  import type { Worker } from "../worker.ts";
7
12
 
8
13
  /**
@@ -55,8 +60,8 @@ export async function Nuxt<B extends Bindings>(
55
60
  return await Website(id, {
56
61
  ...props,
57
62
  noBundle: props?.noBundle ?? true,
58
- build: props?.build ?? `${runner} nuxt build`,
59
- dev: props?.dev ?? `${runner} nuxt dev`,
63
+ build: spreadBuildProps(props, `${runner} nuxt build`),
64
+ dev: spreadDevProps(props, `${runner} nuxt dev`),
60
65
  compatibility: "node",
61
66
  // Default entry point for cloudflare-module preset
62
67
  entrypoint: props?.entrypoint ?? "./.output/server/index.mjs",
@@ -1,30 +1,9 @@
1
- import type { GetPlatformProxyOptions } from "wrangler";
2
- import { dedent } from "../../util/dedent.ts";
1
+ import type { PluginConfig } from "@cloudflare/vite-plugin";
2
+ import alchemyVite from "../vite/plugin.ts";
3
3
 
4
- /**
5
- * TanStackStart server functions and middleware run in Node.js intead of Miniflare when using `vite dev`.
6
- *
7
- * This plugin polyfills the cloudflare:workers module & includes `process.env` during the dev server phase.
8
- *
9
- * @see https://developers.cloudflare.com/workers/framework-guides/web-apps/tanstack/#using-cloudflare-bindings
10
- */
11
- export default function alchemy(options: GetPlatformProxyOptions = {}) {
12
- return {
13
- name: "cloudflare-workers-dev-shim",
14
- apply: "serve", // dev‑only
15
- enforce: "pre",
16
- resolveId(id: string) {
17
- if (id === "cloudflare:workers") return id; // tell Vite we handled it
18
- },
19
- load(id: string) {
20
- if (id === "cloudflare:workers") {
21
- return dedent`
22
- import { getCloudflareEnvProxy } from "alchemy/cloudflare";
23
- export const env = await getCloudflareEnvProxy(${JSON.stringify(
24
- options,
25
- )});
26
- `;
27
- }
28
- },
29
- } as const;
4
+ export default function alchemy(options: PluginConfig = {}) {
5
+ return alchemyVite({
6
+ viteEnvironment: { name: "ssr" },
7
+ ...options,
8
+ });
30
9
  }
@@ -15,10 +15,21 @@ export async function TanStackStart<B extends Bindings>(
15
15
  props?: Partial<TanStackStartProps<B>>,
16
16
  ): Promise<TanStackStart<B>> {
17
17
  return await Vite(id, {
18
- ...props,
18
+ entrypoint: "dist/server/index.js",
19
+ assets: "dist/client",
20
+ compatibility: "node",
19
21
  noBundle: true,
20
- entrypoint: props?.entrypoint ?? ".output/server/index.mjs",
21
- compatibilityFlags: ["nodejs_compat", ...(props?.compatibilityFlags ?? [])],
22
- assets: props?.assets ?? ".output/public",
22
+ spa: false,
23
+ ...props,
24
+ wrangler: {
25
+ ...props?.wrangler,
26
+ transform: async (spec) => {
27
+ const transformed = (await props?.wrangler?.transform?.(spec)) ?? spec;
28
+ if (!props?.wrangler?.main) {
29
+ transformed.main = "@tanstack/react-start/server-entry";
30
+ }
31
+ return transformed;
32
+ },
33
+ },
23
34
  });
24
35
  }
@@ -1,7 +1,12 @@
1
1
  import { getPackageManagerRunner } from "../../util/detect-package-manager.ts";
2
2
  import type { Assets } from "../assets.ts";
3
3
  import type { Bindings } from "../bindings.ts";
4
- import { Website, type WebsiteProps } from "../website.ts";
4
+ import {
5
+ spreadBuildProps,
6
+ spreadDevProps,
7
+ Website,
8
+ type WebsiteProps,
9
+ } from "../website.ts";
5
10
  import type { Worker } from "../worker.ts";
6
11
 
7
12
  export interface ViteProps<B extends Bindings> extends WebsiteProps<B> {}
@@ -27,7 +32,7 @@ export async function Vite<B extends Bindings>(
27
32
  props.assets?.directory ??
28
33
  (props.entrypoint || props.script ? "dist/client" : "dist"),
29
34
  },
30
- build: props.build ?? `${runner} vite build`,
31
- dev: props.dev ?? `${runner} vite dev`,
35
+ build: spreadBuildProps(props, `${runner} vite build`),
36
+ dev: spreadDevProps(props, `${runner} vite dev`),
32
37
  });
33
38
  }
@@ -27,7 +27,7 @@ export interface WebsiteProps<B extends Bindings>
27
27
  /**
28
28
  * The command to run to build the site
29
29
  */
30
- command: string;
30
+ command?: string;
31
31
  /**
32
32
  * Additional environment variables to set when running the build command
33
33
  */
@@ -287,7 +287,7 @@ export async function Website<B extends Bindings>(
287
287
 
288
288
  const scope = Scope.current;
289
289
 
290
- if (build && !scope.local) {
290
+ if (build?.command && !scope.local) {
291
291
  await Exec(`${id}-build`, {
292
292
  cwd: path.relative(process.cwd(), paths.cwd),
293
293
  command: build.command,
@@ -334,6 +334,7 @@ export async function Website<B extends Bindings>(
334
334
  // NOTE: we must set this to ensure the user does not accidentally set `NODE_ENV=production`
335
335
  // which breaks `vite dev` (it won't, for example, re-write `process.env.TSS_APP_BASE` in the `.js` client side bundle)
336
336
  NODE_ENV: "development",
337
+ ALCHEMY_ROOT: Scope.current.rootDir,
337
338
  },
338
339
  });
339
340
  }
@@ -353,3 +354,29 @@ export async function Website<B extends Bindings>(
353
354
  dev: url ? { url } : undefined,
354
355
  })) as Website<B>;
355
356
  }
357
+
358
+ export const spreadBuildProps = (
359
+ props: { build?: WebsiteProps<Bindings>["build"] } | undefined,
360
+ defaultCommand: string,
361
+ ): WebsiteProps<Bindings>["build"] => {
362
+ if (typeof props?.build === "object") {
363
+ return {
364
+ ...props.build,
365
+ command: props.build.command ?? defaultCommand,
366
+ };
367
+ }
368
+ return props?.build ?? defaultCommand;
369
+ };
370
+
371
+ export const spreadDevProps = (
372
+ props: { dev?: WebsiteProps<Bindings>["dev"] } | undefined,
373
+ defaultCommand: string,
374
+ ): WebsiteProps<Bindings>["dev"] => {
375
+ if (typeof props?.dev === "object") {
376
+ return {
377
+ ...props.dev,
378
+ command: props.dev.command ?? defaultCommand,
379
+ };
380
+ }
381
+ return props?.dev ?? defaultCommand;
382
+ };
@@ -18,7 +18,7 @@ import type {
18
18
  MultiStepMigration,
19
19
  SingleStepMigration,
20
20
  } from "./worker-migration.ts";
21
- import type { AssetsConfig, WorkerProps } from "./worker.ts";
21
+ import { isWorker, type AssetsConfig, type WorkerProps } from "./worker.ts";
22
22
 
23
23
  /**
24
24
  * Metadata returned by Cloudflare API for a worker script
@@ -175,6 +175,7 @@ export interface WorkerDefaultEnvironment extends WorkerEnvironment {
175
175
  script: WorkerScriptInfo;
176
176
  }
177
177
 
178
+ //? (jacob): why is this not using BaseWorkerProps?
178
179
  export interface WorkerMetadata {
179
180
  compatibility_date: string;
180
181
  compatibility_flags?: string[];
@@ -202,6 +203,7 @@ export interface WorkerMetadata {
202
203
  limits?: {
203
204
  cpu_ms?: number;
204
205
  };
206
+ tail_consumers?: Array<Worker | { service: string }>;
205
207
  }
206
208
 
207
209
  export async function prepareWorkerMetadata(
@@ -316,6 +318,9 @@ export async function prepareWorkerMetadata(
316
318
  const meta: WorkerMetadata = {
317
319
  compatibility_date: props.compatibilityDate,
318
320
  compatibility_flags: props.compatibilityFlags,
321
+ tail_consumers: props.tailConsumers?.map((consumer) =>
322
+ isWorker(consumer) ? { service: consumer.name } : consumer,
323
+ ),
319
324
  bindings: [],
320
325
  observability: {
321
326
  enabled: props.observability?.enabled !== false,
@@ -375,6 +375,11 @@ export interface BaseWorkerProps<
375
375
  */
376
376
  cpu_ms?: number;
377
377
  };
378
+
379
+ /**
380
+ * Tail consumers that will receive execution logs from this worker
381
+ */
382
+ tailConsumers?: Array<Worker | { service: string }>;
378
383
  }
379
384
 
380
385
  export interface InlineWorkerProps<
@@ -1018,6 +1023,7 @@ const _Worker = Resource(
1018
1023
  props.crons?.map((cron) => ({ cron })) ?? [],
1019
1024
  );
1020
1025
  }
1026
+
1021
1027
  await Promise.all(
1022
1028
  options.workflows.map((workflow) =>
1023
1029
  upsertWorkflow(api, {
@@ -1063,6 +1069,7 @@ const _Worker = Resource(
1063
1069
  url: subdomain?.url,
1064
1070
  assets: props.assets,
1065
1071
  crons: props.crons,
1072
+ tailConsumers: props.tailConsumers,
1066
1073
  routes,
1067
1074
  domains,
1068
1075
  namespace: props.namespace,
@@ -11,7 +11,7 @@ import type { DurableObjectNamespace } from "./durable-object-namespace.ts";
11
11
  import type { EventSource } from "./event-source.ts";
12
12
  import { isQueueEventSource } from "./event-source.ts";
13
13
  import { isQueue } from "./queue.ts";
14
- import type { Worker, WorkerProps } from "./worker.ts";
14
+ import { isWorker, type Worker, type WorkerProps } from "./worker.ts";
15
15
 
16
16
  type WranglerJsonRateLimit = Omit<WorkerBindingRateLimit, "type"> & {
17
17
  type: "rate_limit";
@@ -182,6 +182,15 @@ export async function WranglerJson(
182
182
  spec.vars = { ...worker.env };
183
183
  }
184
184
 
185
+ if (worker.tailConsumers && worker.tailConsumers.length > 0) {
186
+ spec.tail_consumers = worker.tailConsumers.map((consumer) => {
187
+ if (isWorker(consumer)) {
188
+ return { service: consumer.name };
189
+ }
190
+ return { service: consumer.service };
191
+ });
192
+ }
193
+
185
194
  if (worker.crons && worker.crons.length > 0) {
186
195
  spec.triggers = { crons: worker.crons };
187
196
  }
@@ -350,14 +359,7 @@ export interface WranglerJsonSpec {
350
359
  */
351
360
  queues?: {
352
361
  producers?: { queue: string; binding: string }[];
353
- consumers?: {
354
- queue: string;
355
- max_batch_size?: number;
356
- max_concurrency?: number;
357
- max_retries?: number;
358
- max_wait_time_ms?: number;
359
- retry_delay?: number;
360
- }[];
362
+ consumers?: QueueConsumerWranglerJson[];
361
363
  };
362
364
 
363
365
  /**
@@ -492,6 +494,11 @@ export interface WranglerJsonSpec {
492
494
  experimental_remote?: boolean;
493
495
  }[];
494
496
 
497
+ /**
498
+ * Tail consumers that will receive execution logs from this worker
499
+ */
500
+ tail_consumers?: Array<{ service: string }>;
501
+
495
502
  /**
496
503
  * Unsafe bindings section for experimental features
497
504
  */
@@ -500,6 +507,33 @@ export interface WranglerJsonSpec {
500
507
  };
501
508
  }
502
509
 
510
+ /**
511
+ * Queue consumer bindings for wrangler.json
512
+ */
513
+ interface QueueConsumerWranglerJson {
514
+ queue: string;
515
+ /**
516
+ * The maximum number of messages allowed in each batch.
517
+ */
518
+ max_batch_size?: number;
519
+ /**
520
+ * The maximum number of concurrent consumers allowed to run at once. Leaving this unset will mean that the number of invocations will scale to the currently supported maximum.
521
+ */
522
+ max_concurrency?: number;
523
+ /**
524
+ * The maximum number of retries for a message, if it fails or `retryAll()` is invoked.
525
+ */
526
+ max_retries?: number;
527
+ /**
528
+ * The maximum number of seconds to wait for messages to fill a batch before the batch is sent to the consumer Worker.
529
+ */
530
+ max_batch_timeout?: number;
531
+ /**
532
+ * The number of seconds to delay retried messages for by default, before they are re-delivered to the consumer. This can be overridden on a per-message or per-batch basis when retrying messages.
533
+ */
534
+ retry_delay?: number;
535
+ }
536
+
503
537
  /**
504
538
  * Process worker bindings into wrangler.json format
505
539
  */
@@ -554,14 +588,7 @@ function processBindings(
554
588
  }[] = [];
555
589
  const queues: {
556
590
  producers: { queue: string; binding: string }[];
557
- consumers: {
558
- queue: string;
559
- max_batch_size?: number;
560
- max_concurrency?: number;
561
- max_retries?: number;
562
- max_wait_time_ms?: number;
563
- retry_delay?: number;
564
- }[];
591
+ consumers: QueueConsumerWranglerJson[];
565
592
  } = {
566
593
  producers: [],
567
594
  consumers: [],
@@ -604,7 +631,9 @@ function processBindings(
604
631
  max_batch_size: eventSource.settings?.batchSize,
605
632
  max_concurrency: eventSource.settings?.maxConcurrency,
606
633
  max_retries: eventSource.settings?.maxRetries,
607
- max_wait_time_ms: eventSource.settings?.maxWaitTimeMs,
634
+ max_batch_timeout: eventSource.settings?.maxWaitTimeMs
635
+ ? eventSource.settings?.maxWaitTimeMs / 1000
636
+ : undefined,
608
637
  retry_delay: eventSource.settings?.retryDelay,
609
638
  });
610
639
  } else if (isQueue(eventSource)) {
package/src/docker/api.ts CHANGED
@@ -3,6 +3,41 @@ import { Buffer } from "node:buffer";
3
3
  import { spawn } from "node:child_process";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import type { Duration, HealthcheckConfig } from "./container.ts";
7
+
8
+ /**
9
+ * Normalize a duration value to Docker CLI format
10
+ * Accepts either a number (seconds) or a string with unit suffix (ms|s|m|h)
11
+ * Returns a string in Docker CLI format
12
+ *
13
+ * The Duration type provides compile-time type safety, ensuring only valid
14
+ * formats are accepted: number or string matching the pattern `${number}${unit}`
15
+ *
16
+ * @param duration Duration value (number in seconds or string with unit)
17
+ * @returns Normalized duration string for Docker CLI
18
+ *
19
+ * @example
20
+ * normalizeDuration(30) // "30s"
21
+ * normalizeDuration("30s") // "30s"
22
+ * normalizeDuration("1m") // "1m"
23
+ * normalizeDuration("500ms") // "500ms"
24
+ * normalizeDuration("1.5s") // "1.5s"
25
+ */
26
+ export function normalizeDuration(duration: Duration): string {
27
+ if (typeof duration === "number") {
28
+ return `${duration}s`;
29
+ }
30
+
31
+ // Validate string format: must be a number followed by unit (ms|s|m|h)
32
+ const match = duration.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/);
33
+ if (!match) {
34
+ throw new Error(
35
+ `Invalid duration format: "${duration}". Expected format: number followed by unit (ms|s|m|h), e.g., "30s", "1m", "500ms"`,
36
+ );
37
+ }
38
+
39
+ return duration;
40
+ }
6
41
 
7
42
  /**
8
43
  * Options for Docker API requests
@@ -59,7 +94,10 @@ export class DockerApi {
59
94
  * @param args Command arguments to pass to Docker CLI
60
95
  * @returns Result of the command
61
96
  */
62
- async exec(args: string[]): Promise<{ stdout: string; stderr: string }> {
97
+ async exec(
98
+ args: string[],
99
+ remainingAttempts = 3,
100
+ ): Promise<{ stdout: string; stderr: string }> {
63
101
  // If a custom config directory is provided, ensure all commands use it by
64
102
  // setting the DOCKER_CONFIG env variable for the spawned process.
65
103
  const env = this.configDir
@@ -94,8 +132,16 @@ export class DockerApi {
94
132
  try {
95
133
  await subprocess;
96
134
  } catch (error: any) {
135
+ const message = stderr || error.message || "Command failed";
136
+ if (
137
+ message.includes("unexpected status from HEAD request") &&
138
+ remainingAttempts > 0
139
+ ) {
140
+ await new Promise((resolve) => setTimeout(resolve, 1000));
141
+ return this.exec(args, remainingAttempts - 1);
142
+ }
97
143
  // Process failed, but we still have the output
98
- throw new Error(stderr || error.message || "Command failed");
144
+ throw new Error(message);
99
145
  }
100
146
 
101
147
  return { stdout, stderr };
@@ -177,6 +223,7 @@ export class DockerApi {
177
223
  env?: Record<string, string>;
178
224
  volumes?: Record<string, string>;
179
225
  cmd?: string[];
226
+ healthcheck?: HealthcheckConfig;
180
227
  } = {},
181
228
  ): Promise<string> {
182
229
  const args = ["create", "--name", name];
@@ -202,6 +249,43 @@ export class DockerApi {
202
249
  }
203
250
  }
204
251
 
252
+ // Add healthcheck configuration
253
+ if (options.healthcheck) {
254
+ const hc = options.healthcheck;
255
+
256
+ // Format the cmd
257
+ const cmdStr = Array.isArray(hc.cmd) ? hc.cmd.join(" ") : hc.cmd;
258
+ args.push("--health-cmd", cmdStr);
259
+
260
+ // Add interval
261
+ if (hc.interval !== undefined) {
262
+ args.push("--health-interval", normalizeDuration(hc.interval));
263
+ }
264
+
265
+ // Add timeout
266
+ if (hc.timeout !== undefined) {
267
+ args.push("--health-timeout", normalizeDuration(hc.timeout));
268
+ }
269
+
270
+ // Add retries
271
+ if (hc.retries !== undefined) {
272
+ args.push("--health-retries", String(hc.retries));
273
+ }
274
+
275
+ // Add start period
276
+ if (hc.startPeriod !== undefined) {
277
+ args.push("--health-start-period", normalizeDuration(hc.startPeriod));
278
+ }
279
+
280
+ // Add start interval (API 1.44+)
281
+ if (hc.startInterval !== undefined) {
282
+ args.push(
283
+ "--health-start-interval",
284
+ normalizeDuration(hc.startInterval),
285
+ );
286
+ }
287
+ }
288
+
205
289
  args.push(image);
206
290
 
207
291
  // Add command if specified