alchemy 0.1.18 → 0.2.1

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 (330) hide show
  1. package/README.md +506 -0
  2. package/lib/alchemy.d.ts +67 -0
  3. package/lib/alchemy.js +48 -0
  4. package/lib/apply.d.ts +3 -20
  5. package/lib/apply.js +95 -82
  6. package/lib/aws/account-id.d.ts +0 -1
  7. package/lib/aws/account-id.js +0 -1
  8. package/lib/aws/bucket.d.ts +4 -7
  9. package/lib/aws/bucket.js +8 -12
  10. package/lib/aws/function.d.ts +4 -7
  11. package/lib/aws/function.js +9 -30
  12. package/lib/aws/index.d.ts +1 -1
  13. package/lib/aws/index.js +1 -1
  14. package/lib/aws/oidc/github-oidc-provider.d.ts +2 -6
  15. package/lib/aws/oidc/github-oidc-provider.js +10 -15
  16. package/lib/aws/oidc/index.d.ts +0 -1
  17. package/lib/aws/oidc/index.js +0 -1
  18. package/lib/aws/oidc/oidc-provider.d.ts +4 -6
  19. package/lib/aws/oidc/oidc-provider.js +9 -11
  20. package/lib/aws/policy-attachment.d.ts +9 -0
  21. package/lib/aws/policy-attachment.js +20 -0
  22. package/lib/aws/policy.d.ts +4 -16
  23. package/lib/aws/policy.js +9 -40
  24. package/lib/aws/queue.d.ts +4 -34
  25. package/lib/aws/queue.js +10 -20
  26. package/lib/aws/role.d.ts +4 -7
  27. package/lib/aws/role.js +17 -26
  28. package/lib/aws/ses.d.ts +4 -10
  29. package/lib/aws/ses.js +118 -131
  30. package/lib/aws/table.d.ts +4 -7
  31. package/lib/aws/table.js +8 -11
  32. package/lib/cloudflare/api.d.ts +0 -1
  33. package/lib/cloudflare/api.js +0 -1
  34. package/lib/cloudflare/asset-manifest.d.ts +0 -1
  35. package/lib/cloudflare/asset-manifest.js +0 -1
  36. package/lib/cloudflare/auth.d.ts +0 -1
  37. package/lib/cloudflare/auth.js +0 -1
  38. package/lib/cloudflare/bindings.d.ts +1 -1
  39. package/lib/cloudflare/bindings.js +3 -2
  40. package/lib/cloudflare/bound.d.ts +0 -1
  41. package/lib/cloudflare/bound.js +0 -1
  42. package/lib/cloudflare/bucket.d.ts +7 -15
  43. package/lib/cloudflare/bucket.js +12 -19
  44. package/lib/cloudflare/durable-object-namespace.d.ts +0 -3
  45. package/lib/cloudflare/durable-object-namespace.js +0 -4
  46. package/lib/cloudflare/generate-asset-manifest.d.ts +0 -1
  47. package/lib/cloudflare/generate-asset-manifest.js +1 -2
  48. package/lib/cloudflare/index.d.ts +2 -1
  49. package/lib/cloudflare/index.js +2 -1
  50. package/lib/cloudflare/kv-namespace.d.ts +6 -9
  51. package/lib/cloudflare/kv-namespace.js +16 -20
  52. package/lib/cloudflare/state.d.ts +0 -1
  53. package/lib/cloudflare/state.js +0 -1
  54. package/lib/cloudflare/static-site-router.d.ts +0 -1
  55. package/lib/cloudflare/static-site-router.js +0 -4
  56. package/lib/cloudflare/static-site.d.ts +5 -11
  57. package/lib/cloudflare/static-site.js +25 -37
  58. package/lib/cloudflare/types.d.ts +0 -1
  59. package/lib/cloudflare/types.js +0 -1
  60. package/lib/cloudflare/upload-asset-manifest.d.ts +0 -1
  61. package/lib/cloudflare/upload-asset-manifest.js +0 -1
  62. package/lib/cloudflare/worker-metadata.d.ts +0 -1
  63. package/lib/cloudflare/worker-metadata.js +0 -1
  64. package/lib/cloudflare/worker-migration.d.ts +0 -1
  65. package/lib/cloudflare/worker-migration.js +0 -1
  66. package/lib/cloudflare/worker.d.ts +8 -14
  67. package/lib/cloudflare/worker.js +38 -50
  68. package/lib/cloudflare/wrangler.json.d.ts +243 -0
  69. package/lib/cloudflare/wrangler.json.js +35 -0
  70. package/lib/cloudflare/zone-settings.d.ts +301 -0
  71. package/lib/cloudflare/zone-settings.js +1 -0
  72. package/lib/cloudflare/zone.d.ts +196 -0
  73. package/lib/cloudflare/zone.js +175 -0
  74. package/lib/context.d.ts +58 -0
  75. package/lib/context.js +36 -0
  76. package/lib/destroy.d.ts +10 -13
  77. package/lib/destroy.js +104 -43
  78. package/lib/{esbuild.d.ts → esbuild/bundle.d.ts} +4 -18
  79. package/lib/{esbuild.js → esbuild/bundle.js} +10 -24
  80. package/lib/esbuild/index.d.ts +1 -0
  81. package/lib/esbuild/index.js +1 -0
  82. package/lib/fs/file.d.ts +13 -0
  83. package/lib/fs/file.js +20 -0
  84. package/lib/fs/folder.d.ts +10 -0
  85. package/lib/fs/folder.js +16 -0
  86. package/lib/fs/index.d.ts +2 -0
  87. package/lib/fs/index.js +2 -0
  88. package/lib/github/client.d.ts +0 -1
  89. package/lib/github/client.js +0 -1
  90. package/lib/github/index.d.ts +0 -1
  91. package/lib/github/index.js +0 -1
  92. package/lib/github/secret.d.ts +4 -6
  93. package/lib/github/secret.js +10 -12
  94. package/lib/index.d.ts +6 -10
  95. package/lib/index.js +5 -9
  96. package/lib/project/index.d.ts +1 -0
  97. package/lib/project/index.js +1 -0
  98. package/lib/project/vite.d.ts +39 -0
  99. package/lib/project/vite.js +68 -0
  100. package/lib/resource.d.ts +34 -62
  101. package/lib/resource.js +29 -270
  102. package/lib/scope.d.ts +37 -14
  103. package/lib/scope.js +97 -23
  104. package/lib/secret.d.ts +2 -7
  105. package/lib/secret.js +7 -20
  106. package/lib/state.d.ts +14 -10
  107. package/lib/state.js +20 -14
  108. package/lib/stripe/index.d.ts +0 -1
  109. package/lib/stripe/index.js +0 -1
  110. package/lib/stripe/price.d.ts +4 -5
  111. package/lib/stripe/price.js +10 -20
  112. package/lib/stripe/product.d.ts +4 -5
  113. package/lib/stripe/product.js +10 -20
  114. package/lib/stripe/webhook.d.ts +4 -6
  115. package/lib/stripe/webhook.js +12 -25
  116. package/lib/test/bun.d.ts +19 -0
  117. package/lib/test/bun.js +55 -0
  118. package/lib/{utils → util}/content-type.d.ts +0 -1
  119. package/lib/{utils → util}/content-type.js +0 -1
  120. package/lib/{encrypt.d.ts → util/encrypt.d.ts} +0 -1
  121. package/lib/{encrypt.js → util/encrypt.js} +0 -1
  122. package/lib/{error.d.ts → util/ignore.d.ts} +0 -1
  123. package/lib/{error.js → util/ignore.js} +0 -1
  124. package/lib/{utils → util}/retry.d.ts +0 -1
  125. package/lib/{utils → util}/retry.js +0 -1
  126. package/lib/util/rm.d.ts +1 -0
  127. package/lib/util/rm.js +11 -0
  128. package/lib/util/serde.d.ts +3 -0
  129. package/lib/{serde.js → util/serde.js} +19 -10
  130. package/lib/util/slugify.d.ts +1 -0
  131. package/lib/util/slugify.js +3 -0
  132. package/package.json +7 -5
  133. package/src/alchemy.ts +127 -0
  134. package/src/apply.ts +116 -115
  135. package/src/aws/bucket.ts +11 -14
  136. package/src/aws/function.ts +13 -33
  137. package/src/aws/index.ts +1 -0
  138. package/src/aws/oidc/github-oidc-provider.ts +14 -19
  139. package/src/aws/oidc/oidc-provider.ts +19 -12
  140. package/src/aws/policy-attachment.ts +51 -0
  141. package/src/aws/policy.ts +15 -60
  142. package/src/aws/queue.ts +17 -21
  143. package/src/aws/role.ts +25 -28
  144. package/src/aws/ses.ts +150 -164
  145. package/src/aws/table.ts +15 -15
  146. package/src/cloudflare/bindings.ts +8 -0
  147. package/src/cloudflare/bucket.ts +39 -47
  148. package/src/cloudflare/durable-object-namespace.ts +0 -10
  149. package/src/cloudflare/generate-asset-manifest.ts +1 -1
  150. package/src/cloudflare/index.ts +2 -0
  151. package/src/cloudflare/kv-namespace.ts +35 -33
  152. package/src/cloudflare/static-site-router.ts +0 -3
  153. package/src/cloudflare/static-site.ts +55 -71
  154. package/src/cloudflare/worker.ts +106 -112
  155. package/src/cloudflare/wrangler.json.ts +309 -0
  156. package/src/cloudflare/zone-settings.ts +368 -0
  157. package/src/cloudflare/zone.ts +491 -0
  158. package/src/context.ts +122 -0
  159. package/src/destroy.ts +124 -73
  160. package/src/{esbuild.ts → esbuild/bundle.ts} +16 -28
  161. package/src/esbuild/index.ts +1 -0
  162. package/src/fs/file.ts +39 -0
  163. package/src/fs/folder.ts +32 -0
  164. package/src/fs/index.ts +2 -0
  165. package/src/github/secret.ts +19 -16
  166. package/src/index.ts +7 -15
  167. package/src/project/index.ts +1 -0
  168. package/src/project/vite.ts +152 -0
  169. package/src/resource.ts +101 -462
  170. package/src/scope.ts +125 -36
  171. package/src/secret.ts +9 -32
  172. package/src/state.ts +38 -21
  173. package/src/stripe/price.ts +17 -22
  174. package/src/stripe/product.ts +17 -22
  175. package/src/stripe/webhook.ts +21 -27
  176. package/src/test/bun.ts +105 -0
  177. package/src/util/rm.ts +11 -0
  178. package/src/util/serde.ts +53 -0
  179. package/src/util/slugify.ts +3 -0
  180. package/lib/$.d.ts +0 -12
  181. package/lib/$.d.ts.map +0 -1
  182. package/lib/$.js +0 -42
  183. package/lib/$.js.map +0 -1
  184. package/lib/alchemize.d.ts +0 -47
  185. package/lib/alchemize.d.ts.map +0 -1
  186. package/lib/alchemize.js +0 -116
  187. package/lib/alchemize.js.map +0 -1
  188. package/lib/apply.d.ts.map +0 -1
  189. package/lib/apply.js.map +0 -1
  190. package/lib/aws/account-id.d.ts.map +0 -1
  191. package/lib/aws/account-id.js.map +0 -1
  192. package/lib/aws/bucket.d.ts.map +0 -1
  193. package/lib/aws/bucket.js.map +0 -1
  194. package/lib/aws/function.d.ts.map +0 -1
  195. package/lib/aws/function.js.map +0 -1
  196. package/lib/aws/index.d.ts.map +0 -1
  197. package/lib/aws/index.js.map +0 -1
  198. package/lib/aws/oidc/github-oidc-provider.d.ts.map +0 -1
  199. package/lib/aws/oidc/github-oidc-provider.js.map +0 -1
  200. package/lib/aws/oidc/index.d.ts.map +0 -1
  201. package/lib/aws/oidc/index.js.map +0 -1
  202. package/lib/aws/oidc/oidc-provider.d.ts.map +0 -1
  203. package/lib/aws/oidc/oidc-provider.js.map +0 -1
  204. package/lib/aws/policy.d.ts.map +0 -1
  205. package/lib/aws/policy.js.map +0 -1
  206. package/lib/aws/queue.d.ts.map +0 -1
  207. package/lib/aws/queue.js.map +0 -1
  208. package/lib/aws/role.d.ts.map +0 -1
  209. package/lib/aws/role.js.map +0 -1
  210. package/lib/aws/ses.d.ts.map +0 -1
  211. package/lib/aws/ses.js.map +0 -1
  212. package/lib/aws/table.d.ts.map +0 -1
  213. package/lib/aws/table.js.map +0 -1
  214. package/lib/cloudflare/api.d.ts.map +0 -1
  215. package/lib/cloudflare/api.js.map +0 -1
  216. package/lib/cloudflare/asset-manifest.d.ts.map +0 -1
  217. package/lib/cloudflare/asset-manifest.js.map +0 -1
  218. package/lib/cloudflare/auth.d.ts.map +0 -1
  219. package/lib/cloudflare/auth.js.map +0 -1
  220. package/lib/cloudflare/bindings.d.ts.map +0 -1
  221. package/lib/cloudflare/bindings.js.map +0 -1
  222. package/lib/cloudflare/bound.d.ts.map +0 -1
  223. package/lib/cloudflare/bound.js.map +0 -1
  224. package/lib/cloudflare/bucket.d.ts.map +0 -1
  225. package/lib/cloudflare/bucket.js.map +0 -1
  226. package/lib/cloudflare/durable-object-namespace.d.ts.map +0 -1
  227. package/lib/cloudflare/durable-object-namespace.js.map +0 -1
  228. package/lib/cloudflare/generate-asset-manifest.d.ts.map +0 -1
  229. package/lib/cloudflare/generate-asset-manifest.js.map +0 -1
  230. package/lib/cloudflare/index.d.ts.map +0 -1
  231. package/lib/cloudflare/index.js.map +0 -1
  232. package/lib/cloudflare/kv-namespace.d.ts.map +0 -1
  233. package/lib/cloudflare/kv-namespace.js.map +0 -1
  234. package/lib/cloudflare/state.d.ts.map +0 -1
  235. package/lib/cloudflare/state.js.map +0 -1
  236. package/lib/cloudflare/static-site-router.d.ts.map +0 -1
  237. package/lib/cloudflare/static-site-router.js.map +0 -1
  238. package/lib/cloudflare/static-site.d.ts.map +0 -1
  239. package/lib/cloudflare/static-site.js.map +0 -1
  240. package/lib/cloudflare/types.d.ts.map +0 -1
  241. package/lib/cloudflare/types.js.map +0 -1
  242. package/lib/cloudflare/upload-asset-manifest.d.ts.map +0 -1
  243. package/lib/cloudflare/upload-asset-manifest.js.map +0 -1
  244. package/lib/cloudflare/worker-metadata.d.ts.map +0 -1
  245. package/lib/cloudflare/worker-metadata.js.map +0 -1
  246. package/lib/cloudflare/worker-migration.d.ts.map +0 -1
  247. package/lib/cloudflare/worker-migration.js.map +0 -1
  248. package/lib/cloudflare/worker.d.ts.map +0 -1
  249. package/lib/cloudflare/worker.js.map +0 -1
  250. package/lib/destroy.d.ts.map +0 -1
  251. package/lib/destroy.js.map +0 -1
  252. package/lib/encrypt.d.ts.map +0 -1
  253. package/lib/encrypt.js.map +0 -1
  254. package/lib/error.d.ts.map +0 -1
  255. package/lib/error.js.map +0 -1
  256. package/lib/esbuild.d.ts.map +0 -1
  257. package/lib/esbuild.js.map +0 -1
  258. package/lib/fs.d.ts +0 -12
  259. package/lib/fs.d.ts.map +0 -1
  260. package/lib/fs.js +0 -41
  261. package/lib/fs.js.map +0 -1
  262. package/lib/github/client.d.ts.map +0 -1
  263. package/lib/github/client.js.map +0 -1
  264. package/lib/github/index.d.ts.map +0 -1
  265. package/lib/github/index.js.map +0 -1
  266. package/lib/github/secret.d.ts.map +0 -1
  267. package/lib/github/secret.js.map +0 -1
  268. package/lib/global.d.ts +0 -16
  269. package/lib/global.d.ts.map +0 -1
  270. package/lib/global.js +0 -42
  271. package/lib/global.js.map +0 -1
  272. package/lib/index.d.ts.map +0 -1
  273. package/lib/index.js.map +0 -1
  274. package/lib/input.d.ts +0 -7
  275. package/lib/input.d.ts.map +0 -1
  276. package/lib/input.js +0 -2
  277. package/lib/input.js.map +0 -1
  278. package/lib/main.d.ts +0 -3
  279. package/lib/main.d.ts.map +0 -1
  280. package/lib/main.js +0 -32
  281. package/lib/main.js.map +0 -1
  282. package/lib/output.d.ts +0 -25
  283. package/lib/output.d.ts.map +0 -1
  284. package/lib/output.js +0 -23
  285. package/lib/output.js.map +0 -1
  286. package/lib/print.d.ts +0 -7
  287. package/lib/print.d.ts.map +0 -1
  288. package/lib/print.js +0 -10
  289. package/lib/print.js.map +0 -1
  290. package/lib/resource.d.ts.map +0 -1
  291. package/lib/resource.js.map +0 -1
  292. package/lib/scope.d.ts.map +0 -1
  293. package/lib/scope.js.map +0 -1
  294. package/lib/secret.d.ts.map +0 -1
  295. package/lib/secret.js.map +0 -1
  296. package/lib/serde.d.ts +0 -3
  297. package/lib/serde.d.ts.map +0 -1
  298. package/lib/serde.js.map +0 -1
  299. package/lib/slug.d.ts +0 -2
  300. package/lib/slug.d.ts.map +0 -1
  301. package/lib/slug.js +0 -4
  302. package/lib/slug.js.map +0 -1
  303. package/lib/state.d.ts.map +0 -1
  304. package/lib/state.js.map +0 -1
  305. package/lib/stripe/index.d.ts.map +0 -1
  306. package/lib/stripe/index.js.map +0 -1
  307. package/lib/stripe/price.d.ts.map +0 -1
  308. package/lib/stripe/price.js.map +0 -1
  309. package/lib/stripe/product.d.ts.map +0 -1
  310. package/lib/stripe/product.js.map +0 -1
  311. package/lib/stripe/webhook.d.ts.map +0 -1
  312. package/lib/stripe/webhook.js.map +0 -1
  313. package/lib/utils/content-type.d.ts.map +0 -1
  314. package/lib/utils/content-type.js.map +0 -1
  315. package/lib/utils/retry.d.ts.map +0 -1
  316. package/lib/utils/retry.js.map +0 -1
  317. package/src/$.ts +0 -69
  318. package/src/alchemize.ts +0 -195
  319. package/src/fs.ts +0 -47
  320. package/src/global.ts +0 -63
  321. package/src/input.ts +0 -19
  322. package/src/main.ts +0 -34
  323. package/src/output.ts +0 -75
  324. package/src/print.ts +0 -18
  325. package/src/serde.ts +0 -46
  326. package/src/slug.ts +0 -3
  327. /package/src/{utils → util}/content-type.ts +0 -0
  328. /package/src/{encrypt.ts → util/encrypt.ts} +0 -0
  329. /package/src/{error.ts → util/ignore.ts} +0 -0
  330. /package/src/{utils → util}/retry.ts +0 -0
