alchemy 0.69.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.
- package/bin/alchemy.js +14 -33
- package/bin/commands/init.ts +41 -55
- package/lib/cloudflare/bucket-object.d.ts +5 -0
- package/lib/cloudflare/bucket-object.d.ts.map +1 -1
- package/lib/cloudflare/bucket-object.js +1 -1
- package/lib/cloudflare/bucket-object.js.map +1 -1
- package/lib/cloudflare/bucket.d.ts +6 -3
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +60 -8
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/container.js +1 -1
- package/lib/cloudflare/container.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +44 -12
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
- package/lib/cloudflare/tanstack-start/plugin.d.ts +2 -15
- package/lib/cloudflare/tanstack-start/plugin.d.ts.map +1 -1
- package/lib/cloudflare/tanstack-start/plugin.js +5 -25
- package/lib/cloudflare/tanstack-start/plugin.js.map +1 -1
- package/lib/cloudflare/tanstack-start/tanstack-start.d.ts.map +1 -1
- package/lib/cloudflare/tanstack-start/tanstack-start.js +15 -4
- package/lib/cloudflare/tanstack-start/tanstack-start.js.map +1 -1
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +1 -0
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts +4 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +2 -0
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +6 -0
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +1 -0
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +7 -1
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +9 -0
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/docker/api.d.ts +22 -1
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +64 -2
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/container.d.ts +88 -0
- package/lib/docker/container.d.ts.map +1 -1
- package/lib/docker/container.js +36 -0
- package/lib/docker/container.js.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/bucket-object.ts +11 -1
- package/src/cloudflare/bucket.ts +72 -6
- package/src/cloudflare/container.ts +2 -2
- package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +57 -13
- package/src/cloudflare/tanstack-start/plugin.ts +7 -28
- package/src/cloudflare/tanstack-start/tanstack-start.ts +15 -4
- package/src/cloudflare/website.ts +1 -0
- package/src/cloudflare/worker-metadata.ts +6 -1
- package/src/cloudflare/worker.ts +7 -0
- package/src/cloudflare/wrangler.json.ts +15 -1
- package/src/docker/api.ts +86 -2
- package/src/docker/container.ts +97 -0
- package/templates/tanstack-start/.cta.json +13 -0
- package/templates/tanstack-start/README.md +267 -49
- package/templates/tanstack-start/_gitignore +9 -18
- package/templates/tanstack-start/alchemy.run.ts +2 -2
- package/templates/tanstack-start/package.json +24 -19
- package/templates/tanstack-start/public/favicon.ico +0 -0
- package/templates/tanstack-start/public/logo192.png +0 -0
- package/templates/tanstack-start/public/logo512.png +0 -0
- package/templates/tanstack-start/public/manifest.json +25 -0
- package/templates/tanstack-start/public/robots.txt +3 -0
- package/templates/tanstack-start/src/components/Header.tsx +21 -0
- package/templates/tanstack-start/src/logo.svg +12 -0
- package/templates/tanstack-start/src/router.tsx +7 -16
- package/templates/tanstack-start/src/routes/__root.tsx +13 -114
- package/templates/tanstack-start/src/routes/api.demo-names.ts +15 -0
- package/templates/tanstack-start/src/routes/demo.start.api-request.tsx +44 -0
- package/templates/tanstack-start/src/routes/demo.start.server-funcs.tsx +97 -0
- package/templates/tanstack-start/src/routes/index.tsx +31 -4
- package/templates/tanstack-start/src/styles.css +15 -0
- package/templates/tanstack-start/tsconfig.json +19 -22
- package/templates/tanstack-start/types/env.d.ts +2 -2
- package/templates/tanstack-start/vite.config.ts +16 -25
- package/workers/tunnel-proxy.js +1 -1
- package/templates/tanstack-start/_prettierignore +0 -4
- package/templates/tanstack-start/public/android-chrome-192x192.png +0 -0
- package/templates/tanstack-start/public/android-chrome-512x512.png +0 -0
- package/templates/tanstack-start/public/apple-touch-icon.png +0 -0
- package/templates/tanstack-start/public/favicon-16x16.png +0 -0
- package/templates/tanstack-start/public/favicon-32x32.png +0 -0
- package/templates/tanstack-start/public/favicon.png +0 -0
- package/templates/tanstack-start/public/site.webmanifest +0 -19
- package/templates/tanstack-start/src/components/DefaultCatchBoundary.tsx +0 -53
- package/templates/tanstack-start/src/components/NotFound.tsx +0 -25
- package/templates/tanstack-start/src/components/PostError.tsx +0 -5
- package/templates/tanstack-start/src/components/UserError.tsx +0 -5
- package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-a.tsx +0 -10
- package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-b.tsx +0 -10
- package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout.tsx +0 -34
- package/templates/tanstack-start/src/routes/_pathlessLayout.tsx +0 -16
- package/templates/tanstack-start/src/routes/api/users.$userId.ts +0 -28
- package/templates/tanstack-start/src/routes/api/users.ts +0 -59
- package/templates/tanstack-start/src/routes/customScript[.]js.ts +0 -10
- package/templates/tanstack-start/src/routes/deferred.tsx +0 -62
- package/templates/tanstack-start/src/routes/posts.$postId.tsx +0 -34
- package/templates/tanstack-start/src/routes/posts.index.tsx +0 -8
- package/templates/tanstack-start/src/routes/posts.tsx +0 -38
- package/templates/tanstack-start/src/routes/posts_.$postId.deep.tsx +0 -29
- package/templates/tanstack-start/src/routes/redirect.tsx +0 -9
- package/templates/tanstack-start/src/routes/users.$userId.tsx +0 -44
- package/templates/tanstack-start/src/routes/users.index.tsx +0 -18
- package/templates/tanstack-start/src/routes/users.tsx +0 -49
- package/templates/tanstack-start/src/styles/app.css +0 -12
- package/templates/tanstack-start/src/utils/loggingMiddleware.tsx +0 -41
- package/templates/tanstack-start/src/utils/posts.tsx +0 -40
- package/templates/tanstack-start/src/utils/seo.ts +0 -33
- package/templates/tanstack-start/src/utils/users.tsx +0 -5
package/src/cloudflare/bucket.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { R2PutOptions } from "@cloudflare/workers-types/experimental/index.ts";
|
|
1
2
|
import * as mf from "miniflare";
|
|
2
3
|
import { isDeepStrictEqual } from "node:util";
|
|
3
4
|
import type { Context } from "../context.ts";
|
|
@@ -253,6 +254,7 @@ export type R2ObjectMetadata = {
|
|
|
253
254
|
etag: string;
|
|
254
255
|
uploaded: Date;
|
|
255
256
|
size: number;
|
|
257
|
+
httpMetadata?: R2HTTPMetadata;
|
|
256
258
|
};
|
|
257
259
|
|
|
258
260
|
export type R2ObjectContent = R2ObjectMetadata & {
|
|
@@ -272,7 +274,7 @@ export type PutR2ObjectResponse = {
|
|
|
272
274
|
};
|
|
273
275
|
|
|
274
276
|
export type R2Objects = {
|
|
275
|
-
objects: R2ObjectMetadata[];
|
|
277
|
+
objects: Omit<R2ObjectMetadata, "httpMetadata">[];
|
|
276
278
|
} & (
|
|
277
279
|
| {
|
|
278
280
|
truncated: true;
|
|
@@ -296,6 +298,7 @@ export type R2Bucket = _R2Bucket & {
|
|
|
296
298
|
| string
|
|
297
299
|
| null
|
|
298
300
|
| Blob,
|
|
301
|
+
options?: Pick<R2PutOptions, "httpMetadata">,
|
|
299
302
|
): Promise<PutR2ObjectResponse>;
|
|
300
303
|
delete(key: string): Promise<Response>;
|
|
301
304
|
list(options?: R2ListOptions): Promise<R2Objects>;
|
|
@@ -449,7 +452,17 @@ export async function R2Bucket(
|
|
|
449
452
|
...bucket,
|
|
450
453
|
head: async (key: string) => {
|
|
451
454
|
if (isLocal) {
|
|
452
|
-
|
|
455
|
+
const result = await (await localBucket()).head(key);
|
|
456
|
+
if (result) {
|
|
457
|
+
return {
|
|
458
|
+
key: result.key,
|
|
459
|
+
etag: result.etag,
|
|
460
|
+
uploaded: result.uploaded,
|
|
461
|
+
size: result.size,
|
|
462
|
+
httpMetadata: result.httpMetadata,
|
|
463
|
+
} as R2ObjectMetadata;
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
453
466
|
}
|
|
454
467
|
return headObject(api, {
|
|
455
468
|
bucketName: bucket.name,
|
|
@@ -489,9 +502,9 @@ export async function R2Bucket(
|
|
|
489
502
|
put: async (
|
|
490
503
|
key: string,
|
|
491
504
|
value: PutObjectObject,
|
|
505
|
+
options?: Pick<R2PutOptions, "httpMetadata">,
|
|
492
506
|
): Promise<PutR2ObjectResponse> => {
|
|
493
507
|
if (isLocal) {
|
|
494
|
-
// @ts-expect-error - node built-ins vs cloudflare built-ins
|
|
495
508
|
return await (await localBucket()).put(
|
|
496
509
|
key,
|
|
497
510
|
typeof value === "string"
|
|
@@ -505,12 +518,14 @@ export async function R2Bucket(
|
|
|
505
518
|
: value instanceof ReadableStream
|
|
506
519
|
? new Uint8Array(await streamToBuffer(value))
|
|
507
520
|
: value,
|
|
521
|
+
options,
|
|
508
522
|
);
|
|
509
523
|
}
|
|
510
524
|
const response = await putObject(api, {
|
|
511
525
|
bucketName: bucket.name,
|
|
512
526
|
key: key,
|
|
513
527
|
object: value,
|
|
528
|
+
options: options,
|
|
514
529
|
});
|
|
515
530
|
const body = (await response.json()) as {
|
|
516
531
|
result: {
|
|
@@ -546,6 +561,7 @@ const parseR2Object = (key: string, response: Response): R2ObjectContent => ({
|
|
|
546
561
|
uploaded: parseDate(response.headers),
|
|
547
562
|
key,
|
|
548
563
|
size: Number(response.headers.get("Content-Length")),
|
|
564
|
+
httpMetadata: mapHeadersToHttpMetadata(response.headers),
|
|
549
565
|
arrayBuffer: () => response.arrayBuffer(),
|
|
550
566
|
bytes: () => response.bytes(),
|
|
551
567
|
text: () => response.text(),
|
|
@@ -1155,6 +1171,7 @@ export async function headObject(
|
|
|
1155
1171
|
etag: response.headers.get("ETag")?.replace(/"/g, "")!,
|
|
1156
1172
|
uploaded: parseDate(response.headers),
|
|
1157
1173
|
size: Number(response.headers.get("Content-Length")),
|
|
1174
|
+
httpMetadata: mapHeadersToHttpMetadata(response.headers),
|
|
1158
1175
|
};
|
|
1159
1176
|
}
|
|
1160
1177
|
|
|
@@ -1192,27 +1209,76 @@ export type PutObjectObject =
|
|
|
1192
1209
|
| Buffer
|
|
1193
1210
|
| Blob;
|
|
1194
1211
|
|
|
1212
|
+
function mapHttpMetadataToHeaders(
|
|
1213
|
+
httpMetadata: R2PutOptions["httpMetadata"],
|
|
1214
|
+
): Record<string, string> {
|
|
1215
|
+
const headers: Record<string, string> = {};
|
|
1216
|
+
|
|
1217
|
+
if (httpMetadata instanceof Headers) {
|
|
1218
|
+
httpMetadata.forEach((value, key) => {
|
|
1219
|
+
headers[key] = value;
|
|
1220
|
+
});
|
|
1221
|
+
} else {
|
|
1222
|
+
const {
|
|
1223
|
+
contentType,
|
|
1224
|
+
contentLanguage,
|
|
1225
|
+
contentDisposition,
|
|
1226
|
+
contentEncoding,
|
|
1227
|
+
cacheControl,
|
|
1228
|
+
cacheExpiry,
|
|
1229
|
+
} = httpMetadata as R2HTTPMetadata;
|
|
1230
|
+
if (contentType) headers["Content-Type"] = contentType;
|
|
1231
|
+
if (contentLanguage) headers["Content-Language"] = contentLanguage;
|
|
1232
|
+
if (contentDisposition) headers["Content-Disposition"] = contentDisposition;
|
|
1233
|
+
if (contentEncoding) headers["Content-Encoding"] = contentEncoding;
|
|
1234
|
+
if (cacheControl) headers["Cache-Control"] = cacheControl;
|
|
1235
|
+
if (cacheExpiry) headers.Expires = cacheExpiry.toUTCString();
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return headers;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function mapHeadersToHttpMetadata(headers: Headers): R2HTTPMetadata {
|
|
1242
|
+
return {
|
|
1243
|
+
contentType: headers.get("Content-Type") ?? undefined,
|
|
1244
|
+
contentLanguage: headers.get("Content-Language") ?? undefined,
|
|
1245
|
+
contentDisposition: headers.get("Content-Disposition") ?? undefined,
|
|
1246
|
+
contentEncoding: headers.get("Content-Encoding") ?? undefined,
|
|
1247
|
+
cacheControl: headers.get("Cache-Control") ?? undefined,
|
|
1248
|
+
cacheExpiry: headers.get("Expires")
|
|
1249
|
+
? new Date(headers.get("Expires")!)
|
|
1250
|
+
: undefined,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1195
1254
|
export async function putObject(
|
|
1196
1255
|
api: CloudflareApi,
|
|
1197
1256
|
{
|
|
1198
1257
|
bucketName,
|
|
1199
1258
|
key,
|
|
1200
1259
|
object,
|
|
1260
|
+
options,
|
|
1201
1261
|
}: {
|
|
1202
1262
|
bucketName: string;
|
|
1203
1263
|
key: string;
|
|
1204
1264
|
object: PutObjectObject;
|
|
1265
|
+
options?: Pick<R2PutOptions, "httpMetadata">;
|
|
1205
1266
|
},
|
|
1206
1267
|
): Promise<Response> {
|
|
1207
1268
|
// Using withExponentialBackoff for reliability
|
|
1208
1269
|
return await withRetries(async () => {
|
|
1270
|
+
const headers: Record<string, string> = {
|
|
1271
|
+
"Content-Type": "application/octet-stream",
|
|
1272
|
+
...(options?.httpMetadata
|
|
1273
|
+
? mapHttpMetadataToHeaders(options?.httpMetadata)
|
|
1274
|
+
: {}),
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1209
1277
|
const response = await api.put(
|
|
1210
1278
|
`/accounts/${api.accountId}/r2/buckets/${bucketName}/objects/${key}`,
|
|
1211
1279
|
object,
|
|
1212
1280
|
{
|
|
1213
|
-
headers
|
|
1214
|
-
"Content-Type": "application/octet-stream",
|
|
1215
|
-
},
|
|
1281
|
+
headers,
|
|
1216
1282
|
},
|
|
1217
1283
|
);
|
|
1218
1284
|
if (!response.ok) {
|
|
@@ -1044,13 +1044,13 @@ export async function createContainerApplicationRollout(
|
|
|
1044
1044
|
);
|
|
1045
1045
|
const result = (await response.json()) as {
|
|
1046
1046
|
result: CreateRolloutApplicationResponse;
|
|
1047
|
-
errors: { message: string }[];
|
|
1047
|
+
errors: { message: string; code: number }[];
|
|
1048
1048
|
};
|
|
1049
1049
|
if (response.ok) {
|
|
1050
1050
|
return result.result;
|
|
1051
1051
|
}
|
|
1052
1052
|
throw Error(
|
|
1053
|
-
`Failed to create container application rollout: ${result.errors.map((e
|
|
1053
|
+
`Failed to create container application rollout: ${result.errors.map((e) => `[${e.code}] ${e.message}`).join(", ")}`,
|
|
1054
1054
|
);
|
|
1055
1055
|
}
|
|
1056
1056
|
|
|
@@ -31,10 +31,7 @@ export async function createMiniflareWorkerProxy(options: {
|
|
|
31
31
|
});
|
|
32
32
|
server.on("request", async (req, res) => {
|
|
33
33
|
try {
|
|
34
|
-
const
|
|
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
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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");
|
|
@@ -1,30 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
1
|
+
import type { PluginConfig } from "@cloudflare/vite-plugin";
|
|
2
|
+
import alchemyVite from "../vite/plugin.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
18
|
+
entrypoint: "dist/server/index.js",
|
|
19
|
+
assets: "dist/client",
|
|
20
|
+
compatibility: "node",
|
|
19
21
|
noBundle: true,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
MultiStepMigration,
|
|
19
19
|
SingleStepMigration,
|
|
20
20
|
} from "./worker-migration.ts";
|
|
21
|
-
import type
|
|
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,
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
|
@@ -485,6 +494,11 @@ export interface WranglerJsonSpec {
|
|
|
485
494
|
experimental_remote?: boolean;
|
|
486
495
|
}[];
|
|
487
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Tail consumers that will receive execution logs from this worker
|
|
499
|
+
*/
|
|
500
|
+
tail_consumers?: Array<{ service: string }>;
|
|
501
|
+
|
|
488
502
|
/**
|
|
489
503
|
* Unsafe bindings section for experimental features
|
|
490
504
|
*/
|
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(
|
|
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(
|
|
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
|
package/src/docker/container.ts
CHANGED
|
@@ -59,6 +59,62 @@ export interface NetworkMapping {
|
|
|
59
59
|
aliases?: string[];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Duration value supporting both number (seconds) and string format (value + unit)
|
|
64
|
+
* Units: ms (milliseconds), s (seconds), m (minutes), h (hours)
|
|
65
|
+
* Examples: 30, "30s", "1m", "500ms", "2h"
|
|
66
|
+
*/
|
|
67
|
+
export type Duration = number | `${number}${"ms" | "s" | "m" | "h"}`;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Healthcheck configuration
|
|
71
|
+
*/
|
|
72
|
+
export interface HealthcheckConfig {
|
|
73
|
+
/**
|
|
74
|
+
* Command to run to check health.
|
|
75
|
+
* Can be an array of command arguments or a shell command string.
|
|
76
|
+
* Examples:
|
|
77
|
+
* - ["curl", "-f", "http://localhost/"]
|
|
78
|
+
* - "curl -f http://localhost/ || exit 1"
|
|
79
|
+
*/
|
|
80
|
+
cmd: string[] | string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Time between running the check
|
|
84
|
+
* Can be a number (in seconds) or string with unit (e.g., "30s", "1m")
|
|
85
|
+
* @default 0
|
|
86
|
+
*/
|
|
87
|
+
interval?: Duration;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Maximum time to allow one check to run
|
|
91
|
+
* Can be a number (in seconds) or string with unit (e.g., "10s", "500ms")
|
|
92
|
+
* @default 0
|
|
93
|
+
*/
|
|
94
|
+
timeout?: Duration;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Consecutive failures needed to report unhealthy
|
|
98
|
+
*/
|
|
99
|
+
retries?: number;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Start period for the container to initialize before starting
|
|
103
|
+
* health-retries countdown
|
|
104
|
+
* Can be a number (in seconds) or string with unit (e.g., "40s", "1m")
|
|
105
|
+
* @default 0
|
|
106
|
+
*/
|
|
107
|
+
startPeriod?: Duration;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Time between running the check during the start period
|
|
111
|
+
* Can be a number (in seconds) or string with unit (e.g., "5s", "500ms")
|
|
112
|
+
* Requires Docker API 1.44+
|
|
113
|
+
* @default 0
|
|
114
|
+
*/
|
|
115
|
+
startInterval?: Duration;
|
|
116
|
+
}
|
|
117
|
+
|
|
62
118
|
/**
|
|
63
119
|
* Properties for creating a Docker container
|
|
64
120
|
*/
|
|
@@ -115,6 +171,11 @@ export interface ContainerProps {
|
|
|
115
171
|
* Start the container after creation
|
|
116
172
|
*/
|
|
117
173
|
start?: boolean;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Healthcheck configuration
|
|
177
|
+
*/
|
|
178
|
+
healthcheck?: HealthcheckConfig;
|
|
118
179
|
}
|
|
119
180
|
|
|
120
181
|
/**
|
|
@@ -172,6 +233,41 @@ export interface Container extends ContainerProps {
|
|
|
172
233
|
* restart: "always",
|
|
173
234
|
* start: true
|
|
174
235
|
* });
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* // Create a container with healthcheck using numeric values (seconds)
|
|
239
|
+
* const healthyContainer = await Container("api", {
|
|
240
|
+
* image: "my-api:latest",
|
|
241
|
+
* ports: [
|
|
242
|
+
* { external: 3000, internal: 3000 }
|
|
243
|
+
* ],
|
|
244
|
+
* healthcheck: {
|
|
245
|
+
* cmd: ["curl", "-f", "http://localhost:3000/health"],
|
|
246
|
+
* interval: 30,
|
|
247
|
+
* timeout: 10,
|
|
248
|
+
* retries: 3,
|
|
249
|
+
* startPeriod: 40
|
|
250
|
+
* },
|
|
251
|
+
* start: true
|
|
252
|
+
* });
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* // Create a container with healthcheck using string duration format
|
|
256
|
+
* const healthyContainer2 = await Container("api2", {
|
|
257
|
+
* image: "my-api:latest",
|
|
258
|
+
* ports: [
|
|
259
|
+
* { external: 3001, internal: 3000 }
|
|
260
|
+
* ],
|
|
261
|
+
* healthcheck: {
|
|
262
|
+
* cmd: ["curl", "-f", "http://localhost:3000/health"],
|
|
263
|
+
* interval: "30s",
|
|
264
|
+
* timeout: "10s",
|
|
265
|
+
* retries: 3,
|
|
266
|
+
* startPeriod: "1m",
|
|
267
|
+
* startInterval: "500ms"
|
|
268
|
+
* },
|
|
269
|
+
* start: true
|
|
270
|
+
* });
|
|
175
271
|
*/
|
|
176
272
|
export const Container = Resource(
|
|
177
273
|
"docker::Container",
|
|
@@ -245,6 +341,7 @@ export const Container = Resource(
|
|
|
245
341
|
env: props.environment,
|
|
246
342
|
volumes: volumeMappings,
|
|
247
343
|
cmd: props.command,
|
|
344
|
+
healthcheck: props.healthcheck,
|
|
248
345
|
});
|
|
249
346
|
|
|
250
347
|
// Connect to networks if specified
|