effect-pulumi 0.1.1 → 0.2.0
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 +46 -176
- package/dist/index.cjs +460 -199
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -148
- package/dist/index.d.ts +93 -148
- package/dist/index.js +436 -177
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
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,9 @@ 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 ^
|
|
43
|
-
|
|
43
|
+
ranges are `@pulumi/pulumi ^3.0.0` and `effect ^4.0.0`. Projects still on
|
|
44
|
+
Effect 3 should stay on `effect-pulumi@0.1.2`, the last release that supports
|
|
45
|
+
it.
|
|
44
46
|
|
|
45
47
|
`@pulumi/pulumi` and `effect` are peer dependencies - this library extends
|
|
46
48
|
your Pulumi and Effect runtimes, so it must use the same copies you do rather
|
|
@@ -52,12 +54,7 @@ npm install @pulumi/pulumi effect
|
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
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
|
-
```
|
|
57
|
+
program as well as an ESM one - both are shown below.
|
|
61
58
|
|
|
62
59
|
## Quick start
|
|
63
60
|
|
|
@@ -86,101 +83,34 @@ const program = Effect.gen(function* () {
|
|
|
86
83
|
export const { id, key } = await Effect.runPromise(program);
|
|
87
84
|
```
|
|
88
85
|
|
|
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:
|
|
86
|
+
Pulumi's TypeScript programs default to CommonJS (no `"type": "module"` in
|
|
87
|
+
`package.json`), where top-level `await` isn't available. `fromOutput` and
|
|
88
|
+
`fromOutputs` resolve an Output's real value, which is genuinely
|
|
89
|
+
asynchronous, so `Effect.runSync` isn't an option here either - it throws on
|
|
90
|
+
any effect that suspends on real async work. Export the Promise itself
|
|
91
|
+
instead; Pulumi's engine awaits an exported Promise the same way it would an
|
|
92
|
+
awaited value. The `program` itself is unchanged - only the first and last
|
|
93
|
+
lines differ:
|
|
161
94
|
|
|
162
95
|
```ts
|
|
163
|
-
const
|
|
96
|
+
const aws = require("@pulumi/aws");
|
|
97
|
+
const { Effect } = require("effect");
|
|
98
|
+
const { effectify, fromOutput, fromOutputs } = require("effect-pulumi");
|
|
164
99
|
|
|
165
|
-
|
|
166
|
-
// ✗ type error - component args are never auto-lifted:
|
|
167
|
-
// eawsx.ecs.Cluster("app", { vpcId: fromOutput(vpc.id) })
|
|
100
|
+
// ... same program ...
|
|
168
101
|
|
|
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
|
-
});
|
|
102
|
+
module.exports = Effect.runPromise(program);
|
|
174
103
|
```
|
|
175
104
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
`
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
105
|
+
`Effect.runSync` does work for programs that never resolve an Output's value
|
|
106
|
+
- e.g. [`examples/random-pet`](examples/random-pet), which exports raw
|
|
107
|
+
`Output`s from resource properties directly rather than reading through them
|
|
108
|
+
with `fromOutput`.
|
|
109
|
+
|
|
110
|
+
Alternatively, hand `program` to the [Automation API](#automation-api) as an
|
|
111
|
+
inline program and deploy it from the same process.
|
|
112
|
+
|
|
113
|
+
For projects you can deploy as-is, see [`examples/`](examples/).
|
|
184
114
|
|
|
185
115
|
## Automation API
|
|
186
116
|
|
|
@@ -191,93 +121,33 @@ re-selecting.
|
|
|
191
121
|
```ts
|
|
192
122
|
const exit = await Effect.runPromiseExit(
|
|
193
123
|
deploy({
|
|
194
|
-
stackName,
|
|
195
|
-
projectName,
|
|
196
|
-
|
|
124
|
+
stackName: "dev",
|
|
125
|
+
projectName: "my-infra",
|
|
126
|
+
// a PulumiFn - run the Effect, return its result as the stack outputs
|
|
127
|
+
program: async () => Effect.runPromise(program),
|
|
197
128
|
up: { onOutput: (out) => process.stdout.write(out) },
|
|
198
129
|
})
|
|
199
130
|
);
|
|
200
131
|
// Exit/Cause instead of a thrown, stringified error
|
|
201
132
|
```
|
|
202
133
|
|
|
203
|
-
|
|
204
|
-
|
|
134
|
+
Stacks are either inline programs (`projectName` + `program`, above) or an
|
|
135
|
+
existing project on disk (`workDir`), and every operation forwards the
|
|
136
|
+
matching Pulumi options type - `UpOptions`, `DestroyOptions`, and so on. Pass
|
|
205
137
|
`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.
|
|
138
|
+
prints nothing until it finishes.
|
|
219
139
|
|
|
220
|
-
|
|
140
|
+
Preview is opt-in via `preview: true` and comes back on
|
|
141
|
+
`DeployResult.preview`. It is off by default because a preview is a full
|
|
142
|
+
engine run, so previewing and then immediately upping does the work twice -
|
|
143
|
+
and `up` surfaces the same failures anyway.
|
|
221
144
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
145
|
+
Teardown is two steps: `destroyStack` removes the resources but leaves the
|
|
146
|
+
stack registered, while `teardownStack` destroys and then deletes it - the
|
|
147
|
+
latter is what per-run stacks want. Avoid `RemoveOptions.force`, which drops
|
|
148
|
+
a stack while leaving its resources alive and billing with nothing tracking
|
|
149
|
+
them.
|
|
226
150
|
|
|
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
|
-
```
|
|
151
|
+
## License
|
|
240
152
|
|
|
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.
|
|
153
|
+
MIT - see [LICENSE](LICENSE).
|