alchemy 2.0.0-beta.64 → 2.0.0-beta.65

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 (165) hide show
  1. package/lib/AWS/EC2/InternetGateway.d.ts.map +1 -1
  2. package/lib/AWS/EC2/InternetGateway.js +8 -3
  3. package/lib/AWS/EC2/InternetGateway.js.map +1 -1
  4. package/lib/AWS/EC2/LingeringEnis.d.ts +26 -0
  5. package/lib/AWS/EC2/LingeringEnis.d.ts.map +1 -0
  6. package/lib/AWS/EC2/LingeringEnis.js +128 -0
  7. package/lib/AWS/EC2/LingeringEnis.js.map +1 -0
  8. package/lib/AWS/EC2/SecurityGroup.d.ts.map +1 -1
  9. package/lib/AWS/EC2/SecurityGroup.js +14 -14
  10. package/lib/AWS/EC2/SecurityGroup.js.map +1 -1
  11. package/lib/AWS/EC2/Subnet.d.ts.map +1 -1
  12. package/lib/AWS/EC2/Subnet.js +13 -21
  13. package/lib/AWS/EC2/Subnet.js.map +1 -1
  14. package/lib/AWS/ECR/Image.d.ts +1 -0
  15. package/lib/AWS/ECR/Image.d.ts.map +1 -1
  16. package/lib/AWS/EKS/Addon.d.ts.map +1 -1
  17. package/lib/AWS/EKS/Addon.js +15 -3
  18. package/lib/AWS/EKS/Addon.js.map +1 -1
  19. package/lib/AWS/EKS/Deployment.d.ts +10 -1
  20. package/lib/AWS/EKS/Deployment.d.ts.map +1 -1
  21. package/lib/AWS/EKS/Deployment.js +10 -4
  22. package/lib/AWS/EKS/Deployment.js.map +1 -1
  23. package/lib/AWS/EKS/HyperPod.d.ts +57 -0
  24. package/lib/AWS/EKS/HyperPod.d.ts.map +1 -0
  25. package/lib/AWS/EKS/HyperPod.js +46 -0
  26. package/lib/AWS/EKS/HyperPod.js.map +1 -0
  27. package/lib/AWS/EKS/Job.d.ts +10 -1
  28. package/lib/AWS/EKS/Job.d.ts.map +1 -1
  29. package/lib/AWS/EKS/Job.js +16 -6
  30. package/lib/AWS/EKS/Job.js.map +1 -1
  31. package/lib/AWS/EKS/internal/client.d.ts +4 -3
  32. package/lib/AWS/EKS/internal/client.d.ts.map +1 -1
  33. package/lib/AWS/EKS/internal/client.js +33 -2
  34. package/lib/AWS/EKS/internal/client.js.map +1 -1
  35. package/lib/AWS/Lambda/Function.d.ts.map +1 -1
  36. package/lib/AWS/Lambda/Function.js +1 -1
  37. package/lib/AWS/Lambda/Function.js.map +1 -1
  38. package/lib/AWS/Providers.d.ts.map +1 -1
  39. package/lib/AWS/Providers.js +4 -1
  40. package/lib/AWS/Providers.js.map +1 -1
  41. package/lib/AWS/SageMaker/Cluster.d.ts +327 -0
  42. package/lib/AWS/SageMaker/Cluster.d.ts.map +1 -0
  43. package/lib/AWS/SageMaker/Cluster.js +469 -0
  44. package/lib/AWS/SageMaker/Cluster.js.map +1 -0
  45. package/lib/AWS/SageMaker/ClusterSchedulerConfig.d.ts +96 -0
  46. package/lib/AWS/SageMaker/ClusterSchedulerConfig.d.ts.map +1 -0
  47. package/lib/AWS/SageMaker/ClusterSchedulerConfig.js +270 -0
  48. package/lib/AWS/SageMaker/ClusterSchedulerConfig.js.map +1 -0
  49. package/lib/AWS/SageMaker/ComputeQuota.d.ts +106 -0
  50. package/lib/AWS/SageMaker/ComputeQuota.d.ts.map +1 -0
  51. package/lib/AWS/SageMaker/ComputeQuota.js +268 -0
  52. package/lib/AWS/SageMaker/ComputeQuota.js.map +1 -0
  53. package/lib/AWS/SageMaker/index.d.ts +3 -0
  54. package/lib/AWS/SageMaker/index.d.ts.map +1 -1
  55. package/lib/AWS/SageMaker/index.js +3 -0
  56. package/lib/AWS/SageMaker/index.js.map +1 -1
  57. package/lib/Bundle/PurePlugin.d.ts +29 -7
  58. package/lib/Bundle/PurePlugin.d.ts.map +1 -1
  59. package/lib/Bundle/PurePlugin.js +56 -33
  60. package/lib/Bundle/PurePlugin.js.map +1 -1
  61. package/lib/Cli/commands/nuke.d.ts.map +1 -1
  62. package/lib/Cli/commands/nuke.js +231 -37
  63. package/lib/Cli/commands/nuke.js.map +1 -1
  64. package/lib/Cloudflare/StateStore/Api.d.ts +1 -1
  65. package/lib/Cloudflare/Workers/Assets.d.ts +26 -8
  66. package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
  67. package/lib/Cloudflare/Workers/Assets.js +46 -1
  68. package/lib/Cloudflare/Workers/Assets.js.map +1 -1
  69. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +2 -2
  70. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  71. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +39 -6
  72. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  73. package/lib/Cloudflare/Workers/Worker.d.ts +7 -5
  74. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  75. package/lib/Cloudflare/Workers/Worker.js +0 -1
  76. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  77. package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -2
  78. package/lib/Cloudflare/Workers/WorkerBundle.d.ts +20 -1
  79. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
  80. package/lib/Cloudflare/Workers/WorkerBundle.js +11 -0
  81. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
  82. package/lib/Cloudflare/Workers/WorkerProvider.d.ts.map +1 -1
  83. package/lib/Cloudflare/Workers/WorkerProvider.js +10 -4
  84. package/lib/Cloudflare/Workers/WorkerProvider.js.map +1 -1
  85. package/lib/Cloudflare/Workflows/WorkflowBridge.d.ts +2 -1
  86. package/lib/Cloudflare/Workflows/WorkflowBridge.d.ts.map +1 -1
  87. package/lib/Cloudflare/Workflows/WorkflowBridge.js +5 -5
  88. package/lib/Cloudflare/Workflows/WorkflowBridge.js.map +1 -1
  89. package/lib/Cloudflare/Workflows/index.d.ts +1 -1
  90. package/lib/Cloudflare/Workflows/index.d.ts.map +1 -1
  91. package/lib/Cloudflare/Workflows/index.js +1 -1
  92. package/lib/Cloudflare/Workflows/index.js.map +1 -1
  93. package/lib/Docker/Container.d.ts +26 -0
  94. package/lib/Docker/Container.d.ts.map +1 -1
  95. package/lib/Docker/Container.js +20 -1
  96. package/lib/Docker/Container.js.map +1 -1
  97. package/lib/Docker/Docker.d.ts +2 -0
  98. package/lib/Docker/Docker.d.ts.map +1 -1
  99. package/lib/Docker/Docker.js.map +1 -1
  100. package/lib/Output.d.ts +4 -1
  101. package/lib/Output.d.ts.map +1 -1
  102. package/lib/Output.js.map +1 -1
  103. package/lib/Provider.d.ts +20 -0
  104. package/lib/Provider.d.ts.map +1 -1
  105. package/lib/Provider.js.map +1 -1
  106. package/lib/Random.d.ts +1 -1
  107. package/lib/Random.d.ts.map +1 -1
  108. package/lib/Resource.d.ts +14 -1
  109. package/lib/Resource.d.ts.map +1 -1
  110. package/lib/Resource.js.map +1 -1
  111. package/lib/SQL/D1.d.ts +1 -1
  112. package/lib/SQL/D1.js +1 -1
  113. package/lib/SQL/Postgres.d.ts +1 -1
  114. package/lib/SQL/Postgres.js +1 -1
  115. package/lib/State/HttpStateApi.d.ts +11 -11
  116. package/lib/Test/Alchemy.d.ts +1 -1
  117. package/lib/Test/Alchemy.js +1 -1
  118. package/lib/Test/Vitest.d.ts +58 -0
  119. package/lib/Test/Vitest.d.ts.map +1 -0
  120. package/lib/Test/Vitest.js +114 -0
  121. package/lib/Test/Vitest.js.map +1 -0
  122. package/lib/Util/proxy-chain.d.ts +5 -0
  123. package/lib/Util/proxy-chain.d.ts.map +1 -1
  124. package/lib/Util/proxy-chain.js +52 -4
  125. package/lib/Util/proxy-chain.js.map +1 -1
  126. package/package.json +23 -15
  127. package/src/AWS/EC2/InternetGateway.ts +8 -3
  128. package/src/AWS/EC2/LingeringEnis.ts +169 -0
  129. package/src/AWS/EC2/SecurityGroup.ts +24 -28
  130. package/src/AWS/EC2/Subnet.ts +23 -34
  131. package/src/AWS/EKS/Addon.ts +17 -4
  132. package/src/AWS/EKS/Deployment.ts +26 -5
  133. package/src/AWS/EKS/HyperPod.ts +97 -0
  134. package/src/AWS/EKS/Job.ts +32 -7
  135. package/src/AWS/EKS/internal/client.ts +40 -3
  136. package/src/AWS/Lambda/Function.ts +5 -3
  137. package/src/AWS/Providers.ts +6 -0
  138. package/src/AWS/SageMaker/Cluster.ts +775 -0
  139. package/src/AWS/SageMaker/ClusterSchedulerConfig.ts +430 -0
  140. package/src/AWS/SageMaker/ComputeQuota.ts +426 -0
  141. package/src/AWS/SageMaker/index.ts +3 -0
  142. package/src/Bundle/PurePlugin.ts +92 -39
  143. package/src/Cli/commands/nuke.ts +295 -56
  144. package/src/Cloudflare/Workers/Assets.ts +53 -1
  145. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +46 -7
  146. package/src/Cloudflare/Workers/Worker.ts +7 -5
  147. package/src/Cloudflare/Workers/WorkerBundle.ts +31 -1
  148. package/src/Cloudflare/Workers/WorkerProvider.ts +18 -4
  149. package/src/Cloudflare/Workflows/WorkflowBridge.ts +5 -5
  150. package/src/Cloudflare/Workflows/index.ts +1 -1
  151. package/src/Docker/Container.ts +30 -1
  152. package/src/Docker/Docker.ts +2 -0
  153. package/src/Output.ts +19 -11
  154. package/src/Provider.ts +20 -0
  155. package/src/Resource.ts +18 -1
  156. package/src/SQL/D1.ts +1 -1
  157. package/src/SQL/Postgres.ts +1 -1
  158. package/src/Test/Alchemy.ts +1 -1
  159. package/src/Test/Vitest.ts +244 -0
  160. package/src/Util/proxy-chain.ts +73 -4
  161. package/lib/SQL/index.d.ts +0 -4
  162. package/lib/SQL/index.d.ts.map +0 -1
  163. package/lib/SQL/index.js +0 -4
  164. package/lib/SQL/index.js.map +0 -1
  165. package/src/SQL/index.ts +0 -3
