effect-pulumi 0.1.1 → 0.1.2
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/README.md +44 -176
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Mixing Effect and Pulumi by hand means wrapping every constructor and every
|
|
|
12
12
|
data-source call yourself:
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
+
// InfraError here is a tagged error you have to define yourself
|
|
15
16
|
const bucket = yield* Effect.try({
|
|
16
17
|
try: () => new aws.s3.Bucket("assets", { forceDestroy: true }),
|
|
17
18
|
catch: (cause) => new InfraError({ cause }),
|
|
@@ -39,8 +40,7 @@ npm install effect-pulumi
|
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
Requires Node.js ≥ 22 (the floor `@pulumi/pulumi` itself sets). The peer
|
|
42
|
-
ranges are `@pulumi/pulumi ^3.0.0` and `effect ^3.0.0
|
|
43
|
-
against `@pulumi/pulumi` 3.255 and `effect` 3.22.
|
|
43
|
+
ranges are `@pulumi/pulumi ^3.0.0` and `effect ^3.0.0`.
|
|
44
44
|
|
|
45
45
|
`@pulumi/pulumi` and `effect` are peer dependencies - this library extends
|
|
46
46
|
your Pulumi and Effect runtimes, so it must use the same copies you do rather
|
|
@@ -52,12 +52,7 @@ npm install @pulumi/pulumi effect
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Ships as dual ESM + CommonJS, so it works from a conventional CJS Pulumi
|
|
55
|
-
program as well as an ESM one
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
import { effectify } from "effect-pulumi"; // ESM
|
|
59
|
-
const { effectify } = require("effect-pulumi"); // CJS
|
|
60
|
-
```
|
|
55
|
+
program as well as an ESM one - both are shown below.
|
|
61
56
|
|
|
62
57
|
## Quick start
|
|
63
58
|
|
|
@@ -86,101 +81,34 @@ const program = Effect.gen(function* () {
|
|
|
86
81
|
export const { id, key } = await Effect.runPromise(program);
|
|
87
82
|
```
|
|
88
83
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
For examples you can actually deploy, see [`examples/`](examples/) - those use
|
|
99
|
-
`@pulumi/random`, whose resources take no inputs from one another, so they
|
|
100
|
-
don't show `fromOutput` or an Effect in an args slot. For that, see
|
|
101
|
-
[`test/random-password-file-program.ts`](test/random-password-file-program.ts):
|
|
102
|
-
a `RandomPassword`'s Output flows into a `local.File`'s args, and it's
|
|
103
|
-
deployed for real by `npm run test:live` - no cloud credentials needed, since
|
|
104
|
-
`@pulumi/local` only touches the local filesystem.
|
|
105
|
-
|
|
106
|
-
## What `effectify` does
|
|
107
|
-
|
|
108
|
-
`effectify(mod)` returns a lazy, memoized proxy over a provider package:
|
|
109
|
-
|
|
110
|
-
| Export kind | Result |
|
|
111
|
-
| --- | --- |
|
|
112
|
-
| `CustomResource` subclass | `(name, args, opts?) => Effect<R, PulumiError>`, and any **top-level** args field may additionally be an `Effect` (several resolve concurrently) |
|
|
113
|
-
| `ComponentResource` subclass | `(name, args, opts?) => Effect<R, PulumiError>`, args passed through verbatim - never auto-lifted |
|
|
114
|
-
| Namespace object (`aws.s3`) | Recursively proxied, lazily |
|
|
115
|
-
| Invoke function (`aws.s3.getBucket`) | Returns `Effect<R, PulumiError>` instead of `Promise<R>` |
|
|
116
|
-
| `*Output` invoke variant (`getBucketOutput`) | Passed through untouched (returns an `Output`, as upstream) |
|
|
117
|
-
| Enums, plain values, sync functions | Passed through untouched |
|
|
118
|
-
|
|
119
|
-
Semantics worth knowing:
|
|
120
|
-
|
|
121
|
-
- **Outputs still work exactly as in vanilla Pulumi.** `effectify` only
|
|
122
|
-
*additionally* accepts `Effect`s; passing a bare `Output<T>` or `Input<T>`
|
|
123
|
-
needs no unwrapping or rewrapping.
|
|
124
|
-
- **Statics survive the wrapping.** `eaws.s3.Bucket.get(name, id)` (adopt an
|
|
125
|
-
existing resource), `isInstance`, and any other codegen'd static forward to
|
|
126
|
-
the original class - `instanceof` works too - so the wrapped package can be
|
|
127
|
-
the only import a program needs.
|
|
128
|
-
- **Component args are never lifted.** Component args aren't guaranteed to be
|
|
129
|
-
`Input<T>`-shaped the way codegen'd `CustomResource` args are - a component
|
|
130
|
-
may do synchronous work on a bare primitive in its constructor - so passing
|
|
131
|
-
an `Effect` where a component expects a primitive is a type error. See
|
|
132
|
-
[Component resources](#component-resources) for the patterns this implies.
|
|
133
|
-
- **Invokes start when you call them.** Whether a function is async is only
|
|
134
|
-
knowable by calling it, so `eaws.getAmi(args)` fires the invoke immediately
|
|
135
|
-
and hands back an Effect that resolves the already-in-flight call - the
|
|
136
|
-
failure still lands in the typed error channel, but `Effect.retry` re-awaits
|
|
137
|
-
the same call rather than re-invoking. To re-invoke per attempt, defer the
|
|
138
|
-
call site: `Effect.suspend(() => eaws.getAmi(args))`.
|
|
139
|
-
|
|
140
|
-
Resource registration stays synchronous under the hood; `Effect.try` runs its
|
|
141
|
-
thunk immediately. This removes wrapper boilerplate, not Pulumi's execution
|
|
142
|
-
model.
|
|
143
|
-
|
|
144
|
-
## Component resources
|
|
145
|
-
|
|
146
|
-
Component resources are fully supported as *consumers*: `effectify` detects
|
|
147
|
-
any class extending `pulumi.ComponentResource` - your own, or those in a
|
|
148
|
-
component-based package like `@pulumi/awsx` - and wraps its constructor into
|
|
149
|
-
an Effect factory, exactly like a custom resource. Construction errors land
|
|
150
|
-
in the typed error channel, and the component sequences with `yield*` like
|
|
151
|
-
everything else.
|
|
152
|
-
|
|
153
|
-
What differs is the args object. Custom resource args can carry `Effect`
|
|
154
|
-
fields because codegen guarantees they are all `Input<T>`-shaped, so
|
|
155
|
-
substituting a resolved value is always legal. Component args are
|
|
156
|
-
hand-authored: a component may take a bare `replicas: number` and do
|
|
157
|
-
synchronous arithmetic on it inside its constructor, and an `Effect` silently
|
|
158
|
-
swapped in there would break it. So for components, `effectify` refuses at
|
|
159
|
-
the type level instead of guessing - resolve your Effects first, then
|
|
160
|
-
construct with plain values:
|
|
84
|
+
Pulumi's TypeScript programs default to CommonJS (no `"type": "module"` in
|
|
85
|
+
`package.json`), where top-level `await` isn't available. `fromOutput` and
|
|
86
|
+
`fromOutputs` resolve an Output's real value, which is genuinely
|
|
87
|
+
asynchronous, so `Effect.runSync` isn't an option here either - it throws on
|
|
88
|
+
any effect that suspends on real async work. Export the Promise itself
|
|
89
|
+
instead; Pulumi's engine awaits an exported Promise the same way it would an
|
|
90
|
+
awaited value. The `program` itself is unchanged - only the first and last
|
|
91
|
+
lines differ:
|
|
161
92
|
|
|
162
93
|
```ts
|
|
163
|
-
const
|
|
94
|
+
const aws = require("@pulumi/aws");
|
|
95
|
+
const { Effect } = require("effect");
|
|
96
|
+
const { effectify, fromOutput, fromOutputs } = require("effect-pulumi");
|
|
164
97
|
|
|
165
|
-
|
|
166
|
-
// ✗ type error - component args are never auto-lifted:
|
|
167
|
-
// eawsx.ecs.Cluster("app", { vpcId: fromOutput(vpc.id) })
|
|
98
|
+
// ... same program ...
|
|
168
99
|
|
|
169
|
-
|
|
170
|
-
// Input<T>-typed component args accept those as in vanilla Pulumi):
|
|
171
|
-
const vpcId = yield* fromOutput(vpc.id);
|
|
172
|
-
const cluster = yield* eawsx.ecs.Cluster("app", { vpcId });
|
|
173
|
-
});
|
|
100
|
+
module.exports = Effect.runPromise(program);
|
|
174
101
|
```
|
|
175
102
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
`
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
103
|
+
`Effect.runSync` does work for programs that never resolve an Output's value
|
|
104
|
+
- e.g. [`examples/random-pet`](examples/random-pet), which exports raw
|
|
105
|
+
`Output`s from resource properties directly rather than reading through them
|
|
106
|
+
with `fromOutput`.
|
|
107
|
+
|
|
108
|
+
Alternatively, hand `program` to the [Automation API](#automation-api) as an
|
|
109
|
+
inline program and deploy it from the same process.
|
|
110
|
+
|
|
111
|
+
For projects you can deploy as-is, see [`examples/`](examples/).
|
|
184
112
|
|
|
185
113
|
## Automation API
|
|
186
114
|
|
|
@@ -191,93 +119,33 @@ re-selecting.
|
|
|
191
119
|
```ts
|
|
192
120
|
const exit = await Effect.runPromiseExit(
|
|
193
121
|
deploy({
|
|
194
|
-
stackName,
|
|
195
|
-
projectName,
|
|
196
|
-
|
|
122
|
+
stackName: "dev",
|
|
123
|
+
projectName: "my-infra",
|
|
124
|
+
// a PulumiFn - run the Effect, return its result as the stack outputs
|
|
125
|
+
program: async () => Effect.runPromise(program),
|
|
197
126
|
up: { onOutput: (out) => process.stdout.write(out) },
|
|
198
127
|
})
|
|
199
128
|
);
|
|
200
129
|
// Exit/Cause instead of a thrown, stringified error
|
|
201
130
|
```
|
|
202
131
|
|
|
203
|
-
|
|
204
|
-
|
|
132
|
+
Stacks are either inline programs (`projectName` + `program`, above) or an
|
|
133
|
+
existing project on disk (`workDir`), and every operation forwards the
|
|
134
|
+
matching Pulumi options type - `UpOptions`, `DestroyOptions`, and so on. Pass
|
|
205
135
|
`onOutput` to stream the CLI's progress; without it a multi-minute deploy
|
|
206
|
-
prints nothing until it finishes.
|
|
207
|
-
(`projectName` + `program`) or an existing project on disk (`workDir`).
|
|
208
|
-
|
|
209
|
-
Previewing is opt-in via `preview: true` (or a `PreviewOptions` object), and
|
|
210
|
-
its result comes back on `DeployResult.preview`. It is off by default because
|
|
211
|
-
a preview is a full engine run against the provider, so previewing and then
|
|
212
|
-
immediately upping does the work twice - and `up` surfaces the same failures.
|
|
213
|
-
|
|
214
|
-
Teardown is two steps. `destroyStack` removes the resources but leaves the
|
|
215
|
-
stack registered with the backend; `teardownStack` destroys and then deletes
|
|
216
|
-
it, which is what you want for per-run stacks. `RemoveOptions.force` deletes a
|
|
217
|
-
stack while leaving its resources alive and billing - it is reachable, but it
|
|
218
|
-
orphans them.
|
|
136
|
+
prints nothing until it finishes.
|
|
219
137
|
|
|
220
|
-
|
|
138
|
+
Preview is opt-in via `preview: true` and comes back on
|
|
139
|
+
`DeployResult.preview`. It is off by default because a preview is a full
|
|
140
|
+
engine run, so previewing and then immediately upping does the work twice -
|
|
141
|
+
and `up` surfaces the same failures anyway.
|
|
221
142
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
143
|
+
Teardown is two steps: `destroyStack` removes the resources but leaves the
|
|
144
|
+
stack registered, while `teardownStack` destroys and then deletes it - the
|
|
145
|
+
latter is what per-run stacks want. Avoid `RemoveOptions.force`, which drops
|
|
146
|
+
a stack while leaving its resources alive and billing with nothing tracking
|
|
147
|
+
them.
|
|
226
148
|
|
|
227
|
-
|
|
228
|
-
import { Effect } from "effect";
|
|
229
|
-
import { deploy } from "effect-pulumi";
|
|
230
|
-
|
|
231
|
-
const guarded = deploy({ stackName, projectName, program }).pipe(
|
|
232
|
-
// Transient engine failures during `up` are worth another attempt; a
|
|
233
|
-
// failure creating the stack or setting config is not.
|
|
234
|
-
Effect.retry({ times: 2, while: (error) => error.stage === "up" }),
|
|
235
|
-
Effect.catchTag("AutomationError", (error) =>
|
|
236
|
-
Effect.fail(new Error(`deploy failed at ${error.stage}: ${error.message}`))
|
|
237
|
-
)
|
|
238
|
-
);
|
|
239
|
-
```
|
|
149
|
+
## License
|
|
240
150
|
|
|
241
|
-
|
|
242
|
-
resource, or `Effect.exit` / `runPromiseExit` at the edge to inspect the full
|
|
243
|
-
`Cause`.
|
|
244
|
-
|
|
245
|
-
## API
|
|
246
|
-
|
|
247
|
-
| Export | What it does |
|
|
248
|
-
| --- | --- |
|
|
249
|
-
| `effectify(mod)` | Wrap a provider package (or any namespace) once; see the table above |
|
|
250
|
-
| `fromOutput(output)` | `Output<T>` → `Effect<T, PulumiError>`; resolves to the unknown sentinel during `preview` instead of throwing |
|
|
251
|
-
| `fromOutputs(record)` | Record of Outputs → `Effect` of the resolved record |
|
|
252
|
-
| `deploy(opts)` | Select/create stack → apply config → optional preview → `up`; returns `{ stack, result, preview? }` |
|
|
253
|
-
| `createOrSelectStack(opts)` | Inline (`projectName` + `program`) or local (`workDir`) stack |
|
|
254
|
-
| `setStackConfig(stack, config)` | Apply a whole config map in one `setAllConfig` round-trip |
|
|
255
|
-
| `previewStack(stack, opts?)` | `pulumi preview`, returning the `PreviewResult` |
|
|
256
|
-
| `upStack(stack, opts?)` | `pulumi up`, returning the `UpResult` |
|
|
257
|
-
| `refreshStack(stack, opts?)` | Re-sync state from the actual cloud resources |
|
|
258
|
-
| `stackOutputs(stack)` | Read current outputs without running an update |
|
|
259
|
-
| `destroyStack(stack, opts?)` | Destroy resources; the stack stays registered |
|
|
260
|
-
| `removeStack(stack, opts?)` | Delete the stack from the backend |
|
|
261
|
-
| `teardownStack(stack, opts?)` | Destroy then remove - skips the remove if the destroy failed |
|
|
262
|
-
| `PulumiError` | Construction / Output-resolution failure; carries `cause` |
|
|
263
|
-
| `AutomationError` | Lifecycle failure; carries `stage` and `cause` |
|
|
264
|
-
|
|
265
|
-
## Scripts
|
|
266
|
-
|
|
267
|
-
| Command | What it does |
|
|
268
|
-
| --- | --- |
|
|
269
|
-
| `npm run build` | Build dual ESM + CJS (`tsup`) with declarations to `dist/` |
|
|
270
|
-
| `npm run typecheck` | Type-check everything, including tests and examples |
|
|
271
|
-
| `npm run lint` | Lint with `oxlint` (`npm run lint:fix` applies safe fixes) |
|
|
272
|
-
| `npm run format` | Format with `oxfmt` (`npm run format:check` asserts instead) |
|
|
273
|
-
| `npm run check` | `typecheck` + `lint` + `format:check`, the pre-commit sweep |
|
|
274
|
-
| `npm test` | Unit + mocked-provider tests. No credentials needed |
|
|
275
|
-
| `npm run test:package` | Builds, packs a tarball and consumes it from ESM and CJS projects |
|
|
276
|
-
| `npm run test:live` | Deploys `test/s3-bucket-program.ts` against real AWS, then destroys it |
|
|
277
|
-
|
|
278
|
-
`npm test` never runs the live harness: it's excluded in `vitest.config.ts`
|
|
279
|
-
and additionally gated on `EFFECT_PULUMI_RUN_LIVE_TESTS=1`. The live harness
|
|
280
|
-
picks up credentials from the environment the normal way each provider expects
|
|
281
|
-
(e.g. `AWS_PROFILE` / `AWS_ACCESS_KEY_ID`), and uses
|
|
282
|
-
`Effect.acquireRelease` + `Effect.scoped` so the stack is destroyed even when
|
|
283
|
-
an assertion fails.
|
|
151
|
+
MIT - see [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-pulumi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Effect bindings for Pulumi - wrap a provider package once and get Effect-returning resource factories, typed errors, and structured deploy results.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|