package/README.md ADDED
@@ -0,0 +1,506 @@
1
+ # Alchemy
2
+
3
+ Alchemy is an embeddable, zero-dependency, TypeScript-native Infrastructure-as-Code (IaC) library 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 code with zero dependencies.
6
+
7
+ Resources are simple memoized async functions that can run in any JavaScript runtime, including the browser, serverless functions and durable workflows.
8
+
9
+ ```ts
10
+ import alchemy from "alchemy";
11
+
12
+ await using _ = alchemy("cloudflare-worker");
13
+
14
+ export const worker = await Worker("worker", {
15
+ name: "my-worker",
16
+ entrypoint: "./src/index.ts",
17
+ bindings: {
18
+ COUNTER: counter,
19
+ STORAGE: storage, // Bind the R2 bucket to the worker
20
+ AUTH_STORE: authStore,
21
+ GITHUB_CLIENT_ID: secret(process.env.GITHUB_CLIENT_ID),
22
+ GITHUB_CLIENT_SECRET: secret(process.env.GITHUB_CLIENT_SECRET),
23
+ },
24
+ });
25
+ ```
26
+
27
+ # Features
28
+
29
+ - **JS-native** - no second language, toolchains, dependencies, processes, services, etc. to lug around.
30
+ - **Async-native** - resources are just async functions - no complex abstraction to learn.
31
+ - **ESM-native** - built exclusively on ESM, with a slight preference for modern JS runtimes like Bun.
32
+ - **Embeddable** - runs in any JavaScript/TypeScript environment, including the browser!
33
+ - **Extensible** - implement your own resources with a simple function.
34
+ - **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.
35
+ - **No dependencies** - the `alchemy` core package has 0 required dependencies.
36
+ - **No service** - state files are stored locally in your project and can be easily inspected, modified, checked into your repo, etc.
37
+ - **No strong opinions** - structure your codebase however you want, store state anywhere - we don't care!
38
+
39
+ # Examples
40
+
41
+ - CloudFlare ViteJS Website + API Backend with Durable Objects: [examples/cloudflare-vite/](./examples/cloudflare-vite/alchemy.config.ts)
42
+ - Deploy an AWS Lambda Function with a DynamoDB Table and IAM Role: [examples/aws-app/](./examples/aws-app/alchemy.config.ts)
43
+
44
+ # Getting Started
45
+
46
+ 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.
47
+
48
+ ```bash
49
+ # I recommend bun, but you can use any JavaScript runtime.
50
+ bun add alchemy
51
+ ```
52
+
53
+ Usually, you'll want to create an `alchemy.config.ts` script and then define your Resources.
54
+
55
+ > [!TIP]
56
+ > The `alchemy.config.ts` file is just a convention, not a requirement.
57
+
58
+ Your script should start by creating the Alchemy `app` (aka. "Root Scope", more on [Scopes](#resource-scope-tree) later):
59
+
60
+ ```ts
61
+ import alchemy from "alchemy";
62
+
63
+ // async disposables trigger finalization of the stack at the end of the script (after resources are declared)
64
+ await using app = alchemy("my-app", {
65
+ // namespace for stages
66
+ stage: process.env.STAGE ?? "dev",
67
+ // update or destroy the app
68
+ phase: process.argv.includes("--destroy") ? "destroy" : "up"
69
+ // password for encrypting/decrypting secrets stored in state
70
+ password: process.env.SECRET_PASSPHRASE,
71
+ // whether to log Create/Update/Delete events
72
+ quiet: process.argv.includes("--verbose") ? false : true,
73
+ });
74
+
75
+ // (otherwise, declare resources here AFTER the bootstrap)
76
+ ```
77
+
78
+ Now that our app is initialized, we can start creating Resources, e.g. an AWS IAM Role:
79
+
80
+ ```ts
81
+ import { Role } from "alchemy/aws";
82
+
83
+ export const role = await Role("my-role", {
84
+ roleName: "my-role",
85
+ assumeRolePolicy: {
86
+ Version: "2012-10-17",
87
+ Statement: [
88
+ {
89
+ Effect: "Allow",
90
+ // Or whatever principal you want
91
+ Principal: { Service: "lambda.amazonaws.com" },
92
+ Action: "sts:AssumeRole",
93
+ },
94
+ ],
95
+ },
96
+ });
97
+ ```
98
+
99
+ Notice how the `Role` is created by an `await Role(..)` function call.
100
+ In contrast to other IaC frameworks, Alchemy models Resources as memoized async functions that can be executed in any async environment - including the browser, serverless functions and durable workflows.
101
+
102
+ A nice benefit of async-await is how easy it becomes to access physical properties (otherwise known as "Stack Outputs").
103
+ You can just log the role name (crazy concept, right?):
104
+
105
+ ```ts
106
+ console.log({
107
+ roleName: role.roleName, // string
108
+ });
109
+ ```
110
+
111
+ ## Alchemy State
112
+
113
+ Now, when you run your script:
114
+
115
+ ```sh
116
+ bun ./my-app.ts
117
+ ```
118
+
119
+ You'll notice some files show up in `.alchemy/`:
120
+
121
+ ```sh
122
+ .alchemy/
123
+ my-app/
124
+ prod/
125
+ my-role.json
126
+ ```
127
+
128
+ These are called the "state files".
129
+
130
+ Go ahead, click on one and take a look - here's how my `my-role.json` looks:
131
+
132
+ ```jsonc
133
+ {
134
+ "provider": "iam::Role",
135
+ "data": {},
136
+ "deps": [],
137
+ "status": "updated",
138
+ "output": {
139
+ "roleName": "alchemy-api-lambda-role"
140
+ // ..
141
+ },
142
+ "props": {
143
+ "roleName": "alchemy-api-lambda-role",
144
+ "assumeRolePolicy": {
145
+ "Version": "2012-10-17"
146
+ // ..
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ Alchemy uses state to determine when to Create, Update, Delete or Skip Resources at runtime:
153
+
154
+ 1. If the resource doesn't have a prior state, it will be `created`
155
+ 1. If the inputs haven't changed since the last deployment, then it will be `skipped`,
156
+ 1. If the inputs have changed, it will be `updated`
157
+ 1. If the Resource no longer exists in the program (aka. is an orphan), then it will be `deleted`.
158
+
159
+ > [!TIP]
160
+ > Alchemy goes to great effort to be fully transparent. Each Resource's state is just a JSON file, nothing more. You can inspect it, modify it, commit it to your repo, store it in a database, etc.
161
+
162
+ ## "Custom" Resources
163
+
164
+ Adding new Resources is the whole point of Alchemy, and is therefore very simple.
165
+
166
+ 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.
167
+
168
+ Below is an illustrative example of the `dynamo::Table` provider.
169
+
170
+ > [!NOTE]
171
+ > See [table.ts](./alchemy/src/aws/table.ts) for the full implementation.
172
+
173
+ All Resources follow the same templated structure/convention:
174
+
175
+ 1. an interface (or type) for the Resource's (Input) Properties
176
+
177
+ ```ts
178
+ // a type to represent the Resource's input properties
179
+ export interface TableProps {
180
+ name: string;
181
+ //..
182
+ }
183
+ ```
184
+
185
+ 2. an interface (or type) for the Resource's (Output) Attributes
186
+
187
+ ```ts
188
+ // declare a type to represent the Resource's properties (aka. attributes)
189
+ export interface Table extends Resource<"dynamo::Table"> {
190
+ tableArn: string;
191
+ }
192
+ ```
193
+
194
+ 3. a special "Resource" function defining the Resource's globally unique name and resource lifecycle handler:
195
+
196
+ ```ts
197
+ export const Table = Resource(
198
+ "dynamo::Table",
199
+ async function (
200
+ // the resource context (phase, previous state, etc.) is made available as the bound `this` param
201
+ this: Context<TableOutput>,
202
+ // the resource's ID (unique within the current Scope)
203
+ id: string,
204
+ // the resource input properties
205
+ props: TableInputs
206
+ ): Promise<Table> {
207
+ // this function implement the CRUD resource lifecycle for an instance of this Resource
208
+
209
+ if (this.phase === "create") {
210
+ // (create logic)
211
+ } else if (this.phase === "update") {
212
+ // (update logic)
213
+ } else if (this.phase === "delete") {
214
+ // (delete logic)
215
+
216
+ // terminate the delete process early
217
+ return this.destroy();
218
+ }
219
+ // return the created/updated resource properties
220
+ return this(props);
221
+ }
222
+ );
223
+ ```
224
+
225
+ <details>
226
+ <summary>Nitty gritty details on this pattern's design and oddities</summary>
227
+ I call this pattern the "pseudo class", designed to model a Resource with a CRUD lifecycle implemented with memoized async functions.
228
+
229
+ The `this` parameter in this "pseudo class" serves many purposes:
230
+
231
+ 1. contains the resource' `phase` (`create`, `update`, `delete`)
232
+ 2. contains the resource's current state and previous props (`this.props`, `this.fqn`, `this.stage`, `this.scope`)
233
+ 3. provides a handle to destroy the resource (`this.destroy`)
234
+ 4. provides a factory for constructing the resource object (`this({..}`) - you can think of this as emulating `super({..})`
235
+ </details>
236
+
237
+ > [!TIP]
238
+ > 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.
239
+
240
+ That's it! Now you can instantiate DynamoDB Tables:
241
+
242
+ ```ts
243
+ const table = await Table("items", {
244
+ name: "items",
245
+ //..
246
+ });
247
+
248
+ table.tableArn; // string
249
+ ```
250
+
251
+ ## Secrets
252
+
253
+ Recall that the `alchemy` function accepts a `password` property:
254
+
255
+ ```ts
256
+ await using app = alchemy("my-app", {
257
+ // password for encrypting/decrypting secrets stored in state
258
+ password: process.env.SECRET_PASSPHRASE,
259
+ });
260
+ ```
261
+
262
+ This password is used to encrypt and decrypt secret data within an Alchemy state:
263
+
264
+ ```ts
265
+ const OPENAI_API_KEY = alchemy.secret(process.env.OPENAI_API_KEY);
266
+ ```
267
+
268
+ Now, I can pass this secret to a Resource safely:
269
+
270
+ ```ts
271
+ await Worker("my-func", {
272
+ bindings: {
273
+ OPENAI_API_KEY,
274
+ },
275
+ });
276
+ ```
277
+
278
+ In our `.alchemy/` state, the property will be encrypted instead of plain text:
279
+
280
+ ```json
281
+ {
282
+ "props": {
283
+ "bindings": {
284
+ "OPENAI_API_KEY": {
285
+ "@secret": "Tgz3e/WAscu4U1oanm5S4YXH..."
286
+ }
287
+ }
288
+ }
289
+ }
290
+ ```
291
+
292
+ ## Resource Scope Tree
293
+
294
+ Alchemy manages resources with a named tree of `Scope`s, similar to a file system. Each Scope has a name and contains named Resources and other (named) Scopes.
295
+
296
+ ### Application Scope
297
+
298
+ The `alchemy` bootstrap (in your `alchemy.config.ts`) creates and binds to the Alchemy Application Scope (aka. "Root Scope"):
299
+
300
+ ```ts
301
+ await using app = alchemy("my-app", {
302
+ stage: "prod",
303
+ // ..
304
+ });
305
+ ```
306
+
307
+ To get a better understanding, notice how it has 1:1 correspondence with the `.alchemy/` state files:
308
+
309
+ ```sh
310
+ .alchemy/
311
+ my-app/ # app scope
312
+ prod/ # stage scope
313
+ my-role.json # resource instance
314
+ ```
315
+
316
+ ### Stage Scope
317
+
318
+ When you create an app, you can also specify a `stage`.
319
+
320
+ Stage is just an opinionated Scope placed under the root useful as a convention for isolating "stages" such as `prod`, `dev`, `$USER`.
321
+
322
+ ```ts
323
+ await using app = alchemy("my-app", {
324
+ // scope: my-app/prod
325
+ stage: "prod",
326
+ });
327
+ ```
328
+
329
+ ### Instance Scope
330
+
331
+ Each Resource instance has its own scope to isolate Resources created in its Lifecycle Handler:
332
+
333
+ ```ts
334
+ export const MyResource = Resource(
335
+ "my::Resource",
336
+ async function (this, id, props) {
337
+ if (this.phase === "delete") {
338
+ return this.destroy();
339
+ }
340
+ await Role("my-role");
341
+ await Worker("my-worker");
342
+ }
343
+ );
344
+ ```
345
+
346
+ When you create an instance of `MyResource`, its nested Resources will be scoped to the Resource Instance:
347
+
348
+ ```ts
349
+ await MyResource("instance");
350
+ ```
351
+
352
+ ```sh
353
+ .alchemy/
354
+ my-app/ # app
355
+ prod/ # stage
356
+ instance.json # instance
357
+ instance/ # instance scope
358
+ my-role.json # instance
359
+ my-worker.json # instance
360
+ ```
361
+
362
+ ### Nested Scopes
363
+
364
+ Nested Scopes are stored within their parent Scope's state folder:
365
+
366
+ ```sh
367
+ .alchemy/
368
+ my-app/ # app
369
+ prod/ # stage
370
+ nested/ # scope
371
+ my-worker.json # instance
372
+ ```
373
+
374
+ > [!TIP]
375
+ > Scopes can be nested arbitrarily.
376
+
377
+ ### `alchemy.scope`
378
+
379
+ You can create and "enter" a Nested Scope synchronously in a function. This will create and set the current async context's Scope (using AsyncLocalStorage):
380
+
381
+ ```ts
382
+ await using scope = alchemy.scope("nested");
383
+
384
+ // resources created AFTER are placed in the "nested' Scope
385
+ await Worker("my-worker");
386
+ ```
387
+
388
+ ### `alchemy.run`
389
+
390
+ You can also create nested scopes using the `alchemy.run` function and a closure:
391
+
392
+ ```ts
393
+ await alchemy.run("nested", async () => {
394
+ // resources created in here are isolated to the "nested' Scope
395
+ await Worker("my-worker");
396
+ });
397
+
398
+ // resources out here are placed in the "parent" SCope
399
+ await Worker("my-worker");
400
+ ```
401
+
402
+ ### Get the current Scope
403
+
404
+ The current Scope is stored in `AsyncLocalStorage` and accessible when needed:
405
+
406
+ ```ts
407
+ Scope.current; // will throw if not in a scope
408
+ Scope.get(); // Scope | undefined
409
+ await alchemy.run("nested", async (scope) => {
410
+ // scope is passed in as an argument
411
+ });
412
+ // create a Scope and bind to the current async context
413
+ using scope = alchemy.scope("nested");
414
+ ```
415
+
416
+ ## `destroy`
417
+
418
+ `Scope`, `Resource` and `ResourcePromise` can be "destroyed" individually and programmatically.
419
+
420
+ ### Destroy a Resource
421
+
422
+ Say, you've got some two resources, a `Role` and a `Function`.
423
+
424
+ ```ts
425
+ const role = await Role("my-role", {
426
+ name: "my-role",
427
+ //..
428
+ });
429
+
430
+ const func = await Function("my-function", {
431
+ name: "my-function",
432
+ role: role.roleArn,
433
+ //..
434
+ });
435
+ ```
436
+
437
+ Each of these Resources is known as a "sub-graph".
438
+
439
+ In this case we have `Role` (a 1-node graph, `Role`), and `Function` (a 2-node graph, `Role → Function`).
440
+
441
+ Each sub-graph can be "applied" or "destroyed" individually using the `apply` and `destroy` functions:
442
+
443
+ ```ts
444
+ import { destroy } from "alchemy";
445
+
446
+ await destroy(func); // will delete just the Function
447
+
448
+ // destroy deletes the resource and any downstream dependencies
449
+ // so, if you want to delete Role AND Function, you should call destroy(role)
450
+ await destroy(role); // will delete Role and then Function
451
+ ```
452
+
453
+ ### Destroy a Scope
454
+
455
+ You can destroy all Resources in a Scope with a single `destroy` call:
456
+
457
+ ```ts
458
+ const scope = alchemy.scope("scope");
459
+ try {
460
+ await Role("role");
461
+ await Worker("worker");
462
+ } finally {
463
+ // destroy them all!
464
+ await destroy(scope);
465
+ }
466
+ ```
467
+
468
+ ### Destroy the App
469
+
470
+ To destroy the whole app (aka. the whole graph), you can call `alchemy` with the `phase: "destroy"` option. This will delete all resources in the specified or default stage.
471
+
472
+ ```ts
473
+ await using _ = alchemy({
474
+ phase: "destroy",
475
+ // ..
476
+ });
477
+ ```
478
+
479
+ > [!TIP]
480
+ > 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.
481
+ >
482
+ > ```ts
483
+ > await using _ = alchemy({
484
+ > // decide the mode/stage however you want, e.g. a CLI parser
485
+ > phase: process.argv[2] === "destroy" ? "destroy" : "up",
486
+ > stage: process.argv[3],
487
+ > });
488
+ > ```
489
+
490
+ ## Test Resources
491
+
492
+ > [!NOTE]
493
+ > TODO
494
+
495
+ ## Physical Names
496
+
497
+ > [!CAUTION]
498
+ > 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.
499
+ >
500
+ > ```ts
501
+ > const Table = Resource("dynamo::Table", async function (this, inputs) {
502
+ > const tableName = `${this.stage}-${inputs.tableName}`;
503
+ >
504
+ > // ..
505
+ > });
506
+ > ```
@@ -0,0 +1,67 @@
1
+ import { destroy } from "./destroy";
2
+ import { Scope } from "./scope";
3
+ import { secret } from "./secret";
4
+ import type { StateStoreType } from "./state";
5
+ export type alchemy = Alchemy;
6
+ export declare const alchemy: Alchemy;
7
+ export interface Alchemy {
8
+ scope: typeof scope;
9
+ run: typeof run;
10
+ destroy: typeof destroy;
11
+ secret: typeof secret;
12
+ (...parameters: Parameters<typeof scope>): ReturnType<typeof scope>;
13
+ }
14
+ export interface AlchemyOptions {
15
+ /**
16
+ * The name of the application.
17
+ */
18
+ appName?: string;
19
+ /**
20
+ * Determines whether the resources will be created/updated or deleted.
21
+ *
22
+ * @default "up"
23
+ */
24
+ phase?: "up" | "destroy";
25
+ /**
26
+ * Name to scope the resource state under (e.g. `.alchemy/{stage}/..`).
27
+ *
28
+ * @default - your POSIX username
29
+ */
30
+ stage?: string;
31
+ /**
32
+ * If true, will not prune resources that were dropped from the root stack.
33
+ *
34
+ * @default true
35
+ */
36
+ destroyOrphans?: boolean;
37
+ /**
38
+ * A custom state store to use instead of the default file system store.
39
+ */
40
+ stateStore?: StateStoreType;
41
+ /**
42
+ * A custom scope to use as a parent.
43
+ */
44
+ parent?: Scope;
45
+ /**
46
+ * If true, will not print any Create/Update/Delete messages.
47
+ *
48
+ * @default false
49
+ */
50
+ quiet?: boolean;
51
+ /**
52
+ * A passphrase to use to encrypt/decrypt secrets.
53
+ */
54
+ password?: string;
55
+ }
56
+ /**
57
+ * Enter a new scope synchronously.
58
+ * @param options
59
+ * @returns
60
+ */
61
+ declare function scope(id: string | undefined, options?: AlchemyOptions): Scope;
62
+ declare function run<T>(...args: [id: string, fn: (this: Scope, scope: Scope) => Promise<T>] | [
63
+ id: string,
64
+ options: AlchemyOptions,
65
+ fn: (this: Scope, scope: Scope) => Promise<T>
66
+ ]): Promise<T>;
67
+ export {};
package/lib/alchemy.js ADDED
@@ -0,0 +1,48 @@
1
+ import { DestroyedSignal, destroy } from "./destroy";
2
+ import { Scope } from "./scope";
3
+ import { secret } from "./secret";
4
+ // TODO: support browser
5
+ const DEFAULT_STAGE = process.env.ALCHEMY_STAGE ?? process.env.USER ?? "dev";
6
+ function _alchemy(appName, options) {
7
+ return scope(undefined, {
8
+ ...options,
9
+ appName,
10
+ stage: options.stage,
11
+ });
12
+ }
13
+ _alchemy.destroy = destroy;
14
+ _alchemy.run = run;
15
+ _alchemy.scope = scope;
16
+ _alchemy.secret = secret;
17
+ export const alchemy = _alchemy;
18
+ /**
19
+ * Enter a new scope synchronously.
20
+ * @param options
21
+ * @returns
22
+ */
23
+ function scope(id, options) {
24
+ const scope = new Scope({
25
+ ...options,
26
+ appName: options?.appName,
27
+ stage: options?.stage ?? DEFAULT_STAGE,
28
+ scopeName: id,
29
+ parent: options?.parent ?? Scope.get(),
30
+ });
31
+ scope.enter();
32
+ return scope;
33
+ }
34
+ async function run(...args) {
35
+ const [id, options, fn] = typeof args[1] === "function"
36
+ ? [args[0], undefined, args[1]]
37
+ : args;
38
+ await using scope = alchemy.scope(id, options);
39
+ try {
40
+ return await fn.bind(scope)(scope);
41
+ }
42
+ catch (error) {
43
+ if (!(error instanceof DestroyedSignal)) {
44
+ scope.fail();
45
+ }
46
+ throw error;
47
+ }
48
+ }
package/lib/apply.d.ts CHANGED
@@ -1,23 +1,6 @@
1
- import { type Output, type Resolved } from "./output";
2
- import { type Scope as IScope } from "./scope";
3
- import type { StateStore } from "./state";
1
+ import { type PendingResource, type Resource, type ResourceProps } from "./resource";
4
2
  export interface ApplyOptions {
5
- stage: string;
6
- scope: IScope;
7
- stateStore: StateStore;
8
3
  quiet?: boolean;
4
+ alwaysUpdate?: boolean;
9
5
  }
10
- /**
11
- * Apply a sub-graph to produce a resource.
12
- * @param output A sub-graph that produces a resource.
13
- * @returns The resource properties.
14
- */
15
- export declare function apply<T>(output: T, options?: Partial<ApplyOptions>): Promise<Resolved<T>>;
16
- declare class Evaluated<T> {
17
- readonly value: T;
18
- readonly deps: string[];
19
- constructor(value: T, deps?: string[]);
20
- }
21
- export declare function evaluate<T>(output: T | Output<T>, options: ApplyOptions): Promise<Evaluated<T>>;
22
- export {};
23
- //# sourceMappingURL=apply.d.ts.map
6
+ export declare function apply<Out extends Resource>(resource: PendingResource<Out>, props: ResourceProps, options?: ApplyOptions): Promise<Awaited<Out>>;