alchemy 2.0.0-beta.35 → 2.0.0-beta.37
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/exec.js +176 -26
- package/bin/exec.js.map +1 -1
- package/lib/AWS/Lambda/Function.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.js +22 -2
- package/lib/AWS/Lambda/Function.js.map +1 -1
- package/lib/Apply.d.ts.map +1 -1
- package/lib/Apply.js +6 -1
- package/lib/Apply.js.map +1 -1
- package/lib/Cli/main.d.ts.map +1 -1
- package/lib/Cli/main.js +2 -0
- package/lib/Cli/main.js.map +1 -1
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts +54 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js +17 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts +37 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js +45 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/index.d.ts +3 -0
- package/lib/Cloudflare/AnalyticsEngine/index.d.ts.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/index.js +3 -0
- package/lib/Cloudflare/AnalyticsEngine/index.js.map +1 -0
- package/lib/Cloudflare/D1/D1Connection.d.ts +47 -3
- package/lib/Cloudflare/D1/D1Connection.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1Connection.js +62 -5
- package/lib/Cloudflare/D1/D1Connection.js.map +1 -1
- package/lib/Cloudflare/Images/Images.d.ts +103 -0
- package/lib/Cloudflare/Images/Images.d.ts.map +1 -0
- package/lib/Cloudflare/Images/Images.js +82 -0
- package/lib/Cloudflare/Images/Images.js.map +1 -0
- package/lib/Cloudflare/Images/ImagesBinding.d.ts +73 -0
- package/lib/Cloudflare/Images/ImagesBinding.d.ts.map +1 -0
- package/lib/Cloudflare/Images/ImagesBinding.js +83 -0
- package/lib/Cloudflare/Images/ImagesBinding.js.map +1 -0
- package/lib/Cloudflare/Images/index.d.ts +3 -0
- package/lib/Cloudflare/Images/index.d.ts.map +1 -0
- package/lib/Cloudflare/Images/index.js +3 -0
- package/lib/Cloudflare/Images/index.js.map +1 -0
- package/lib/Cloudflare/Local/SidecarHandlers.js +9 -1
- package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +6 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/R2/R2Bucket.d.ts +135 -0
- package/lib/Cloudflare/R2/R2Bucket.d.ts.map +1 -1
- package/lib/Cloudflare/R2/R2Bucket.js +146 -7
- package/lib/Cloudflare/R2/R2Bucket.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +6 -1
- package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/Api.js +23 -1
- package/lib/Cloudflare/StateStore/Api.js.map +1 -1
- package/lib/Cloudflare/StateStore/Store.d.ts +16 -1
- package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/Store.js +22 -5
- package/lib/Cloudflare/StateStore/Store.js.map +1 -1
- package/lib/Cloudflare/Workers/CronEventSource.d.ts +35 -0
- package/lib/Cloudflare/Workers/CronEventSource.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/CronEventSource.js +55 -0
- package/lib/Cloudflare/Workers/CronEventSource.js.map +1 -0
- package/lib/Cloudflare/Workers/InferEnv.d.ts +37 -2
- package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +2 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
- package/lib/Cloudflare/Workers/Rpc.d.ts +76 -3
- package/lib/Cloudflare/Workers/Rpc.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Rpc.js +197 -13
- package/lib/Cloudflare/Workers/Rpc.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +26 -6
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +183 -41
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.js +15 -5
- package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
- package/lib/Cloudflare/Workers/Workflow.d.ts +50 -47
- package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Workflow.js +41 -35
- package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
- package/lib/Cloudflare/Workers/index.d.ts +1 -0
- package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/index.js +1 -0
- package/lib/Cloudflare/Workers/index.js.map +1 -1
- package/lib/Cloudflare/index.d.ts +2 -0
- package/lib/Cloudflare/index.d.ts.map +1 -1
- package/lib/Cloudflare/index.js +2 -0
- package/lib/Cloudflare/index.js.map +1 -1
- package/lib/Deploy.d.ts +4 -1
- package/lib/Deploy.d.ts.map +1 -1
- package/lib/Deploy.js +2 -2
- package/lib/Deploy.js.map +1 -1
- package/lib/Destroy.d.ts +4 -1
- package/lib/Destroy.d.ts.map +1 -1
- package/lib/Destroy.js +2 -2
- package/lib/Destroy.js.map +1 -1
- package/lib/Output.d.ts +45 -5
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js +100 -4
- package/lib/Output.js.map +1 -1
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +93 -12
- package/lib/Plan.js.map +1 -1
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Platform.js +16 -3
- package/lib/Platform.js.map +1 -1
- package/lib/Ref.d.ts +4 -1
- package/lib/Ref.d.ts.map +1 -1
- package/lib/Ref.js +1 -1
- package/lib/Ref.js.map +1 -1
- package/lib/Resource.d.ts +8 -0
- package/lib/Resource.d.ts.map +1 -1
- package/lib/Resource.js +10 -0
- package/lib/Resource.js.map +1 -1
- package/lib/RuntimeContext.d.ts +3 -1
- package/lib/RuntimeContext.d.ts.map +1 -1
- package/lib/RuntimeContext.js.map +1 -1
- package/lib/Secret.d.ts +50 -0
- package/lib/Secret.d.ts.map +1 -0
- package/lib/Secret.js +23 -0
- package/lib/Secret.js.map +1 -0
- package/lib/Serverless/Function.d.ts +9 -1
- package/lib/Serverless/Function.d.ts.map +1 -1
- package/lib/Stack.d.ts +14 -5
- package/lib/Stack.d.ts.map +1 -1
- package/lib/Stack.js +31 -19
- package/lib/Stack.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +15 -1
- package/lib/State/HttpStateApi.d.ts.map +1 -1
- package/lib/State/HttpStateApi.js +11 -0
- package/lib/State/HttpStateApi.js.map +1 -1
- package/lib/State/HttpStateStore.d.ts.map +1 -1
- package/lib/State/HttpStateStore.js +11 -0
- package/lib/State/HttpStateStore.js.map +1 -1
- package/lib/State/InMemoryState.d.ts +2 -2
- package/lib/State/InMemoryState.d.ts.map +1 -1
- package/lib/State/InMemoryState.js +12 -6
- package/lib/State/InMemoryState.js.map +1 -1
- package/lib/State/LocalState.d.ts.map +1 -1
- package/lib/State/LocalState.js +11 -2
- package/lib/State/LocalState.js.map +1 -1
- package/lib/State/State.d.ts +21 -0
- package/lib/State/State.d.ts.map +1 -1
- package/lib/Test/Bun.d.ts.map +1 -1
- package/lib/Test/Bun.js +30 -7
- package/lib/Test/Bun.js.map +1 -1
- package/lib/Test/Core.d.ts +17 -2
- package/lib/Test/Core.d.ts.map +1 -1
- package/lib/Test/Core.js +29 -11
- package/lib/Test/Core.js.map +1 -1
- package/lib/Test/Vitest.d.ts.map +1 -1
- package/lib/Test/Vitest.js +26 -5
- package/lib/Test/Vitest.js.map +1 -1
- package/lib/Util/LocalhostDns.d.ts +23 -0
- package/lib/Util/LocalhostDns.d.ts.map +1 -0
- package/lib/Util/LocalhostDns.js +46 -0
- package/lib/Util/LocalhostDns.js.map +1 -0
- package/lib/Util/PlatformServices.d.ts.map +1 -1
- package/lib/Util/PlatformServices.js +2 -0
- package/lib/Util/PlatformServices.js.map +1 -1
- package/lib/Util/effect.d.ts +7 -0
- package/lib/Util/effect.d.ts.map +1 -1
- package/lib/Util/effect.js +6 -0
- package/lib/Util/effect.js.map +1 -1
- package/lib/Variable.d.ts +29 -0
- package/lib/Variable.d.ts.map +1 -0
- package/lib/Variable.js +9 -0
- package/lib/Variable.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/AWS/Lambda/Function.ts +30 -2
- package/src/Apply.ts +8 -1
- package/src/Cli/main.ts +3 -0
- package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.ts +82 -0
- package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.ts +106 -0
- package/src/Cloudflare/AnalyticsEngine/index.ts +2 -0
- package/src/Cloudflare/D1/D1Connection.ts +95 -12
- package/src/Cloudflare/Images/Images.ts +121 -0
- package/src/Cloudflare/Images/ImagesBinding.ts +192 -0
- package/src/Cloudflare/Images/index.ts +2 -0
- package/src/Cloudflare/Local/SidecarHandlers.ts +9 -1
- package/src/Cloudflare/Providers.ts +8 -0
- package/src/Cloudflare/R2/R2Bucket.ts +271 -7
- package/src/Cloudflare/StateStore/Api.ts +34 -1
- package/src/Cloudflare/StateStore/Store.ts +40 -10
- package/src/Cloudflare/Workers/CronEventSource.ts +89 -0
- package/src/Cloudflare/Workers/InferEnv.ts +94 -16
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +9 -1
- package/src/Cloudflare/Workers/Rpc.ts +227 -18
- package/src/Cloudflare/Workers/Worker.ts +295 -62
- package/src/Cloudflare/Workers/WorkerBundle.ts +15 -5
- package/src/Cloudflare/Workers/Workflow.ts +74 -70
- package/src/Cloudflare/Workers/index.ts +1 -0
- package/src/Cloudflare/index.ts +2 -0
- package/src/Deploy.ts +5 -1
- package/src/Destroy.ts +5 -1
- package/src/Output.ts +121 -12
- package/src/Plan.ts +101 -12
- package/src/Platform.ts +19 -4
- package/src/Ref.ts +10 -5
- package/src/Resource.ts +22 -0
- package/src/RuntimeContext.ts +1 -0
- package/src/Secret.ts +87 -0
- package/src/Serverless/Function.ts +10 -1
- package/src/Stack.ts +52 -29
- package/src/State/HttpStateApi.ts +21 -0
- package/src/State/HttpStateStore.ts +21 -0
- package/src/State/InMemoryState.ts +29 -8
- package/src/State/LocalState.ts +28 -4
- package/src/State/State.ts +21 -0
- package/src/Test/Bun.ts +43 -7
- package/src/Test/Core.ts +29 -8
- package/src/Test/Vitest.ts +36 -4
- package/src/Util/LocalhostDns.ts +52 -0
- package/src/Util/PlatformServices.ts +2 -0
- package/src/Util/effect.ts +25 -0
- package/src/Variable.ts +44 -0
- package/src/index.ts +3 -0
package/src/State/LocalState.ts
CHANGED
|
@@ -46,6 +46,9 @@ export const makeLocalState = () =>
|
|
|
46
46
|
fqn: string;
|
|
47
47
|
}) => path.join(stateDir, stack, stage, `${encodeFqn(fqn)}.json`);
|
|
48
48
|
|
|
49
|
+
const outputFile = ({ stack, stage }: { stack: string; stage: string }) =>
|
|
50
|
+
path.join(stateDir, stack, stage, `__stack_output__.json`);
|
|
51
|
+
|
|
49
52
|
const created = new Set<string>();
|
|
50
53
|
|
|
51
54
|
const ensure = (dir: string) =>
|
|
@@ -107,11 +110,32 @@ export const makeLocalState = () =>
|
|
|
107
110
|
list: (request) =>
|
|
108
111
|
fs.readDirectory(stageDir(request)).pipe(
|
|
109
112
|
recover,
|
|
110
|
-
Effect.map(
|
|
111
|
-
(files)
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
Effect.map((files) =>
|
|
114
|
+
(files ?? [])
|
|
115
|
+
// Filter the bookkeeping file before decoding — `decodeFqn`
|
|
116
|
+
// replaces `__` with `/`, which would turn the literal name
|
|
117
|
+
// `__stack_output__` into `/stack_output/` and slip past the
|
|
118
|
+
// filter, leaving the engine to look up a non-existent
|
|
119
|
+
// resource.
|
|
120
|
+
.filter((file) => file !== "__stack_output__.json")
|
|
121
|
+
.map((file) => decodeFqn(file.replace(/\.json$/, ""))),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
getOutput: (request) =>
|
|
125
|
+
fs.readFile(outputFile(request)).pipe(
|
|
126
|
+
Effect.map((file) => JSON.parse(file.toString(), reviveState)),
|
|
127
|
+
recover,
|
|
128
|
+
),
|
|
129
|
+
setOutput: (request) =>
|
|
130
|
+
ensure(stageDir(request)).pipe(
|
|
131
|
+
Effect.flatMap(() =>
|
|
132
|
+
fs.writeFileString(
|
|
133
|
+
outputFile(request),
|
|
134
|
+
JSON.stringify(encodeState(request.value as any), null, 2),
|
|
135
|
+
),
|
|
114
136
|
),
|
|
137
|
+
recover,
|
|
138
|
+
Effect.map(() => request.value),
|
|
115
139
|
),
|
|
116
140
|
};
|
|
117
141
|
return state;
|
package/src/State/State.ts
CHANGED
|
@@ -87,4 +87,25 @@ export interface StateService {
|
|
|
87
87
|
stack: string;
|
|
88
88
|
stage: string;
|
|
89
89
|
}): Effect.Effect<readonly string[], StateStoreError, never>;
|
|
90
|
+
/**
|
|
91
|
+
* Read the persisted stack output for `(stack, stage)`. Returns
|
|
92
|
+
* `undefined` when the stack has not been deployed (or has been
|
|
93
|
+
* destroyed) at this stage.
|
|
94
|
+
*
|
|
95
|
+
* Stack outputs are written by `apply` once the deploy succeeds and
|
|
96
|
+
* read by cross-stack references (`yield* OtherStack` /
|
|
97
|
+
* `OtherStack.stage.<name>` / `Output.stackRef(...)`).
|
|
98
|
+
*/
|
|
99
|
+
getOutput(request: {
|
|
100
|
+
stack: string;
|
|
101
|
+
stage: string;
|
|
102
|
+
}): Effect.Effect<unknown, StateStoreError, never>;
|
|
103
|
+
/**
|
|
104
|
+
* Persist the resolved stack output for `(stack, stage)`.
|
|
105
|
+
*/
|
|
106
|
+
setOutput(request: {
|
|
107
|
+
stack: string;
|
|
108
|
+
stage: string;
|
|
109
|
+
value: unknown;
|
|
110
|
+
}): Effect.Effect<unknown, StateStoreError, never>;
|
|
90
111
|
}
|
package/src/Test/Bun.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import bun from "bun:test";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import * as Scope from "effect/Scope";
|
|
3
5
|
import type { HookOptions } from "node:test";
|
|
4
6
|
|
|
5
7
|
import type { AlchemyContext } from "../AlchemyContext.ts";
|
|
6
8
|
import type { CompiledStack } from "../Stack.ts";
|
|
7
9
|
import type { Stage } from "../Stage.ts";
|
|
10
|
+
import { installLocalhostDns } from "../Util/LocalhostDns.ts";
|
|
8
11
|
import * as Core from "./Core.ts";
|
|
9
12
|
|
|
13
|
+
installLocalhostDns();
|
|
14
|
+
|
|
10
15
|
export type MakeOptions<ROut = any> = Core.MakeOptions<ROut>;
|
|
11
16
|
export type ScratchStack = Core.ScratchStack;
|
|
12
17
|
export type TestEffect<A, R = never> = Core.TestEffect<A, R>;
|
|
@@ -106,7 +111,15 @@ const DEFAULT_HOOK_TIMEOUT: HookOptions = { timeout: 120_000 };
|
|
|
106
111
|
* ```
|
|
107
112
|
*/
|
|
108
113
|
export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
|
|
109
|
-
|
|
114
|
+
// Single scope shared across `beforeAll`, every `test`, and `afterAll`.
|
|
115
|
+
// Scoped resources in dev mode (the Cloudflare sidecar process and its
|
|
116
|
+
// workerd children) must outlive a single `Effect.runPromise` boundary,
|
|
117
|
+
// otherwise the proxy is killed the moment `beforeAll(deploy(Stack))`
|
|
118
|
+
// resolves and every later `HttpClient.get(workerUrl)` hits a dead port.
|
|
119
|
+
// The scope is closed by `destroy(...)` (or never — the next test run
|
|
120
|
+
// reclaims any leaked sidecar via the lock file).
|
|
121
|
+
const sharedScope = Scope.makeUnsafe("sequential");
|
|
122
|
+
const runEff = <A>(eff: TestEffect<A>) => Core.run(eff, options, sharedScope);
|
|
110
123
|
|
|
111
124
|
const test = ((name, eff, opts) => {
|
|
112
125
|
bun.test(name, () => runEff(eff), opts);
|
|
@@ -130,10 +143,11 @@ export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
|
|
|
130
143
|
fn: (stack: ScratchStack) => Effect.Effect<void, any, any>,
|
|
131
144
|
) => {
|
|
132
145
|
const scratch = Core.scratchStack(options, name);
|
|
133
|
-
return Core.run(
|
|
134
|
-
|
|
135
|
-
state: scratch.state,
|
|
136
|
-
|
|
146
|
+
return Core.run(
|
|
147
|
+
Core.withProviders(fn(scratch), options, scratch.name),
|
|
148
|
+
{ ...options, state: scratch.state },
|
|
149
|
+
sharedScope,
|
|
150
|
+
);
|
|
137
151
|
};
|
|
138
152
|
|
|
139
153
|
const provider = ((name, fn, opts) => {
|
|
@@ -175,13 +189,35 @@ export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
|
|
|
175
189
|
bun.afterEach(() => runEff(eff), hookOptions);
|
|
176
190
|
};
|
|
177
191
|
|
|
192
|
+
// `destroy(Stack)` needs the dev sidecar alive so it can call `sidecar.stop`
|
|
193
|
+
// for each worker. We close the shared scope only AFTER destroy completes.
|
|
194
|
+
// `Scope.close` on an already-closed scope is a no-op, so it's safe for both
|
|
195
|
+
// the destroy wrapper AND the fallback cleanup hook below to call it.
|
|
196
|
+
const closeScope = Effect.suspend(() =>
|
|
197
|
+
Scope.close(sharedScope, Exit.void),
|
|
198
|
+
).pipe(Effect.ignore);
|
|
199
|
+
|
|
200
|
+
// Fallback cleanup: if the user never calls `destroy(Stack)` (e.g.
|
|
201
|
+
// `NO_DESTROY=1`), nothing else closes the shared scope and the sidecar
|
|
202
|
+
// child process leaks past the test process. Register an `afterAll` that
|
|
203
|
+
// closes it. We defer registration to a microtask so it runs AFTER any
|
|
204
|
+
// user-registered `afterAll` (including `destroy(Stack)`); bun runs
|
|
205
|
+
// afterAll hooks in registration order.
|
|
206
|
+
queueMicrotask(() => {
|
|
207
|
+
bun.afterAll(() => Effect.runPromise(closeScope), DEFAULT_HOOK_TIMEOUT);
|
|
208
|
+
});
|
|
209
|
+
|
|
178
210
|
return {
|
|
179
211
|
test,
|
|
180
212
|
beforeAll,
|
|
181
213
|
beforeEach,
|
|
182
214
|
afterAll,
|
|
183
215
|
afterEach,
|
|
184
|
-
deploy: (stack, callOpts) =>
|
|
185
|
-
|
|
216
|
+
deploy: (stack, callOpts) =>
|
|
217
|
+
Core.deploy(options, stack, { ...callOpts, scope: sharedScope }),
|
|
218
|
+
destroy: (stack, callOpts) =>
|
|
219
|
+
Core.destroy(options, stack, { ...callOpts, scope: sharedScope }).pipe(
|
|
220
|
+
Effect.ensuring(closeScope),
|
|
221
|
+
),
|
|
186
222
|
};
|
|
187
223
|
};
|
package/src/Test/Core.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ConfigProvider } from "effect/ConfigProvider";
|
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
3
|
import * as Layer from "effect/Layer";
|
|
4
4
|
import * as Option from "effect/Option";
|
|
5
|
+
import * as Scope from "effect/Scope";
|
|
5
6
|
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
6
7
|
|
|
7
8
|
import { AdoptPolicy } from "../AdoptPolicy.ts";
|
|
@@ -90,14 +91,22 @@ const alchemyLayer = Layer.mergeAll(LoggingCli, AlchemyContextLive);
|
|
|
90
91
|
* registry that the user's `providers` layer populates, the platform layers,
|
|
91
92
|
* and the configured state store. Adapters wrap this into runner-specific
|
|
92
93
|
* thunks (`bun.test` -> `runPromise`, `it.live` -> as-is).
|
|
94
|
+
*
|
|
95
|
+
* When `scope` is provided, scoped resources (like the Cloudflare dev
|
|
96
|
+
* sidecar) survive past this effect and are tied to the lifetime of the
|
|
97
|
+
* provided scope instead. The runner is responsible for closing it.
|
|
98
|
+
*
|
|
99
|
+
* When `scope` is omitted, the effect runs with `Effect.scoped` and any
|
|
100
|
+
* scoped resources are torn down as soon as it resolves.
|
|
93
101
|
*/
|
|
94
102
|
export const toEffect = <A>(
|
|
95
103
|
effect: TestEffect<A>,
|
|
96
104
|
options: MakeOptions,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
105
|
+
scope?: Scope.Scope,
|
|
106
|
+
): Effect.Effect<A, any, never> => {
|
|
107
|
+
const base = Effect.gen(function* () {
|
|
108
|
+
const cfg = yield* loadConfigProvider(Option.none());
|
|
109
|
+
const configProvider = withProfileOverride(cfg, options.profile);
|
|
101
110
|
return yield* effect.pipe(
|
|
102
111
|
provideFreshArtifactStore,
|
|
103
112
|
Effect.provide(Layer.succeed(ConfigProvider, configProvider)),
|
|
@@ -112,14 +121,19 @@ export const toEffect = <A>(
|
|
|
112
121
|
Effect.provide(options.state ?? State.localState()),
|
|
113
122
|
Effect.provideService(AuthProviders, {}),
|
|
114
123
|
Effect.provide(Layer.provideMerge(alchemyLayer, platformLayer)),
|
|
115
|
-
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
scope === undefined ? Effect.scoped(base) : Scope.provide(base, scope)
|
|
116
128
|
) as Effect.Effect<A, any, never>;
|
|
129
|
+
};
|
|
117
130
|
|
|
118
131
|
/** Promise wrapper around {@link toEffect} for `bun.test`-style runners. */
|
|
119
132
|
export const run = <A>(
|
|
120
133
|
effect: TestEffect<A>,
|
|
121
134
|
options: MakeOptions,
|
|
122
|
-
|
|
135
|
+
scope?: Scope.Scope,
|
|
136
|
+
): Promise<A> => Effect.runPromise(toEffect(effect, options, scope));
|
|
123
137
|
|
|
124
138
|
/**
|
|
125
139
|
* Wrap an effect so it runs with `options.providers` + a placeholder Stack +
|
|
@@ -148,27 +162,34 @@ export const withProviders = <A, E, R, ROut>(
|
|
|
148
162
|
* Curried `deploy` for the test factory: bakes in the configured stage and
|
|
149
163
|
* adds the telemetry layer the CLI uses, so `beforeAll(deploy(Stack))` works
|
|
150
164
|
* the same way as `alchemy deploy`.
|
|
165
|
+
*
|
|
166
|
+
* `scope`, when supplied, is forwarded down so the dev sidecar (and other
|
|
167
|
+
* scoped resources) lives until the caller closes it instead of dying as
|
|
168
|
+
* soon as `deploy` resolves. The test harness uses this to keep workerd
|
|
169
|
+
* alive across `beforeAll` → tests → `afterAll`.
|
|
151
170
|
*/
|
|
152
171
|
export const deploy = <A>(
|
|
153
172
|
options: MakeOptions,
|
|
154
173
|
stack: TestEffect<CompiledStack<A>, Stage | AlchemyContext>,
|
|
155
|
-
callOptions?: { stage?: string },
|
|
174
|
+
callOptions?: { stage?: string; scope?: Scope.Scope },
|
|
156
175
|
) =>
|
|
157
176
|
_deploy({
|
|
158
177
|
stack: stack as Effect.Effect<CompiledStack<A>, never, any>,
|
|
159
178
|
stage: callOptions?.stage ?? options.stage ?? "test",
|
|
160
179
|
dev: resolveDev(options),
|
|
180
|
+
scope: callOptions?.scope,
|
|
161
181
|
}).pipe(Effect.provide(TelemetryLive));
|
|
162
182
|
|
|
163
183
|
export const destroy = (
|
|
164
184
|
options: MakeOptions,
|
|
165
185
|
stack: TestEffect<CompiledStack, Stage | AlchemyContext>,
|
|
166
|
-
callOptions?: { stage?: string },
|
|
186
|
+
callOptions?: { stage?: string; scope?: Scope.Scope },
|
|
167
187
|
) =>
|
|
168
188
|
_destroy({
|
|
169
189
|
stack: stack as Effect.Effect<CompiledStack, never, any>,
|
|
170
190
|
stage: callOptions?.stage ?? options.stage ?? "test",
|
|
171
191
|
dev: resolveDev(options),
|
|
192
|
+
scope: callOptions?.scope,
|
|
172
193
|
}).pipe(Effect.provide(TelemetryLive));
|
|
173
194
|
|
|
174
195
|
/**
|
package/src/Test/Vitest.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { it } from "@effect/vitest";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import * as Scope from "effect/Scope";
|
|
3
5
|
import {
|
|
4
6
|
afterAll as vitestAfterAll,
|
|
5
7
|
afterEach as vitestAfterEach,
|
|
@@ -10,8 +12,11 @@ import {
|
|
|
10
12
|
import type { AlchemyContext } from "../AlchemyContext.ts";
|
|
11
13
|
import type { CompiledStack } from "../Stack.ts";
|
|
12
14
|
import type { Stage } from "../Stage.ts";
|
|
15
|
+
import { installLocalhostDns } from "../Util/LocalhostDns.ts";
|
|
13
16
|
import * as Core from "./Core.ts";
|
|
14
17
|
|
|
18
|
+
installLocalhostDns();
|
|
19
|
+
|
|
15
20
|
export type MakeOptions<ROut = any> = Core.MakeOptions<ROut>;
|
|
16
21
|
export type ScratchStack = Core.ScratchStack;
|
|
17
22
|
export type TestEffect<A, R = never> = Core.TestEffect<A, R>;
|
|
@@ -95,8 +100,13 @@ const DEFAULT_TIMEOUT = 120_000;
|
|
|
95
100
|
* Effect-aware tests stay first-class.
|
|
96
101
|
*/
|
|
97
102
|
export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
// See `Test/Bun.ts` for the rationale: the dev sidecar must outlive a
|
|
104
|
+
// single `runPromise` boundary, so all hooks share one scope that's only
|
|
105
|
+
// closed after `destroy(...)` runs.
|
|
106
|
+
const sharedScope = Scope.makeUnsafe("sequential");
|
|
107
|
+
const wrap = <A>(eff: TestEffect<A>) =>
|
|
108
|
+
Core.toEffect(eff, options, sharedScope);
|
|
109
|
+
const runEff = <A>(eff: TestEffect<A>) => Core.run(eff, options, sharedScope);
|
|
100
110
|
|
|
101
111
|
const test = ((name, eff, opts) => {
|
|
102
112
|
it.live(name, () => wrap(eff), timeoutOf(opts));
|
|
@@ -127,6 +137,7 @@ export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
|
|
|
127
137
|
return Core.toEffect(
|
|
128
138
|
Core.withProviders(fn(scratch), options, scratch.name),
|
|
129
139
|
{ ...options, state: scratch.state },
|
|
140
|
+
sharedScope,
|
|
130
141
|
);
|
|
131
142
|
};
|
|
132
143
|
|
|
@@ -179,13 +190,34 @@ export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
|
|
|
179
190
|
vitestAfterEach(() => runEff(eff), timeoutOf(hookOptions));
|
|
180
191
|
};
|
|
181
192
|
|
|
193
|
+
// `destroy(Stack)` needs the dev sidecar alive so it can call `sidecar.stop`.
|
|
194
|
+
// `Scope.close` on an already-closed scope is a no-op, so the destroy
|
|
195
|
+
// wrapper AND the fallback cleanup hook below can both call it safely.
|
|
196
|
+
const closeScope = Effect.suspend(() =>
|
|
197
|
+
Scope.close(sharedScope, Exit.void),
|
|
198
|
+
).pipe(Effect.ignore);
|
|
199
|
+
|
|
200
|
+
// Fallback cleanup: if the user never calls `destroy(Stack)` (e.g.
|
|
201
|
+
// `NO_DESTROY=1`), nothing else closes the shared scope and the sidecar
|
|
202
|
+
// child process leaks past the test process. Register an `afterAll` that
|
|
203
|
+
// closes it. We defer registration to a microtask so it runs AFTER any
|
|
204
|
+
// user-registered `afterAll` (including `destroy(Stack)`); vitest runs
|
|
205
|
+
// afterAll hooks in registration order.
|
|
206
|
+
queueMicrotask(() => {
|
|
207
|
+
vitestAfterAll(() => Effect.runPromise(closeScope), DEFAULT_TIMEOUT);
|
|
208
|
+
});
|
|
209
|
+
|
|
182
210
|
return {
|
|
183
211
|
test,
|
|
184
212
|
beforeAll,
|
|
185
213
|
beforeEach,
|
|
186
214
|
afterAll,
|
|
187
215
|
afterEach,
|
|
188
|
-
deploy: (stack, callOpts) =>
|
|
189
|
-
|
|
216
|
+
deploy: (stack, callOpts) =>
|
|
217
|
+
Core.deploy(options, stack, { ...callOpts, scope: sharedScope }),
|
|
218
|
+
destroy: (stack, callOpts) =>
|
|
219
|
+
Core.destroy(options, stack, { ...callOpts, scope: sharedScope }).pipe(
|
|
220
|
+
Effect.ensuring(closeScope),
|
|
221
|
+
),
|
|
190
222
|
};
|
|
191
223
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-level DNS shim that makes `*.localhost` resolve to 127.0.0.1
|
|
3
|
+
* for any HTTP client that goes through undici (Node's global `fetch`,
|
|
4
|
+
* undici-based clients, etc.).
|
|
5
|
+
*
|
|
6
|
+
* Background: macOS's `getaddrinfo` does not honor RFC 6761 for arbitrary
|
|
7
|
+
* `*.localhost` names — only the bare `localhost` is mapped to loopback.
|
|
8
|
+
* The Cloudflare local proxy returns URLs like
|
|
9
|
+
* `http://my-worker.localhost:1337` and dispatches by the URL hostname,
|
|
10
|
+
* so we cannot rewrite the URL client-side to `127.0.0.1` (and `Host` is
|
|
11
|
+
* a forbidden fetch header). The fix is to make the host resolvable at
|
|
12
|
+
* the network layer.
|
|
13
|
+
*
|
|
14
|
+
* On Bun, `globalThis.fetch` is the native (Zig) implementation and does
|
|
15
|
+
* not consult undici's global dispatcher; Bun resolves `*.localhost`
|
|
16
|
+
* itself on Darwin/Linux, so this shim is effectively a no-op there for
|
|
17
|
+
* the global `fetch` and only matters if user code reaches for `undici`
|
|
18
|
+
* directly.
|
|
19
|
+
*
|
|
20
|
+
* Calling `installLocalhostDns()` is idempotent.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as dns from "node:dns";
|
|
24
|
+
import * as undici from "undici";
|
|
25
|
+
|
|
26
|
+
let installed = false;
|
|
27
|
+
|
|
28
|
+
const isLocalhost = (hostname: string): boolean =>
|
|
29
|
+
hostname === "localhost" ||
|
|
30
|
+
hostname === "[::1]" ||
|
|
31
|
+
hostname.endsWith(".localhost");
|
|
32
|
+
|
|
33
|
+
export const installLocalhostDns = (): void => {
|
|
34
|
+
if (installed) return;
|
|
35
|
+
installed = true;
|
|
36
|
+
|
|
37
|
+
undici.setGlobalDispatcher(
|
|
38
|
+
new undici.Agent({
|
|
39
|
+
connect: {
|
|
40
|
+
lookup: (hostname, options, cb) => {
|
|
41
|
+
if (isLocalhost(hostname)) {
|
|
42
|
+
if (options && (options as { all?: boolean }).all) {
|
|
43
|
+
return cb(null, [{ address: "127.0.0.1", family: 4 }]);
|
|
44
|
+
}
|
|
45
|
+
return cb(null, "127.0.0.1", 4);
|
|
46
|
+
}
|
|
47
|
+
return dns.lookup(hostname, options, cb);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -9,6 +9,7 @@ import type { HttpServer } from "effect/unstable/http/HttpServer";
|
|
|
9
9
|
import type { ServeError } from "effect/unstable/http/HttpServerError";
|
|
10
10
|
import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
11
11
|
import type { WebSocketConstructor } from "effect/unstable/socket/Socket";
|
|
12
|
+
import { installLocalhostDns } from "./LocalhostDns.ts";
|
|
12
13
|
|
|
13
14
|
const isBun = typeof Bun !== "undefined";
|
|
14
15
|
|
|
@@ -52,6 +53,7 @@ export const runMain = <E, A>(
|
|
|
52
53
|
readonly teardown?: Teardown | undefined;
|
|
53
54
|
},
|
|
54
55
|
): void => {
|
|
56
|
+
installLocalhostDns();
|
|
55
57
|
if (isBun) {
|
|
56
58
|
void import("@effect/platform-bun/BunRuntime").then((BunRuntime) =>
|
|
57
59
|
BunRuntime.runMain(effect, options),
|
package/src/Util/effect.ts
CHANGED
|
@@ -64,6 +64,31 @@ export const taggedFunction = <
|
|
|
64
64
|
}) as Tag & Fn;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
export const isYieldableEffect = (
|
|
68
|
+
value: unknown,
|
|
69
|
+
): value is Effect.Effect<unknown, unknown, unknown> =>
|
|
70
|
+
Effect.isEffect(value) &&
|
|
71
|
+
typeof (value as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===
|
|
72
|
+
"function";
|
|
73
|
+
|
|
74
|
+
export type YieldableEffectLike<A = unknown, E = unknown, R = unknown> =
|
|
75
|
+
| Effect.Effect<A, E, R>
|
|
76
|
+
| {
|
|
77
|
+
asEffect: () => Effect.Effect<A, E, R>;
|
|
78
|
+
[Symbol.iterator]: () => Iterator<unknown>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const isEffectClassLike = (
|
|
82
|
+
value: unknown,
|
|
83
|
+
): value is YieldableEffectLike =>
|
|
84
|
+
typeof value === "function" &&
|
|
85
|
+
typeof (value as { asEffect?: unknown }).asEffect === "function";
|
|
86
|
+
|
|
87
|
+
export const isYieldableEffectLike = (
|
|
88
|
+
value: unknown,
|
|
89
|
+
): value is YieldableEffectLike =>
|
|
90
|
+
isYieldableEffect(value) || isEffectClassLike(value);
|
|
91
|
+
|
|
67
92
|
export type UnwrapEffect<T> =
|
|
68
93
|
T extends Effect.Effect<infer A, any, any> ? A : T;
|
|
69
94
|
|
package/src/Variable.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as Config from "effect/Config";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Output from "./Output.ts";
|
|
4
|
+
import { toOutput } from "./Secret.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shapes a {@link variable} can be derived from.
|
|
8
|
+
*/
|
|
9
|
+
export type VariableInput<T, R = never> =
|
|
10
|
+
| T
|
|
11
|
+
| Effect.Effect<T, any, R>
|
|
12
|
+
| Config.Config<T>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bind a non-secret value into the active {@link RuntimeContext} (e.g. a
|
|
16
|
+
* Cloudflare Worker's `env`) under the given `name`. Counterpart to
|
|
17
|
+
* {@link Secret} for plain (non-redacted) values — the resource provider
|
|
18
|
+
* deploys it as `plain_text` / `json` rather than `secret_text`.
|
|
19
|
+
*
|
|
20
|
+
* The result is an `Output<T>` — `yield*`-ing it returns an `Accessor<T>`
|
|
21
|
+
* that resolves the value at runtime.
|
|
22
|
+
*
|
|
23
|
+
* @example One-arg shortcut — reads `Config.string("DATABASE_URL")`
|
|
24
|
+
* ```ts
|
|
25
|
+
* const url = yield* Alchemy.Variable("DATABASE_URL");
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example From a literal
|
|
29
|
+
* ```ts
|
|
30
|
+
* const port = yield* Alchemy.Variable("PORT", 3000);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function Variable(name: string): Output.Output<string, never>;
|
|
34
|
+
export function Variable<T, R = never>(
|
|
35
|
+
name: string,
|
|
36
|
+
value: VariableInput<T, R>,
|
|
37
|
+
): Output.Output<T, R>;
|
|
38
|
+
export function Variable<T = string, R = never>(
|
|
39
|
+
name: string,
|
|
40
|
+
value?: VariableInput<T, R>,
|
|
41
|
+
): Output.Output<T, R> {
|
|
42
|
+
const source: any = value ?? Config.string(name);
|
|
43
|
+
return Output.named(toOutput<T, R>(source), name);
|
|
44
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./Diff.ts";
|
|
|
14
14
|
export * from "./Input.ts";
|
|
15
15
|
export * from "./InstanceId.ts";
|
|
16
16
|
export * from "./Namespace.ts";
|
|
17
|
+
export { stackRef } from "./Output.ts";
|
|
17
18
|
export type { Output } from "./Output.ts";
|
|
18
19
|
export * from "./PhysicalName.ts";
|
|
19
20
|
export * as Plan from "./Plan.ts";
|
|
@@ -23,12 +24,14 @@ export * from "./Ref.ts";
|
|
|
23
24
|
export * as RemovalPolicy from "./RemovalPolicy.ts";
|
|
24
25
|
export * from "./Resource.ts";
|
|
25
26
|
export * as Schema from "./Schema.ts";
|
|
27
|
+
export { Secret } from "./Secret.ts";
|
|
26
28
|
export * as Server from "./Server/index.ts";
|
|
27
29
|
export * as Serverless from "./Serverless/index.ts";
|
|
28
30
|
export { Stack } from "./Stack.ts";
|
|
29
31
|
export * from "./Stage.ts";
|
|
30
32
|
export { inMemoryState, localState } from "./State/index.ts";
|
|
31
33
|
export * from "./Trait.ts";
|
|
34
|
+
export { Variable } from "./Variable.ts";
|
|
32
35
|
|
|
33
36
|
export * as Construct from "./Construct.ts";
|
|
34
37
|
|