@@ -20,6 +20,25 @@ import {
20
20
  type DurableObjectExport,
21
21
  } from "./DurableObject.ts";
22
22
 
23
+ /**
24
+ * Bundler options for a Worker: the generic {@link Bundle.BundleExtraOptions}
25
+ * plus rolldown output overrides merged over Alchemy's defaults.
26
+ */
27
+ export interface WorkerBuildOptions extends Bundle.BundleExtraOptions {
28
+ /**
29
+ * Rolldown output options merged over Alchemy's defaults. Use this to
30
+ * control chunking (`codeSplitting`), minification, etc.
31
+ */
32
+ output?: rolldown.OutputOptions;
33
+ /**
34
+ * Forwarded to rolldown's `preserveEntrySignatures` input option. Some
35
+ * `output.codeSplitting` configurations require relaxing it (e.g.
36
+ * `includeDependenciesRecursively: false` needs `"allow-extension"`).
37
+ * Workers must keep their entry exports, so never pass `false`.
38
+ */
39
+ preserveEntrySignatures?: rolldown.InputOptions["preserveEntrySignatures"];
40
+ }
41
+
23
42
  export interface WorkerBundleOptions {
24
43
  id: string;
25
44
  main: string;
@@ -36,7 +55,7 @@ export interface WorkerBundleOptions {
36
55
  exports: Record<string, DurableObjectExport | WorkflowExport>;
37
56
  };
38
57
  stack: { name: string; stage: string };
39
- extraOptions: Bundle.BundleExtraOptions | undefined;
58
+ extraOptions: WorkerBuildOptions | undefined;
40
59
  }
