alchemy 0.1.4 → 0.1.5
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/package.json +5 -2
- package/src/aws/auto/cfn.json +225310 -0
- package/src/aws/auto/docs.ts +73 -0
- package/src/aws/auto/index.ts +4 -0
- package/src/aws/auto/resource.ts +269 -0
- package/src/aws/auto/service.ts +51 -0
- package/src/aws/auto/spec.ts +160 -0
- package/src/markdown/design.ts +2 -2
- package/src/stripe/price.ts +244 -0
- package/src/stripe/product.ts +193 -0
- package/src/stripe/webhook.ts +184 -0
- package/src/typescript/install-dependencies.ts +61 -0
- package/test/aws/function.test.ts +4 -4
- package/test/aws/queue.test.ts +1 -1
- package/test/aws/role.test.ts +2 -2
- package/test/aws/table.test.ts +1 -1
- package/test/esbuild.test.ts +1 -1
- package/test/scope.test.ts +3 -3
- package/test/stripe/stripe.test.ts +136 -0
- package/README.md +0 -324
- package/src/typescript/install-packages.ts +0 -50
package/test/aws/role.test.ts
CHANGED
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
} from "@aws-sdk/client-iam";
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
7
|
import { apply } from "../../src/apply";
|
|
8
|
-
import type { PolicyDocument } from "../../src/
|
|
9
|
-
import { Role, type RoleProps } from "../../src/
|
|
8
|
+
import type { PolicyDocument } from "../../src/aws/policy";
|
|
9
|
+
import { Role, type RoleProps } from "../../src/aws/role";
|
|
10
10
|
import { destroy } from "../../src/destroy";
|
|
11
11
|
|
|
12
12
|
// Verify role was deleted
|
package/test/aws/table.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "@aws-sdk/client-dynamodb";
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
7
|
import { apply } from "../../src/apply";
|
|
8
|
-
import { Table } from "../../src/
|
|
8
|
+
import { Table } from "../../src/aws/table";
|
|
9
9
|
import { destroy } from "../../src/destroy";
|
|
10
10
|
|
|
11
11
|
const dynamo = new DynamoDBClient({});
|
package/test/esbuild.test.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { apply } from "../src/apply";
|
|
5
|
-
import { Bundle } from "../src/components/esbuild";
|
|
6
5
|
import { destroy } from "../src/destroy";
|
|
6
|
+
import { Bundle } from "../src/esbuild";
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
9
9
|
|
package/test/scope.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { apply } from "../src/apply";
|
|
3
|
-
import type { PolicyDocument } from "../src/
|
|
4
|
-
import { Role } from "../src/
|
|
5
|
-
import { File } from "../src/components/fs";
|
|
3
|
+
import type { PolicyDocument } from "../src/aws/policy";
|
|
4
|
+
import { Role } from "../src/aws/role";
|
|
6
5
|
import { destroy } from "../src/destroy";
|
|
6
|
+
import { File } from "../src/fs";
|
|
7
7
|
import { rootScope } from "../src/global";
|
|
8
8
|
import { type Context, Resource } from "../src/resource";
|
|
9
9
|
import { Scope, getScope, withScope } from "../src/scope";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import Stripe from "stripe";
|
|
3
|
+
import { apply } from "../../src/apply";
|
|
4
|
+
import { destroy } from "../../src/destroy";
|
|
5
|
+
import { Price } from "../../src/stripe/price";
|
|
6
|
+
import { Product } from "../../src/stripe/product";
|
|
7
|
+
import { WebhookEndpoint } from "../../src/stripe/webhook";
|
|
8
|
+
|
|
9
|
+
const stripeApiKey = process.env.STRIPE_API_KEY;
|
|
10
|
+
if (!stripeApiKey) {
|
|
11
|
+
throw new Error("STRIPE_API_KEY environment variable is required");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Initialize a Stripe client for verification
|
|
15
|
+
const stripe = new Stripe(stripeApiKey);
|
|
16
|
+
|
|
17
|
+
describe("Stripe Resources", () => {
|
|
18
|
+
test("create and destroy stripe resources", async () => {
|
|
19
|
+
// Create a test product
|
|
20
|
+
const product = new Product("alchemy-test-product", {
|
|
21
|
+
name: "Alchemy Test Product",
|
|
22
|
+
description: "A product created by Alchemy tests",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Apply the product first to get its ID
|
|
26
|
+
const productOutput = await apply(product);
|
|
27
|
+
expect(productOutput.id).toBeTruthy();
|
|
28
|
+
expect(productOutput.name).toBe("Alchemy Test Product");
|
|
29
|
+
|
|
30
|
+
// Verify with Stripe API
|
|
31
|
+
const stripeProduct = await stripe.products.retrieve(productOutput.id);
|
|
32
|
+
expect(stripeProduct.name).toBe("Alchemy Test Product");
|
|
33
|
+
|
|
34
|
+
// Create a price for the product
|
|
35
|
+
const price = new Price("alchemy-test-price", {
|
|
36
|
+
product: productOutput.id,
|
|
37
|
+
currency: "usd",
|
|
38
|
+
unitAmount: 1500, // $15.00
|
|
39
|
+
recurring: {
|
|
40
|
+
interval: "month",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Apply the price
|
|
45
|
+
const priceOutput = await apply(price);
|
|
46
|
+
expect(priceOutput.id).toBeTruthy();
|
|
47
|
+
expect(priceOutput.unitAmount).toBe(1500);
|
|
48
|
+
expect(priceOutput.recurring?.interval).toBe("month");
|
|
49
|
+
|
|
50
|
+
// Verify with Stripe API
|
|
51
|
+
const stripePrice = await stripe.prices.retrieve(priceOutput.id);
|
|
52
|
+
expect(stripePrice.unit_amount).toBe(1500);
|
|
53
|
+
|
|
54
|
+
// Create a webhook endpoint
|
|
55
|
+
const webhook = new WebhookEndpoint("alchemy-test-webhook", {
|
|
56
|
+
url: "https://example.com/alchemy-webhook",
|
|
57
|
+
enabledEvents: [
|
|
58
|
+
"checkout.session.completed",
|
|
59
|
+
"customer.subscription.created",
|
|
60
|
+
],
|
|
61
|
+
description: "Webhook for Alchemy tests",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Apply the webhook
|
|
65
|
+
const webhookOutput = await apply(webhook);
|
|
66
|
+
expect(webhookOutput.id).toBeTruthy();
|
|
67
|
+
expect(webhookOutput.url).toBe("https://example.com/alchemy-webhook");
|
|
68
|
+
expect(webhookOutput.secret).toBeTruthy();
|
|
69
|
+
|
|
70
|
+
// Verify with Stripe API
|
|
71
|
+
const stripeWebhook = await stripe.webhookEndpoints.retrieve(
|
|
72
|
+
webhookOutput.id,
|
|
73
|
+
);
|
|
74
|
+
expect(stripeWebhook.url).toBe("https://example.com/alchemy-webhook");
|
|
75
|
+
|
|
76
|
+
// Clean up resources
|
|
77
|
+
console.log("Cleaning up Stripe resources...");
|
|
78
|
+
await destroy(webhook);
|
|
79
|
+
await destroy(price);
|
|
80
|
+
await destroy(product);
|
|
81
|
+
|
|
82
|
+
// Verify clean up
|
|
83
|
+
await assertProductDeactivated(productOutput.id);
|
|
84
|
+
await assertPriceDeactivated(priceOutput.id);
|
|
85
|
+
await assertWebhookDeleted(webhookOutput.id);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Helper functions for verification
|
|
90
|
+
async function assertProductDeactivated(productId: string) {
|
|
91
|
+
try {
|
|
92
|
+
const product = await stripe.products.retrieve(productId);
|
|
93
|
+
// Products are deactivated, not deleted
|
|
94
|
+
expect(product.active).toBe(false);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// If product is not found, that's also acceptable
|
|
97
|
+
if (
|
|
98
|
+
error instanceof Stripe.errors.StripeError &&
|
|
99
|
+
error.code === "resource_missing"
|
|
100
|
+
) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function assertPriceDeactivated(priceId: string) {
|
|
108
|
+
try {
|
|
109
|
+
const price = await stripe.prices.retrieve(priceId);
|
|
110
|
+
// Prices are deactivated, not deleted
|
|
111
|
+
expect(price.active).toBe(false);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// If price is not found, that's also acceptable
|
|
114
|
+
if (
|
|
115
|
+
error instanceof Stripe.errors.StripeError &&
|
|
116
|
+
error.code === "resource_missing"
|
|
117
|
+
) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function assertWebhookDeleted(webhookId: string) {
|
|
125
|
+
try {
|
|
126
|
+
await stripe.webhookEndpoints.retrieve(webhookId);
|
|
127
|
+
throw new Error("Webhook still exists");
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Webhook should be deleted, so we expect a resource_missing error
|
|
130
|
+
if (error instanceof Stripe.errors.StripeError) {
|
|
131
|
+
expect(error.code).toBe("resource_missing");
|
|
132
|
+
} else {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/README.md
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
# Alchemy
|
|
2
|
-
|
|
3
|
-
Alchemy is a JS-native, embeddable library for the materialization of resource graphs. From Infrastructure-as-Code (IaC) to code generation, Alchemy provides the fundamental building blocks for modeling resources that are Created, Updated and Deleted automatically.
|
|
4
|
-
|
|
5
|
-
Unlike similar tools like Pulumi, Terraform, and CloudFormation, Alchemy is implemented in pure ESM-native TypeScript with zero dependencies. It can run in any JavaScript runtime, including the browser.
|
|
6
|
-
|
|
7
|
-
[](./alchemy.gif)
|
|
8
|
-
|
|
9
|
-
# Features
|
|
10
|
-
|
|
11
|
-
- **JS-native** - no second language, toolchains, dependencies, processes, services, etc. to lug around.
|
|
12
|
-
- **ESM-native** - built exclusively on ESM, with a slight preference for modern JS runtimes like Bun.
|
|
13
|
-
- **Embeddable** - runs in any JavaScript/TypeScript environment, including the browser!
|
|
14
|
-
- **Extensible** - implement your own resources with a simple function.
|
|
15
|
-
- **AI-first** - alchemy actively encourages you to use LLMs to create/copy/fork/modify resources to fit your needs. No more waiting around for a provider to be implemented, just do it yourself in a few minutes.
|
|
16
|
-
- **No dependencies** - the `alchemy` core package has 0 required dependencies.
|
|
17
|
-
- **No service** - state files are stored locally in your project and can be easily inspected, modified, checked into your repo, etc.
|
|
18
|
-
- **No strong opinions** - structure your codebase however you want, store state anywhere - we don't care!
|
|
19
|
-
|
|
20
|
-
# Examples
|
|
21
|
-
|
|
22
|
-
- LLM-based code generation of all 200+ AWS CloudFormation resources: [examples/generate-aws-cfn/gen.ts](./examples/generate-aws-cfn/gen.ts)
|
|
23
|
-
- Deploy an AWS Lambda Function with a DynamoDB Table and IAM Role: [examples/deploy-aws/app.ts](./examples/deploy-aws/)
|
|
24
|
-
- Generate a TODO application from a Markdown document: [examples/markdown-program/index.md](./examples/markdown-program/)
|
|
25
|
-
|
|
26
|
-
# Getting Started
|
|
27
|
-
|
|
28
|
-
An alchemy "app" (if you want to call it that) is just an ordinary TypeScript or JavaScript script. Once you've installed the `alchemy` package, you can start using it however you want.
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
# I recommend bun, but you can use any JavaScript runtime.
|
|
32
|
-
bun add alchemy
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Usually, you'll want to create a script where you'll define your resources.
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
import { Role } from "alchemy/aws";
|
|
39
|
-
|
|
40
|
-
const role = new Role("my-role", {
|
|
41
|
-
name: "my-role",
|
|
42
|
-
//..
|
|
43
|
-
});
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Then, call `alchemize` at the end to trigger the Create/Update/Delete lifecycle.
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
await alchemize();
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Finally, run your script.
|
|
53
|
-
|
|
54
|
-
```sh
|
|
55
|
-
bun ./my-app.ts
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
You'll notice some files show up in your repo:
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
.alchemy/
|
|
62
|
-
- sam/
|
|
63
|
-
- my-role.json
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Go ahead, click on one and take a look. Here's how my role looks:
|
|
67
|
-
|
|
68
|
-
```jsonc
|
|
69
|
-
{
|
|
70
|
-
"provider": "iam::Role",
|
|
71
|
-
"data": {},
|
|
72
|
-
"deps": [],
|
|
73
|
-
"status": "updated",
|
|
74
|
-
"output": {
|
|
75
|
-
"roleName": "alchemy-api-lambda-role"
|
|
76
|
-
// ..
|
|
77
|
-
},
|
|
78
|
-
"inputs": [
|
|
79
|
-
{
|
|
80
|
-
"roleName": "alchemy-api-lambda-role",
|
|
81
|
-
"assumeRolePolicy": {
|
|
82
|
-
"Version": "2012-10-17"
|
|
83
|
-
// ..
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
]
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
> [!TIP]
|
|
91
|
-
> Alchemy goes to great effort to be fully transparent. State is just a JSON file, nothing more. You can inspect it, modify it, commit it to your repo, etc.
|
|
92
|
-
|
|
93
|
-
## `alchemize`
|
|
94
|
-
|
|
95
|
-
Calling `alchemize` will create new resources, update modified resources and then delete "orphaned" resources at the end.
|
|
96
|
-
|
|
97
|
-
This is equivalent to `terraform apply`, `pulumi up`, `cdk deploy`, etc. except really fast and in your control.
|
|
98
|
-
|
|
99
|
-
## Creating a Resource Provider
|
|
100
|
-
|
|
101
|
-
Adding new resources is the whole point of Alchemy, and is therefore very simple.
|
|
102
|
-
|
|
103
|
-
A Resource provider is just a function with a globally unique name, e.g. `dynamo::Table`, and an implementation of the Create, Update, Delete lifecycle operations.
|
|
104
|
-
|
|
105
|
-
E.g. below we show what a simple `dynamo::Table` provider might look like.
|
|
106
|
-
|
|
107
|
-
> [!NOTE]
|
|
108
|
-
> See [table.ts](./alchemy/src/components/aws/table.ts) for the full implementation.
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
interface TableInputs {
|
|
112
|
-
name: string;
|
|
113
|
-
//..
|
|
114
|
-
}
|
|
115
|
-
interface TableOutput {
|
|
116
|
-
tableArn: string;
|
|
117
|
-
}
|
|
118
|
-
class Table extends Resource(
|
|
119
|
-
"dynamo::Table",
|
|
120
|
-
async (ctx: Context<TableOutput>, inputs: TableInputs) => {
|
|
121
|
-
if (ctx.event === "create") {
|
|
122
|
-
// create logic
|
|
123
|
-
} else if (ctx.event === "update") {
|
|
124
|
-
// update logic
|
|
125
|
-
} else if (ctx.event === "delete") {
|
|
126
|
-
// delete logic
|
|
127
|
-
}
|
|
128
|
-
// ..
|
|
129
|
-
return output;
|
|
130
|
-
}
|
|
131
|
-
) {}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
> [!TIP]
|
|
135
|
-
> Use Cursor or an LLM like Claude/OpenAI to generate the implementation of your resource. I think you'll be pleasantly surprised at how well it works, especially if you provide the API reference docs in your context.
|
|
136
|
-
|
|
137
|
-
That's it! Now you can instantiate tables in your app.
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
const table = new Table("items", {
|
|
141
|
-
name: "items",
|
|
142
|
-
//..
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
table.tableArn; // Output<string>
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
You may have noticed the odd pattern of extending the result of a function call, `extends Resource(..)`. This is called the "mix-in" pattern and is optional. You are free to just use a `const` instead:
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
const Table = Resource("dynamo::Table", async (ctx, inputs) => {
|
|
152
|
-
//..
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const table = new Table("items", {
|
|
156
|
-
name: "items",
|
|
157
|
-
//..
|
|
158
|
-
});
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
> [!TIP]
|
|
162
|
-
> I recommend using a class so each resource has a type, e.g. `Table`, and a place to add helper/utility methods. Totally optional, though. Knock yourself out.
|
|
163
|
-
|
|
164
|
-
## Lazy Outputs
|
|
165
|
-
|
|
166
|
-
A Resource is evaluated lazily (not when you call `new`), so you can't immediately access the value of a property like `tableArn` or `functionArn`. Instead you reference properties using the `Output<T>` interface.
|
|
167
|
-
|
|
168
|
-
```ts
|
|
169
|
-
const table = new Table("my-table", { .. });
|
|
170
|
-
|
|
171
|
-
const tableArn = table.tableArn; // Output<string>
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Outputs can be chained explicitly and implicitly:
|
|
175
|
-
|
|
176
|
-
1. _Explicitly_ with `.apply`
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
const tableArn: Output<string> = table.tableArn.apply((arn: string) =>
|
|
180
|
-
// do something with the arn string value
|
|
181
|
-
arn.replace("table", "arn:aws:dynamodb:")
|
|
182
|
-
);
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
2. _Implicitly_ chained
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
const tableArn = table.attributeDefinitions[0].attributeName; // Output<string>
|
|
189
|
-
// equivalent to:
|
|
190
|
-
// table.apply(t => t.attributeDefinitions[0].attributeName);
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
> [!NOTE]
|
|
194
|
-
> Output is inspired by Pulumi, with a little extra added sugar.
|
|
195
|
-
|
|
196
|
-
## `apply` and `destroy`
|
|
197
|
-
|
|
198
|
-
Calling `alchemize` is optional. Any object in your graph (`Resource` or `Output<T>`) can be "applied" or "destroyed" individually and programmatically.
|
|
199
|
-
|
|
200
|
-
Say, you've got some two resources, a `Role` and a `Function`.
|
|
201
|
-
|
|
202
|
-
```ts
|
|
203
|
-
const role = new Role("my-role", {
|
|
204
|
-
name: "my-role",
|
|
205
|
-
//..
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const func = new Function("my-function", {
|
|
209
|
-
name: "my-function",
|
|
210
|
-
role: role.roleArn,
|
|
211
|
-
//..
|
|
212
|
-
});
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Each of these Resources is known as a "sub-graph".
|
|
216
|
-
|
|
217
|
-
In this case we have `Role` (a 1-node graph, `Role`), and `Function` (a 2-node graph, `Role → Function`).
|
|
218
|
-
|
|
219
|
-
Each sub-graph can be "applied" or "destroyed" individually using the `apply` and `destroy` functions:
|
|
220
|
-
|
|
221
|
-
```ts
|
|
222
|
-
import { apply, destroy } from "alchemy";
|
|
223
|
-
|
|
224
|
-
// will create Role and then Function (in that order)
|
|
225
|
-
const { functionArn } = await apply(func);
|
|
226
|
-
|
|
227
|
-
// you can destroy it right after if you want ☠️
|
|
228
|
-
await destroy(func); // will delete just the Function
|
|
229
|
-
|
|
230
|
-
// destroy deletes the resource and any downstream dependencies
|
|
231
|
-
// so, if you want to delete Role AND Function, you should call destroy(role)
|
|
232
|
-
await destroy(role); // will delete Role and then Function
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
## Destroying the app
|
|
236
|
-
|
|
237
|
-
To destroy the whole app (aka. the whole graph), you can call `alchemize` with the `mode: "destroy"` option. This will delete all resources in the specified or default stage.
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
await alchemize({ mode: "destroy", stage: <optional> });
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
> [!TIP]
|
|
244
|
-
> Alchemy is designed to have the minimum number of opinions as possible. This "embeddable" design is so that you can implement your own tools around Alchemy, e.g. a CLI or UI, instead of being stuck with a specific tool.
|
|
245
|
-
>
|
|
246
|
-
> ```ts
|
|
247
|
-
> await alchemize({
|
|
248
|
-
> // decide the mode/stage however you want, e.g. a CLI parser
|
|
249
|
-
> mode: process.argv[2] === "destroy" ? "destroy" : "up",
|
|
250
|
-
> stage: process.argv[3],
|
|
251
|
-
> });
|
|
252
|
-
> ```
|
|
253
|
-
|
|
254
|
-
## "Stage" and State
|
|
255
|
-
|
|
256
|
-
> [!NOTE]
|
|
257
|
-
> Stage is inspired by [SST](https://sst.dev)'s stage concept.
|
|
258
|
-
|
|
259
|
-
Alchemy supports a "stage" concept to help isolate different environments from each other. E.g. a `"user"` or `"dev"` or `"prod"` stage.
|
|
260
|
-
|
|
261
|
-
By default, the stage is assumed to be your user name (a sensible default for local development).
|
|
262
|
-
|
|
263
|
-
To override the stage, you have three options:
|
|
264
|
-
|
|
265
|
-
1. Pass the `stage` option to `alchemize`/`apply`/`destroy` (recommended)
|
|
266
|
-
|
|
267
|
-
```ts
|
|
268
|
-
// alchemize the entire app
|
|
269
|
-
await alchemize({ stage: "production" });
|
|
270
|
-
|
|
271
|
-
// apply a single resource
|
|
272
|
-
await apply(func, { stage: "production" });
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
2. Config in `./alchemy.ts` (up to you if you want to have a global config). See the [alchemy.ts section](#global-values-and-the-alchemy-ts-config-file) for more details.
|
|
276
|
-
|
|
277
|
-
```ts
|
|
278
|
-
export default {
|
|
279
|
-
defaultStage: "production",
|
|
280
|
-
};
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
3. Set the `ALCHEMY_STAGE` environment variable (not recommended, but available as an escape hatch)
|
|
284
|
-
|
|
285
|
-
```sh
|
|
286
|
-
ALCHEMY_STAGE=production bun ./my-app.ts
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
Each Resource "provider" can access the stage it's being deployed to via the `ctx.stage` property.
|
|
290
|
-
|
|
291
|
-
```ts
|
|
292
|
-
class Table extends Resource("dynamo::Table", async (ctx, inputs) => {
|
|
293
|
-
ctx.stage; // "production"
|
|
294
|
-
});
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
> [!CAUTION]
|
|
298
|
-
> It is up to you to ensure that the physical names of resources don't conflict - alchemy does not (yet) offer any help or opinions here. You must decide on physical names, but you're free to add name generation logic to your resources if you so desire.
|
|
299
|
-
>
|
|
300
|
-
> ```ts
|
|
301
|
-
> class Table extends Resource("dynamo::Table", async (ctx, inputs) => {
|
|
302
|
-
> const tableName = `${ctx.stage}-${inputs.tableName}`;
|
|
303
|
-
>
|
|
304
|
-
> // ..
|
|
305
|
-
> });
|
|
306
|
-
> ```
|
|
307
|
-
|
|
308
|
-
## Global values and the `alchemy.ts` config file.
|
|
309
|
-
|
|
310
|
-
Alchemy looks for a `${cwd}/alchemy.ts` file and imports it if it finds it. This can be useful for emulating SST's `sst.config.ts` file as a convention for global configuration.
|
|
311
|
-
|
|
312
|
-
It supports overriding the `defaultStage` (instead of defaulting to your username) and providing a custom `stateStore` (instead of writing to the local file system).
|
|
313
|
-
|
|
314
|
-
```ts
|
|
315
|
-
import type { Config } from "alchemy";
|
|
316
|
-
|
|
317
|
-
export default {
|
|
318
|
-
defaultStage: "dev",
|
|
319
|
-
stateStore: myCustomStateStore,
|
|
320
|
-
} satisfies Config;
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
> [!NOTE]
|
|
324
|
-
> See [global.ts](./alchemy/src/global.ts).
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { tool } from "ai";
|
|
2
|
-
import { exec } from "child_process";
|
|
3
|
-
import { promisify } from "util";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
|
|
6
|
-
const execAsync = promisify(exec);
|
|
7
|
-
|
|
8
|
-
export const installPackages = tool({
|
|
9
|
-
description: "Installs one or more packages using bun add",
|
|
10
|
-
parameters: z.object({
|
|
11
|
-
packages: z.array(z.string()).describe("Array of package names to install"),
|
|
12
|
-
isDev: z
|
|
13
|
-
.boolean()
|
|
14
|
-
.optional()
|
|
15
|
-
.describe("Whether to install as dev dependencies"),
|
|
16
|
-
cwd: z
|
|
17
|
-
.string()
|
|
18
|
-
.optional()
|
|
19
|
-
.describe("Working directory where to run the install command"),
|
|
20
|
-
}),
|
|
21
|
-
execute: async ({ packages, isDev = false, cwd = process.cwd() }) => {
|
|
22
|
-
if (packages.length === 0) {
|
|
23
|
-
throw new Error("No packages specified for installation");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const command = `bun add ${isDev ? "-D " : ""}${packages.join(" ")}`;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
console.log(`Running command: ${command}`);
|
|
30
|
-
const { stdout, stderr } = await execAsync(command, { cwd });
|
|
31
|
-
|
|
32
|
-
if (stderr) {
|
|
33
|
-
console.error("Installation stderr:", stderr);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
success: true,
|
|
38
|
-
command,
|
|
39
|
-
output: stdout,
|
|
40
|
-
packages,
|
|
41
|
-
type: isDev ? "dev" : "regular",
|
|
42
|
-
};
|
|
43
|
-
} catch (error: any) {
|
|
44
|
-
console.error("Installation error:", error);
|
|
45
|
-
throw new Error(
|
|
46
|
-
`Failed to install packages: ${error?.message || String(error)}`,
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
});
|