41
60
 
42
61
  export const WorkerBundle = Effect.gen(function* () {
@@ -48,6 +67,7 @@ export const WorkerBundle = Effect.gen(function* () {
48
67
  const realMain = yield* sanitizeMain(options.main);
49
68
  const inputOptions: rolldown.InputOptions = {
50
69
  input: realMain,
70
+ preserveEntrySignatures: options.extraOptions?.preserveEntrySignatures,
51
71
  // Forever-devtool native modules that vite/chokidar reference behind
52
72
  // runtime guards. Rolldown resolves before tree-shaking, so the dead
53
73
  // `require('../pkg')` (lightningcss < 1.32) and `require('fsevents')`
@@ -89,7 +109,17 @@ export const WorkerBundle = Effect.gen(function* () {
89
109
  sourcemap: "hidden",
90
110
  minify: true,
91
111
  keepNames: true,
112
+ // Rolldown's default chunking can split top-level initializer modules
113
+ // (e.g. Drizzle `pgTable` schemas) away from the classes they read,
114
+ // and workerd then evaluates a reader before its imported binding is
115
+ // initialized — the script fails Cloudflare startup validation with
116
+ // `ScriptStartupError: Cannot access '<minified>' before
117
+ // initialization` (#749). `strictExecutionOrder` wraps cross-chunk
118
+ // modules so evaluation follows ESM semantics regardless of how the
119
+ // graph was chunked. See DrizzleSchemaChunks.test.ts.
120
+ strictExecutionOrder: true,
92
121
  dir: `.alchemy/bundles/${options.id}`,
122
+ ...options.extraOptions?.output,
93
123
  };
94
124
  return { inputOptions, outputOptions, extraOptions: options.extraOptions };
95
125
  });
@@ -23,7 +23,12 @@ import { sha256Object } from "../../Util/sha256.ts";
23
23
  import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
24
24
  import { CloudflareLogs } from "../Logs.ts";
25
25
  import { listAllZones, resolveZoneId } from "../Zone/lookup.ts";
26
- import { readAssets, uploadAssets } from "./Assets.ts";
26
+ import {
27
+ mergeAssetsConfigFiles,
28
+ readAssets,
29
+ readAssetsConfigFiles,
30
+ uploadAssets,
31
+ } from "./Assets.ts";
27
32
  import { getCompatibility } from "./Compatibility.ts";
28
33
  import { isDurableObjectExport } from "./DurableObject.ts";
29
34
  import { LocalWorkerProvider } from "./LocalWorkerProvider.ts";
@@ -1360,8 +1365,8 @@ export const LiveWorkerProvider = () =>
1360
1365
  output: Worker["Attributes"] | undefined,
1361
1366
  ) => {
1362
1367
  if (!Predicate.hasProperty(assets, "hash")) return undefined;
1363
- const { directory: _, hash, ...config } = assets;
1364
- return { config, hash, skip: hash === output?.hash?.assets };
1368
+ const { directory, hash, ...config } = assets;
1369
+ return { directory, config, hash, skip: hash === output?.hash?.assets };
1365
1370
  };
1366
1371
 
1367
1372
  const putWorker = Effect.fn(function* (
@@ -1445,7 +1450,16 @@ export const LiveWorkerProvider = () =>
1445
1450
  `Cloudflare Worker update: assets unchanged for ${name}, keeping existing`,
1446
1451
  );
1447
1452
  keepAssets = true;
1448
- metadataAssets = { config: prebuiltAssets.config };
1453
+ // `keepAssets` only preserves the uploaded files — the PUT
1454
+ // replaces the asset config wholesale. The skip path never
1455
+ // walked the directory, so read just `_headers`/`_redirects`
1456
+ // here or a no-op deploy would wipe their rules.
1457
+ metadataAssets = {
1458
+ config: mergeAssetsConfigFiles(
1459
+ prebuiltAssets.config,
1460
+ yield* readAssetsConfigFiles(prebuiltAssets.directory),
1461
+ ),
1462
+ };
1449
1463
  metadataBindings.push({
1450
1464
  type: "assets",
1451
1465
  name: "ASSETS",
@@ -116,7 +116,7 @@ const wrapWorkflowEvent = (event: any): WorkflowEventService["Service"] => ({
116
116
  schedule: event.schedule,
117
117
  });
118
118
 
119
- const wrapWorkflowStep = (step: any): WorkflowStep["Service"] => ({
119
+ export const wrapWorkflowStep = (step: any): WorkflowStep["Service"] => ({
120
120
  do: <T>(options: WorkflowTaskOptions<T, any, any>): Effect.Effect<T> => {
121
121
  const { name } = options;
122
122
  // The surrounding body context is already provided in `task`; the bridge
@@ -151,7 +151,7 @@ const wrapWorkflowStep = (step: any): WorkflowStep["Service"] => ({
151
151
  rollbackConfig: options.rollbackConfig,
152
152
  }
153
153
  : undefined;
154
- return Effect.tryPromise(() => {
154
+ return Effect.promise(() => {
155
155
  if (config && rollback) return step.do(name, config, callback, rollback);
156
156
  if (config) return step.do(name, config, callback);
157
157
  if (rollback) return step.do(name, callback, rollback);
@@ -159,14 +159,14 @@ const wrapWorkflowStep = (step: any): WorkflowStep["Service"] => ({
159
159
  });
160
160
  },
161
161
  sleep: (name: string, duration: string | number): Effect.Effect<void> =>
162
- Effect.tryPromise(() => step.sleep(name, duration)),
162
+ Effect.promise(() => step.sleep(name, duration)),
163
163
  sleepUntil: (name: string, timestamp: Date | number): Effect.Effect<void> =>
164
- Effect.tryPromise(() => step.sleepUntil(name, timestamp)),
164
+ Effect.promise(() => step.sleepUntil(name, timestamp)),
165
165
  waitForEvent: <T>(
166
166
  name: string,
167
167
  options: any,
168
168
  ): Effect.Effect<WorkflowStepEvent<T>> =>
169
- Effect.tryPromise(
169
+ Effect.promise(
170
170
  () => step.waitForEvent(name, options) as Promise<WorkflowStepEvent<T>>,
171
171
  ),
172
172
  });
@@ -1,2 +1,2 @@
1
1
  export * from "./Workflow.ts";
2
- export * from "./WorkflowBridge.ts";
2
+ export { makeWorkflowBridge } from "./WorkflowBridge.ts";
@@ -8,6 +8,7 @@ import { isResolved } from "../Diff.ts";
8
8
  import * as Provider from "../Provider.ts";
9
9
  import { Resource } from "../Resource.ts";
10
10
  import { createInternalTags, hasAlchemyTags } from "../Tags.ts";
11
+ import { toSeconds } from "../Util/Duration.ts";
11
12
  import { Docker, dockerPhysicalName } from "./Docker.ts";
12
13
  import type { Providers } from "./Providers.ts";
13
14
 
@@ -30,6 +31,16 @@ export interface ContainerProps {
30
31
  volumes?: Container.VolumeMapping[];
31
32
  /** Restart policy. */
32
33
  restart?: "no" | "always" | "on-failure" | "unless-stopped";
34
+ /**
35
+ * Container labels. Alchemy's internal ownership labels are added
36
+ * automatically.
37
+ */
38
+ labels?: Record<string, string>;
39
+ /**
40
+ * Grace period before Docker forcefully kills the container after stopping
41
+ * it.
42
+ */
43
+ stopTimeout?: Duration.Input;
33
44
  /** Networks to connect after create. */
34
45
  networks?: Container.NetworkMapping[];
35
46
  /** Remove the container when it exits. @default false */
@@ -161,6 +172,22 @@ export interface Container extends Resource<
161
172
  * });
162
173
  * const runtime = yield* Docker.inspectContainer(postgresName);
163
174
  * ```
175
+ *
176
+ * @section Traefik
177
+ * @example Route a container through Traefik
178
+ * ```typescript
179
+ * const api = yield* Docker.Container("api", {
180
+ * image: "ghcr.io/acme/api:latest",
181
+ * networks: [{ name: "traefik" }],
182
+ * labels: {
183
+ * "traefik.enable": "true",
184
+ * "traefik.http.routers.api.rule": "Host(`api.example.com`)",
185
+ * "traefik.http.services.api.loadbalancer.server.port": "3000",
186
+ * },
187
+ * stopTimeout: "30 seconds",
188
+ * start: true,
189
+ * });
190
+ * ```
164
191
  */
165
192
  export const Container = Resource<Container>("Docker.Container");
166
193
 
@@ -308,7 +335,7 @@ export const ContainerProvider = () =>
308
335
  const internalTags = yield* createInternalTags(id);
309
336
  const { stdout: containerId } = yield* docker.container.create({
310
337
  ...args,
311
- label: internalTags,
338
+ label: { ...args.label, ...internalTags },
312
339
  });
313
340
  yield* Effect.forEach(
314
341
  news.networks ?? [],
@@ -355,6 +382,8 @@ const makeCreateArgs = (id: string, news: ContainerProps, instanceId: string) =>
355
382
  `${port.external}:${port.internal}/${port.protocol ?? "tcp"}`,
356
383
  ),
357
384
  restart: news.restart ?? "no",
385
+ label: news.labels,
386
+ "stop-timeout": toSeconds(news.stopTimeout)?.toString(),
358
387
  rm: news.removeOnExit ?? false,
359
388
  ...(news.healthcheck
360
389
  ? {
@@ -49,6 +49,7 @@ export class Docker extends Context.Service<
49
49
  "health-retries": number | undefined;
50
50
  "health-start-period": string | undefined;
51
51
  "health-start-interval": string | undefined;
52
+ "stop-timeout": string | undefined;
52
53
  p: Array<string> | undefined;
53
54
  command: Array<string> | undefined;
54
55
  label?: Record<string, string>;
@@ -193,6 +194,7 @@ export declare namespace Docker {
193
194
  Cmd: string[] | null;
194
195
  Env: string[] | null;
195
196
  Labels: Record<string, string> | null;
197
+ StopTimeout?: number;
196
198
  Healthcheck?: {
197
199
  Test: string[] | null;
198
200
  Interval?: number;
package/src/Output.ts CHANGED
@@ -12,7 +12,7 @@ import { RuntimeContext, sanitizeKey } from "./RuntimeContext.ts";
12
12
  import { Stack } from "./Stack.ts";
13
13
  import { Stage } from "./Stage.ts";
14
14
  import * as State from "./State/State.ts";
15
- import { isPrimitive } from "./Util/data.ts";
15
+ import { isPrimitive, type Primitive } from "./Util/data.ts";
16
16
 
17
17
  const inspect = Symbol.for("nodejs.util.inspect.custom");
18
18
 
@@ -73,16 +73,24 @@ export interface Output<A = any, Req = any> extends Pipeable {
73
73
 
74
74
  export interface Accessor<A> extends Effect.Effect<A> {}
75
75
 
76
- export type ToOutput<A, Req = never> = [Extract<A, object>] extends [never]
77
- ? Output<A, Req>
78
- : [Extract<A, any[]>] extends [never]
79
- ? ObjectExpr<
80
- {
81
- [attr in keyof A]: A[attr];
82
- },
83
- Req
84
- >
85
- : ArrayExpr<Extract<A, any[]>, Req>;
76
+ export type ToOutput<A, Req = never> =
77
+ // Branded primitives (`string & Brand<"...">`) are assignable to `object`
78
+ // via the brand intersection, so they must short-circuit to a plain Output
79
+ // before the object check — otherwise they explode into an ObjectExpr
80
+ // mapped over every String/Number method. Date is opaque for the same
81
+ // reason (mirrors AttrOutput in Resource.ts).
82
+ [A] extends [Primitive | Date]
83
+ ? Output<A, Req>
84
+ : [Extract<A, object>] extends [never]
85
+ ? Output<A, Req>
86
+ : [Extract<A, any[]>] extends [never]
87
+ ? ObjectExpr<
88
+ {
89
+ [attr in keyof A]: A[attr];
90
+ },
91
+ Req
92
+ >
93
+ : ArrayExpr<Extract<A, any[]>, Req>;
86
94
 
87
95
  export const ExprSymbol = Symbol.for("alchemy/Expr");
88
96
 
package/src/Provider.ts CHANGED
@@ -134,6 +134,26 @@ export interface ProviderService<
134
134
  * {@link singleton}, these are ordinary multi-instance resources.
135
135
  */
136
136
  skip?: boolean;
137
+ /**
138
+ * Provider IDs (picomatch globs, e.g. `"AWS.IAM.Role"` or `"AWS.EC2.*"`)
139
+ * whose resources this type's **cloud-side teardown** consumes — they
140
+ * must still exist while this type is deleting. `alchemy unsafe nuke`
141
+ * topologically orders deletion so every resource of this type is fully
142
+ * gone before any matching type starts deleting; if this type's deletes
143
+ * fail, the matching types are held back rather than deleted out from
144
+ * under an in-flight teardown.
145
+ *
146
+ * Example: SageMaker HyperPod deletes node ENIs by assuming the
147
+ * instance group's execution role — deleting the role (or the VPC)
148
+ * mid-teardown wedges the cluster in `Deleting` permanently, so
149
+ * `AWS.SageMaker.Cluster` declares `dependsOn: ["AWS.IAM.Role", ...]`.
150
+ *
151
+ * The constraint only takes effect when resources of this type are
152
+ * actually present in the nuke target set; declaration cycles collapse
153
+ * into a single concurrent wave (with a logged warning) instead of
154
+ * failing.
155
+ */
156
+ dependsOn?: readonly string[];
137
157
  };
138
158
  /**
139
159
  * Enumerates every existing resource of this type in the ambient scope
package/src/Resource.ts CHANGED
@@ -190,9 +190,26 @@ export type Resource<
190
190
  ...args: any[]
191
191
  ): (binding: Input<Binding>) => Effect.Effect<void>;
192
192
  } & {
193
- [attr in keyof Attributes]-?: Output.Output<Attributes[attr], never>;
193
+ [attr in keyof Attributes]-?: AttrOutput<Attributes[attr]>;
194
194
  };
195
195
 
196
+ /**
197
+ * Accessor type for one attribute. Pure object attributes upgrade to
198
+ * {@link Output.ObjectExpr} so nested access is typed —
199
+ * `hyperpod.instanceGroups.workers` — while primitives, unions with
200
+ * `undefined`, branded string unions (`"a" | (string & {})`), and arrays
201
+ * stay plain {@link Output.Output} (running them through `ToOutput` would
202
+ * classify the `string & {}` branch as an object and explode into
203
+ * String-method mapped types).
204
+ */
205
+ type AttrOutput<A> = [A] extends [
206
+ string | number | boolean | bigint | null | undefined | Date | any[],
207
+ ]
208
+ ? Output.Output<A, never>
209
+ : [A] extends [Record<string, any>]
210
+ ? Output.ObjectExpr<A, never>
211
+ : Output.Output<A, never>;
212
+
196
213
  export interface ResourceOptions {
197
214
  /**
198
215
  * Default removal policy for this resource type when the caller has not
package/src/SQL/D1.ts CHANGED
@@ -31,7 +31,7 @@ export type D1Config = Omit<D1Client.D1ClientConfig, "db">;
31
31
  * resolved once at Worker init and used from any handler:
32
32
  *
33
33
  * ```typescript
34
- * import * as SQL from "alchemy/SQL";
34
+ * import * as SQL from "alchemy/SQL/D1";
35
35
  *
36
36
  * const d1 = yield* Cloudflare.D1.QueryDatabase(Db);
37
37
  * const sql = yield* SQL.D1(d1);
@@ -28,7 +28,7 @@ export type PostgresConfig<E, R> = Omit<PgClient.PgPoolConfig, "url"> & {
28
28
  * handler:
29
29
  *
30
30
  * ```typescript
31
- * import * as SQL from "alchemy/SQL";
31
+ * import * as SQL from "alchemy/SQL/Postgres";
32
32
  *
33
33
  * const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
34
34
  * const sql = yield* SQL.Postgres({ url: hd.connectionString });
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Test adapter for the `alchemy-test` runner (see `packages/alchemy-test`).
5
5
  *
6
- * Same shape as {@link "./Bun.ts"}, but registers
6
+ * Same shape as {@link "./Vitest.ts"} / {@link "./Bun.ts"}, but registers
7
7
  * tests as raw Effects with the alchemy-test harness so the single-process
8
8
  * runner can inject a buffering Logger/Console per test and manage
9
9
  * concurrency + timeouts itself.
@@ -0,0 +1,244 @@
1
+ /** @effect-diagnostics anyUnknownInErrorContext:off */
2
+ import { it } from "@effect/vitest";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Exit from "effect/Exit";
5
+ import * as Scope from "effect/Scope";
6
+ import {
7
+ afterAll as vitestAfterAll,
8
+ afterEach as vitestAfterEach,
9
+ beforeAll as vitestBeforeAll,
10
+ beforeEach as vitestBeforeEach,
11
+ } from "vitest";
12
+
13
+ import type { AlchemyContext } from "../AlchemyContext.ts";
14
+ import type { CompiledStack } from "../Stack.ts";
15
+ import type { Stage } from "../Stage.ts";
16
+ import * as Core from "./Core.ts";
17
+
18
+ export {
19
+ executeWhenReady,
20
+ getWhenReady,
21
+ guardContentType,
22
+ guardedFetchLayer,
23
+ rpcClientLayer,
24
+ WorkerNotReady,
25
+ type EdgeGuardOptions,
26
+ type WhenReadyOptions,
27
+ } from "./Http.ts";
28
+
29
+ export type MakeOptions<ROut = any> = Core.MakeOptions<ROut>;
30
+ export type ScratchStack = Core.ScratchStack;
31
+ export type TestEffect<A, R = never> = Core.TestEffect<A, R>;
32
+
33
+ type TestOptions = number | { timeout?: number };
34
+
35
+ const timeoutOf = (opts: TestOptions | undefined): number | undefined =>
36
+ typeof opts === "number" ? opts : opts?.timeout;
37
+
38
+ interface TestFn {
39
+ (name: string, eff: TestEffect<void>, options?: TestOptions): void;
40
+ skip: (name: string, eff: TestEffect<void>, options?: TestOptions) => void;
41
+ skipIf: (
42
+ condition: boolean,
43
+ ) => (name: string, eff: TestEffect<void>, options?: TestOptions) => void;
44
+ only: (name: string, eff: TestEffect<void>, options?: TestOptions) => void;
45
+ todo: (name: string, eff: TestEffect<void>, options?: TestOptions) => void;
46
+ provider: ProviderFn;
47
+ }
48
+
49
+ interface ProviderFn {
50
+ (
51
+ name: string,
52
+ fn: (stack: ScratchStack) => Effect.Effect<void, any, any>,
53
+ options?: TestOptions,
54
+ ): void;
55
+ skip: (
56
+ name: string,
57
+ fn: (stack: ScratchStack) => Effect.Effect<void, any, any>,
58
+ options?: TestOptions,
59
+ ) => void;
60
+ skipIf: (
61
+ condition: boolean,
62
+ ) => (
63
+ name: string,
64
+ fn: (stack: ScratchStack) => Effect.Effect<void, any, any>,
65
+ options?: TestOptions,
66
+ ) => void;
67
+ }
68
+
69
+ interface BeforeAllFn {
70
+ <A>(eff: TestEffect<A>, options?: TestOptions): Effect.Effect<A>;
71
+ }
72
+
73
+ interface BeforeEachFn {
74
+ (eff: TestEffect<void>, options?: TestOptions): void;
75
+ }
76
+
77
+ interface AfterAllFn {
78
+ (eff: TestEffect<any>, options?: TestOptions): void;
79
+ skipIf: (
80
+ predicate: boolean,
81
+ ) => (eff: TestEffect<any>, options?: TestOptions) => void;
82
+ }
83
+
84
+ interface AfterEachFn {
85
+ (eff: TestEffect<void>, options?: TestOptions): void;
86
+ }
87
+
88
+ export interface TestApi {
89
+ test: TestFn;
90
+ beforeAll: BeforeAllFn;
91
+ beforeEach: BeforeEachFn;
92
+ afterAll: AfterAllFn;
93
+ afterEach: AfterEachFn;
94
+ deploy: <A>(
95
+ stack: TestEffect<CompiledStack<A>, Stage | AlchemyContext>,
96
+ options?: { stage?: string },
97
+ ) => ReturnType<typeof Core.deploy<A>>;
98
+ destroy: (
99
+ stack: TestEffect<CompiledStack, Stage | AlchemyContext>,
100
+ options?: { stage?: string },
101
+ ) => ReturnType<typeof Core.destroy>;
102
+ }
103
+
104
+ const DEFAULT_TIMEOUT = 120_000;
105
+
106
+ /**
107
+ * Build the per-file test API. See {@link "./Bun.ts"} for the same shape
108
+ * over `bun:test`. Vitest variant uses `@effect/vitest`'s `it.live` so
109
+ * Effect-aware tests stay first-class.
110
+ */
111
+ export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
112
+ // See `Test/Bun.ts` for the rationale: the dev sidecar must outlive a
113
+ // single `runPromise` boundary, so all hooks share one scope that's only
114
+ // closed after `destroy(...)` runs.
115
+ const sharedScope = Scope.makeUnsafe("sequential");
116
+ const wrap = <A>(eff: TestEffect<A>) =>
117
+ Core.toEffect(eff, options, sharedScope);
118
+ const runEff = <A>(eff: TestEffect<A>) => Core.run(eff, options, sharedScope);
119
+
120
+ const test = ((name, eff, opts) => {
121
+ it.live(name, () => wrap(eff), timeoutOf(opts));
122
+ }) as TestFn;
123
+
124
+ test.skip = (name, _eff, opts) => {
125
+ it.skip(name, () => {}, timeoutOf(opts));
126
+ };
127
+ test.skipIf = (condition) => (name, eff, opts) => {
128
+ if (condition) {
129
+ it.skip(name, () => {}, timeoutOf(opts));
130
+ } else {
131
+ it.live(name, () => wrap(eff), timeoutOf(opts));
132
+ }
133
+ };
134
+ test.only = (name, eff, opts) => {
135
+ it.only(name, () => wrap(eff) as Effect.Effect<any>, timeoutOf(opts));
136
+ };
137
+ test.todo = (name, _eff, _opts) => {
138
+ it.todo(name);
139
+ };
140
+
141
+ const wrapProvider = (
142
+ name: string,
143
+ fn: (stack: ScratchStack) => Effect.Effect<void, any, any>,
144
+ ) => {
145
+ const scratch = Core.scratchStack(options, name);
146
+ // Guarantee teardown. `test.provider` has no built-in cleanup, so a body
147
+ // that fails (assertion, API error like a 409/Unauthorized) or is
148
+ // interrupted (vitest timeout) BEFORE its trailing `stack.destroy()` would
149
+ // otherwise leak every cloud resource it deployed: the scratch's in-memory
150
+ // state is discarded with the process, so no later run can reclaim the
151
+ // orphan (only an account-wide `nuke` can). `scratch.destroy()` is
152
+ // idempotent (empty-plan apply against the shared scratch state) — a no-op
153
+ // when the body already destroyed, and it reclaims the orphans otherwise.
154
+ // `Effect.ensuring` runs the finalizer on success, failure, AND interruption.
155
+ const body = Core.withProviders(fn(scratch), options, scratch.name).pipe(
156
+ Effect.ensuring(scratch.destroy().pipe(Effect.ignore)),
157
+ );
158
+ return Core.toEffect(
159
+ body,
160
+ { ...options, state: scratch.state },
161
+ sharedScope,
162
+ );
163
+ };
164
+
165
+ const provider = ((name, fn, opts) => {
166
+ it.live(name, () => wrapProvider(name, fn), timeoutOf(opts));
167
+ }) as ProviderFn;
168
+ provider.skip = (name, _fn, opts) => {
169
+ it.skip(name, () => {}, timeoutOf(opts));
170
+ };
171
+ provider.skipIf = (condition) => (name, fn, opts) => {
172
+ if (condition) {
173
+ it.skip(name, () => {}, timeoutOf(opts));
174
+ } else {
175
+ it.live(name, () => wrapProvider(name, fn), timeoutOf(opts));
176
+ }
177
+ };
178
+ test.provider = provider;
179
+
180
+ const beforeAll: BeforeAllFn = <A>(
181
+ eff: TestEffect<A>,
182
+ hookOptions?: TestOptions,
183
+ ) => {
184
+ let result: A;
185
+ vitestBeforeAll(
186
+ () => runEff(eff).then((v) => (result = v)),
187
+ timeoutOf(hookOptions) ?? DEFAULT_TIMEOUT,
188
+ );
189
+ return Effect.sync(() => result);
190
+ };
191
+
192
+ const beforeEach: BeforeEachFn = (eff, hookOptions) => {
193
+ vitestBeforeEach(() => runEff(eff), timeoutOf(hookOptions));
194
+ };
195
+
196
+ const afterAll = ((eff, hookOptions) => {
197
+ vitestAfterAll(
198
+ () => runEff(eff),
199
+ timeoutOf(hookOptions) ?? DEFAULT_TIMEOUT,
200
+ );
201
+ }) as AfterAllFn;
202
+ afterAll.skipIf = (predicate) => (eff, hookOptions) => {
203
+ if (predicate) return;
204
+ vitestAfterAll(
205
+ () => runEff(eff),
206
+ timeoutOf(hookOptions) ?? DEFAULT_TIMEOUT,
207
+ );
208
+ };
209
+
210
+ const afterEach: AfterEachFn = (eff, hookOptions) => {
211
+ vitestAfterEach(() => runEff(eff), timeoutOf(hookOptions));
212
+ };
213
+
214
+ // `destroy(Stack)` needs the dev sidecar alive so it can call `sidecar.stop`.
215
+ // `Scope.close` on an already-closed scope is a no-op, so the destroy
216
+ // wrapper AND the fallback cleanup hook below can both call it safely.
217
+ const closeScope = Effect.suspend(() =>
218
+ Scope.close(sharedScope, Exit.void),
219
+ ).pipe(Effect.ignore);
220
+
221
+ // Fallback cleanup: if the user never calls `destroy(Stack)` (e.g.
222
+ // `NO_DESTROY=1`), nothing else closes the shared scope and the sidecar
223
+ // child process leaks past the test process. Register an `afterAll` that
224
+ // closes it. We defer registration to a microtask so it runs AFTER any
225
+ // user-registered `afterAll` (including `destroy(Stack)`); vitest runs
226
+ // afterAll hooks in registration order.
227
+ queueMicrotask(() => {
228
+ vitestAfterAll(() => Effect.runPromise(closeScope), DEFAULT_TIMEOUT);
229
+ });
230
+
231
+ return {
232
+ test,
233
+ beforeAll,
234
+ beforeEach,
235
+ afterAll,
236
+ afterEach,
237
+ deploy: (stack, callOpts) =>
238
+ Core.deploy(options, stack, { ...callOpts, scope: sharedScope }),
239
+ destroy: (stack, callOpts) =>
240
+ Core.destroy(options, stack, { ...callOpts, scope: sharedScope }).pipe(
241
+ Effect.ensuring(closeScope),
242
+ ),
243
+ };
244
+ };