alchemy 0.3.1 → 0.4.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 (113) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/document.d.ts +12 -1
  3. package/lib/ai/document.js +10 -1
  4. package/lib/ai/index.d.ts +0 -2
  5. package/lib/ai/index.js +0 -2
  6. package/lib/alchemy.d.ts +5 -0
  7. package/lib/alchemy.js +44 -1
  8. package/lib/apply.js +17 -10
  9. package/lib/aws/account-id.d.ts +4 -1
  10. package/lib/aws/account-id.js +1 -1
  11. package/lib/aws/credentials.d.ts +5 -0
  12. package/lib/aws/credentials.js +0 -0
  13. package/lib/aws/oidc/oidc-provider.js +2 -2
  14. package/lib/aws/role.js +35 -23
  15. package/lib/cloudflare/account-api-token.d.ts +168 -0
  16. package/lib/cloudflare/account-api-token.js +179 -0
  17. package/lib/cloudflare/account-id.d.ts +1 -0
  18. package/lib/cloudflare/account-id.js +0 -0
  19. package/lib/cloudflare/api.d.ts +38 -1
  20. package/lib/cloudflare/api.js +55 -4
  21. package/lib/cloudflare/assets.d.ts +74 -0
  22. package/lib/cloudflare/assets.js +73 -0
  23. package/lib/cloudflare/bindings.d.ts +6 -1
  24. package/lib/cloudflare/bindings.js +6 -0
  25. package/lib/cloudflare/bucket.d.ts +118 -3
  26. package/lib/cloudflare/bucket.js +269 -89
  27. package/lib/cloudflare/custom-domain.d.ts +75 -0
  28. package/lib/cloudflare/custom-domain.js +154 -0
  29. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  30. package/lib/cloudflare/index.d.ts +6 -1
  31. package/lib/cloudflare/index.js +6 -1
  32. package/lib/cloudflare/permission-groups.d.ts +78 -0
  33. package/lib/cloudflare/permission-groups.js +48 -0
  34. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  35. package/lib/cloudflare/r2-rest-state-store.js +55 -24
  36. package/lib/cloudflare/static-site.d.ts +17 -18
  37. package/lib/cloudflare/static-site.js +19 -10
  38. package/lib/cloudflare/worker.d.ts +14 -0
  39. package/lib/cloudflare/worker.js +150 -3
  40. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  41. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  42. package/lib/fs/file-system-state-store.js +1 -1
  43. package/lib/fs/file.d.ts +18 -0
  44. package/lib/fs/file.js +6 -0
  45. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +10 -4
  46. package/lib/internal/docs/providers.js +196 -0
  47. package/lib/secret.d.ts +3 -0
  48. package/lib/secret.js +13 -0
  49. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  50. package/lib/{util/serde.js → serde.js} +12 -4
  51. package/lib/test/bun.js +3 -2
  52. package/lib/util/sha256.d.ts +1 -0
  53. package/lib/util/sha256.js +4 -0
  54. package/lib/web/vitepress/config.d.ts +2 -1
  55. package/lib/web/vitepress/config.js +1 -0
  56. package/lib/web/vitepress/index.d.ts +1 -0
  57. package/lib/web/vitepress/index.js +1 -0
  58. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  59. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  60. package/lib/web/vitepress/vitepress.js +3 -2
  61. package/package.json +8 -4
  62. package/src/ai/document.ts +26 -2
  63. package/src/ai/index.ts +0 -2
  64. package/src/alchemy.ts +56 -2
  65. package/src/apply.ts +21 -20
  66. package/src/aws/account-id.ts +6 -2
  67. package/src/aws/credentials.ts +6 -0
  68. package/src/aws/oidc/oidc-provider.ts +17 -17
  69. package/src/aws/role.ts +68 -54
  70. package/src/cloudflare/account-api-token.ts +365 -0
  71. package/src/cloudflare/account-id.ts +0 -0
  72. package/src/cloudflare/api.ts +88 -17
  73. package/src/cloudflare/assets.ts +155 -0
  74. package/src/cloudflare/bindings.ts +11 -2
  75. package/src/cloudflare/bucket.ts +493 -133
  76. package/src/cloudflare/custom-domain.ts +318 -0
  77. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  78. package/src/cloudflare/index.ts +6 -1
  79. package/src/cloudflare/permission-groups.ts +137 -0
  80. package/src/cloudflare/r2-rest-state-store.ts +87 -42
  81. package/src/cloudflare/static-site.ts +20 -32
  82. package/src/cloudflare/worker.ts +253 -5
  83. package/src/dns/import-dns.ts +4 -4
  84. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  85. package/src/fs/file-system-state-store.ts +5 -5
  86. package/src/fs/file.ts +29 -0
  87. package/src/internal/docs/providers.ts +281 -0
  88. package/src/secret.ts +16 -0
  89. package/src/{util/serde.ts → serde.ts} +16 -10
  90. package/src/test/bun.ts +8 -7
  91. package/src/util/sha256.ts +5 -0
  92. package/src/web/vitepress/config.ts +3 -1
  93. package/src/web/vitepress/index.ts +1 -0
  94. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  95. package/src/web/vitepress/vitepress.ts +3 -2
  96. package/lib/ai/approve.d.ts +0 -99
  97. package/lib/ai/approve.js +0 -76
  98. package/lib/ai/review.d.ts +0 -122
  99. package/lib/ai/review.js +0 -101
  100. package/lib/internal/getting-started.d.ts +0 -21
  101. package/lib/internal/getting-started.js +0 -87
  102. package/lib/internal/index.d.ts +0 -3
  103. package/lib/internal/index.js +0 -3
  104. package/lib/internal/providers.js +0 -172
  105. package/lib/internal/tutorial.d.ts +0 -104
  106. package/lib/internal/tutorial.js +0 -251
  107. package/src/ai/approve.ts +0 -163
  108. package/src/ai/review.ts +0 -213
  109. package/src/internal/getting-started.ts +0 -115
  110. package/src/internal/index.ts +0 -3
  111. package/src/internal/providers.ts +0 -241
  112. package/src/internal/tutorial.ts +0 -392
  113. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
@@ -7,17 +7,14 @@ import sodium from "libsodium-wrappers";
7
7
  * @param key - The encryption key
8
8
  * @returns The base64-encoded encrypted value with nonce
9
9
  */
10
- export async function encryptWithKey(
11
- value: string,
12
- key: string,
13
- ): Promise<string> {
10
+ export async function encrypt(value: string, key: string): Promise<string> {
14
11
  // Initialize libsodium
15
12
  await sodium.ready;
16
13
 
17
14
  // Derive a key from the passphrase
18
15
  const cryptoKey = sodium.crypto_generichash(
19
16
  sodium.crypto_secretbox_KEYBYTES,
20
- sodium.from_string(key),
17
+ sodium.from_string(key)
21
18
  );
22
19
 
23
20
  // Generate a random nonce
@@ -27,7 +24,7 @@ export async function encryptWithKey(
27
24
  const encryptedBin = sodium.crypto_secretbox_easy(
28
25
  sodium.from_string(value),
29
26
  nonce,
30
- cryptoKey,
27
+ cryptoKey
31
28
  );
32
29
 
33
30
  // Combine nonce and ciphertext, then encode to base64
@@ -47,7 +44,7 @@ export async function encryptWithKey(
47
44
  */
48
45
  export async function decryptWithKey(
49
46
  encryptedValue: string,
50
- key: string,
47
+ key: string
51
48
  ): Promise<string> {
52
49
  // Initialize libsodium
53
50
  await sodium.ready;
@@ -55,13 +52,13 @@ export async function decryptWithKey(
55
52
  // Derive a key from the passphrase
56
53
  const cryptoKey = sodium.crypto_generichash(
57
54
  sodium.crypto_secretbox_KEYBYTES,
58
- sodium.from_string(key),
55
+ sodium.from_string(key)
59
56
  );
60
57
 
61
58
  // Decode the base64 combined value
62
59
  const combined = sodium.from_base64(
63
60
  encryptedValue,
64
- sodium.base64_variants.ORIGINAL,
61
+ sodium.base64_variants.ORIGINAL
65
62
  );
66
63
 
67
64
  // Extract nonce and ciphertext
@@ -72,7 +69,7 @@ export async function decryptWithKey(
72
69
  const decryptedBin = sodium.crypto_secretbox_open_easy(
73
70
  ciphertext,
74
71
  nonce,
75
- cryptoKey,
72
+ cryptoKey
76
73
  );
77
74
 
78
75
  return sodium.to_string(decryptedBin);
@@ -1,9 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { Scope } from "../scope";
4
+ import { deserialize, serialize } from "../serde";
4
5
  import type { State, StateStore } from "../state";
5
6
  import { ignore } from "../util/ignore";
6
- import { deserialize, serialize } from "../util/serde";
7
7
 
8
8
  const stateRootDir = path.join(process.cwd(), ".alchemy");
9
9
 
@@ -48,7 +48,7 @@ export class FileSystemStateStore implements StateStore {
48
48
  const content = await fs.readFile(await this.getPath(key), "utf8");
49
49
  const state = (await deserialize(
50
50
  this.scope,
51
- JSON.parse(content),
51
+ JSON.parse(content)
52
52
  )) as State;
53
53
  if (state.output === undefined) {
54
54
  state.output = {} as any;
@@ -66,7 +66,7 @@ export class FileSystemStateStore implements StateStore {
66
66
  async set(key: string, value: State): Promise<void> {
67
67
  return fs.writeFile(
68
68
  await this.getPath(key),
69
- JSON.stringify(await serialize(this.scope, value), null, 2),
69
+ JSON.stringify(await serialize(this.scope, value), null, 2)
70
70
  );
71
71
  }
72
72
 
@@ -88,9 +88,9 @@ export class FileSystemStateStore implements StateStore {
88
88
  return [] as const;
89
89
  }
90
90
  return [[id, s]] as const;
91
- }),
91
+ })
92
92
  )
93
- ).flat(),
93
+ ).flat()
94
94
  );
95
95
  }
96
96
 
package/src/fs/file.ts CHANGED
@@ -52,6 +52,28 @@ declare module "../alchemy" {
52
52
  */
53
53
  files(paths: string[]): Promise<FileCollection>;
54
54
  files(path: string, ...paths: string[]): Promise<FileCollection>;
55
+
56
+ /**
57
+ * Gets all of the files in a directory.
58
+ * @param path Path to the directory
59
+ * @param props Optional properties
60
+ * @returns Promise resolving to a FileCollection
61
+ *
62
+ * @example
63
+ * // Get all files in a directory
64
+ * const files = await alchemy.folder("./docs");
65
+ *
66
+ */
67
+ folder(
68
+ path: string,
69
+ props?: {
70
+ /**
71
+ * Whether to recursively get all files in the directory
72
+ * @default false
73
+ */
74
+ recursive?: boolean;
75
+ }
76
+ ): Promise<FileCollection>;
55
77
  }
56
78
  }
57
79
 
@@ -78,6 +100,13 @@ alchemy.files = async (
78
100
  };
79
101
  };
80
102
 
103
+ alchemy.folder = async (dir: string, props?: { recursive?: boolean }) => {
104
+ const files = await fs.promises.readdir(dir, {
105
+ recursive: props?.recursive ?? false,
106
+ });
107
+ return alchemy.files(files.map((file) => path.join(dir, file)));
108
+ };
109
+
81
110
  /**
82
111
  * Base file resource type
83
112
  */
@@ -0,0 +1,281 @@
1
+ import { type } from "arktype";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { Data, Document } from "../../ai";
5
+ import { alchemy } from "../../alchemy";
6
+ import { Folder } from "../../fs";
7
+
8
+ export interface DocsProps {
9
+ /**
10
+ * The output directory for the docs.
11
+ */
12
+ outDir: string | Folder;
13
+
14
+ /**
15
+ * The source directory for the docs.
16
+ */
17
+ srcDir: string;
18
+
19
+ /**
20
+ * Whether to filter the docs.
21
+ * If true, include all docs.
22
+ * If false, include none.
23
+ * If a number, include that many providers.
24
+ *
25
+ * @default true (all docs)
26
+ */
27
+ filter?: boolean | number;
28
+
29
+ /**
30
+ * Whether to run in parallel.
31
+ *
32
+ * @default true
33
+ */
34
+ parallel?: boolean;
35
+ }
36
+
37
+ export type Providers = {
38
+ dir: string;
39
+ provider: string;
40
+ documents: Document[];
41
+ }[];
42
+
43
+ export async function Providers({
44
+ srcDir,
45
+ outDir,
46
+ filter,
47
+ parallel = true,
48
+ }: DocsProps): Promise<Providers> {
49
+ outDir = typeof outDir === "string" ? outDir : outDir.path;
50
+
51
+ const exclude = [
52
+ "util",
53
+ "test",
54
+ "vitepress",
55
+ "vite",
56
+ "shadcn",
57
+ "internal",
58
+ "web",
59
+ ];
60
+
61
+ // Get all folders in the alchemy/src directory
62
+ let providers = (
63
+ await fs.readdir(srcDir, {
64
+ withFileTypes: true,
65
+ })
66
+ )
67
+ .filter((dirent) => dirent.isDirectory() && !exclude.includes(dirent.name))
68
+ .map((dirent) => path.join(dirent.parentPath, dirent.name));
69
+
70
+ // For each provider, list all files
71
+ if (filter === false) {
72
+ return [];
73
+ } else if (typeof filter === "number") {
74
+ providers = providers.slice(0, filter);
75
+ }
76
+
77
+ if (parallel) {
78
+ return await Promise.all(
79
+ providers.map((provider) =>
80
+ generateProviderDocs({ provider, outDir, parallel })
81
+ )
82
+ );
83
+ } else {
84
+ const generatedProviders = [];
85
+ for (const provider of providers) {
86
+ generatedProviders.push(
87
+ await generateProviderDocs({ provider, outDir, parallel })
88
+ );
89
+ }
90
+ return generatedProviders;
91
+ }
92
+ }
93
+
94
+ async function generateProviderDocs({
95
+ provider,
96
+ outDir,
97
+ parallel,
98
+ }: {
99
+ provider: string;
100
+ outDir: string;
101
+ parallel: boolean;
102
+ }) {
103
+ const providerName = path.basename(provider);
104
+ const files = (
105
+ await fs.readdir(path.resolve(provider), {
106
+ withFileTypes: true,
107
+ })
108
+ )
109
+ .filter((dirent) => dirent.isFile())
110
+ .map((dirent) =>
111
+ path.relative(process.cwd(), path.resolve(provider, dirent.name))
112
+ )
113
+ .filter((file) => file.endsWith(".ts") && !file.endsWith("index.ts"));
114
+
115
+ type Group = (typeof groups)[number];
116
+
117
+ const {
118
+ object: { groups },
119
+ } = await Data(`docs/${providerName}`, {
120
+ model: {
121
+ id: "o3-mini",
122
+ provider: "openai",
123
+ options: {
124
+ reasoningEffort: "high",
125
+ },
126
+ },
127
+ temperature: 0.1,
128
+ schema: type({
129
+ groups: type({
130
+ identifier: type("string").describe(
131
+ "The identifier of the file's primary exported Resource/Function/Type, e.g. Bucket or StaticSite, AstroFile, TypeScriptFile"
132
+ ),
133
+ filename: type("string").describe(
134
+ "The filename of the Resource's Document, e.g. bucket.md or static-site.md"
135
+ ),
136
+ category: type("'Resource'|'Client'|'Utility'|'Types'").describe(
137
+ "The classification of the Resource's Document, one of: Resource, Client, Utility, or Types."
138
+ ),
139
+ }).array(),
140
+ }),
141
+ system: await alchemy`
142
+ You are a technical writer tasked with identifying the distinct documents that need to be written for a document group (folder) in a documentation site.
143
+ You will be provided with a list of documents and instructions on how to classify them.
144
+ Each document has a title, file name, and category.
145
+ `,
146
+ prompt: await alchemy`
147
+ Identify and classify the documents that need to be written for the '${provider}' Service's Alchemy Resources.
148
+ For background knowledge on Alchemy, see ${alchemy.file("./README.md")}.
149
+ For background knowledge on the structure of an Alchemy Resource, see ${alchemy.file("./.cursorrules")}.
150
+
151
+ The ${provider} Service has the following resources:
152
+ ${alchemy.files(files)}
153
+
154
+ A file is considered a "Resource" if it contains a const <ResourceName> = Resource(...) call or if it is a function that calls a Resource function, e.g. const TypeScriptFile = () => File(...).
155
+ A file is considered a "Client" if it exposes a wrapper around creating a SDK client or fetch.
156
+ A file is considered a "Utility" if it contains utility functions that are not resources or clients.
157
+ A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
158
+
159
+ The title should be simply the name of the resource's const in code (with spaces added in between each word), e.g. "Bucket" or "Function", except with spaces, e.g. "Static Site" for "const StaticSite". Maintain all other casing.
160
+
161
+ // "Resource Name"
162
+ const ResourceName = Resource(...)
163
+ `,
164
+ });
165
+
166
+ const providerDocsDir = (await Folder(path.join(outDir, providerName))).path;
167
+
168
+ let documents: Document[] = [];
169
+ if (parallel) {
170
+ documents = await Promise.all(
171
+ groups.filter((g) => g.category === "Resource").map(generateDocument)
172
+ );
173
+ } else {
174
+ for (const g of groups.filter((g) => g.category === "Resource")) {
175
+ documents.push(await generateDocument(g));
176
+ }
177
+ }
178
+
179
+ async function generateDocument(g: Group) {
180
+ return Document(`docs/${providerName}/${g.identifier}`, {
181
+ title: g.identifier,
182
+ path: path.join(
183
+ providerDocsDir,
184
+ `${g.filename.replace(".ts", "").replace(".md", "")}.md`
185
+ ),
186
+ freeze: true,
187
+ model: {
188
+ id: "claude-3-5-sonnet-latest",
189
+ provider: "anthropic",
190
+ // options: {
191
+ // reasoningEffort: "high",
192
+ // },
193
+ },
194
+ prompt: await alchemy`
195
+ You are a technical writer writing API documentation for an Alchemy IaC Resource.
196
+ See ${alchemy.file("./README.md")} to understand the overview of Alchemy.
197
+ See ${alchemy.file("./.cursorrules")} to better understand the structure and convention of an Alchemy Resource.
198
+
199
+ Relevant files for the ${providerName} Service:
200
+ ${alchemy.files(files)}
201
+
202
+ Write concise documentation for the "${g.identifier}" Resource.
203
+
204
+ > [!CAUTION]
205
+ > Avoid the temptation to over explain or over describe. Focus on concise, simple, high value snippets. One heading and 0-1 descriptions per snippet.
206
+
207
+ > [!TIP]
208
+ > Make sure the examples follow a natural progression from the minimal example to logical next steps of how the Resource might be used.
209
+
210
+ Each document must follow the following format:
211
+
212
+ # ${g.identifier}
213
+
214
+ (simple description with an external link to the provider's website)
215
+ e.g.
216
+ The Efs component lets you add [Amazon Elastic File System (EFS)](https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html) to your app.
217
+
218
+ # Minimal Example
219
+
220
+ (brief 1-2 sentences of what it does)
221
+
222
+ \`\`\`ts
223
+ import { ${g.identifier.replaceAll(" ", "")} } from "alchemy/${providerName}";
224
+
225
+ (example)
226
+ \`\`\`
227
+
228
+ # (one heading per variation)
229
+
230
+ (brief 1-2 sentences of what it does)
231
+
232
+ \`\`\`ts
233
+ import { ${g.identifier.replaceAll(" ", "")} } from "alchemy/${providerName}";
234
+
235
+ (example)
236
+ \`\`\`
237
+
238
+ Before writing the document, think through:
239
+ 1. What is the minimal, most common example use case for this resource?
240
+ 2. What are the variations (e.g. combination of different options) that are also commonly used, e.g. specifying the memory size of a lambda function.
241
+ 3. Make sure to draw from the examples and your understanding of Alchemy.
242
+
243
+ Refer to alchemy docs to understand the context of how this documentation is consumed:
244
+ - ${alchemy.file("./alchemy-web/docs/what-is-alchemy.md")}
245
+ - ${alchemy.file("./alchemy-web/docs/getting-started.md")}
246
+ - ${alchemy.folder("./alchemy-web/docs/concepts/")}
247
+
248
+ ${
249
+ providerName === "cloudflare"
250
+ ? await alchemy`# Bind to a Worker
251
+ (if it is a Cloudflare Resource)
252
+
253
+ (brief 1-2 sentences of what it does)
254
+
255
+ \`\`\`ts
256
+ import { Worker, ${g.identifier.replaceAll(" ", "")} } from "alchemy/${providerName}";
257
+
258
+ const myResource = await ${g.identifier.replaceAll(" ", "")}("my-resource", {
259
+ // ...
260
+ });
261
+
262
+ await Worker("my-worker", {
263
+ name: "my-worker",
264
+ script: "console.log('Hello, world!')",
265
+ bindings: {
266
+ myResource,
267
+ },
268
+ });
269
+ \`\`\``
270
+ : ""
271
+ }
272
+ `,
273
+ });
274
+ }
275
+
276
+ return {
277
+ dir: providerDocsDir,
278
+ provider: providerName,
279
+ documents,
280
+ };
281
+ }
package/src/secret.ts CHANGED
@@ -83,3 +83,19 @@ export function secret<S extends string | undefined>(unencrypted: S): Secret {
83
83
  }
84
84
  return new Secret(unencrypted);
85
85
  }
86
+
87
+ export namespace secret {
88
+ export async function env(
89
+ name: string,
90
+ value?: string,
91
+ error?: string
92
+ ): Promise<Secret> {
93
+ const alchemy = await import("./alchemy");
94
+ const result = await alchemy.env(name, value, error);
95
+ if (typeof result === "string") {
96
+ return secret(result);
97
+ } else {
98
+ throw new Error(`Secret environment variable ${name} is not a string`);
99
+ }
100
+ }
101
+ }
@@ -1,6 +1,6 @@
1
- import { Scope } from "../scope";
2
- import { Secret } from "../secret";
3
- import { decryptWithKey, encryptWithKey } from "./encrypt";
1
+ import { decryptWithKey, encrypt } from "./encrypt";
2
+ import { Scope } from "./scope";
3
+ import { Secret } from "./secret";
4
4
 
5
5
  import type { Type } from "arktype";
6
6
 
@@ -18,7 +18,7 @@ export async function serialize(
18
18
  value: any,
19
19
  options?: {
20
20
  encrypt?: boolean;
21
- },
21
+ }
22
22
  ): Promise<any> {
23
23
  if (Array.isArray(value)) {
24
24
  return Promise.all(value.map((value) => serialize(scope, value, options)));
@@ -29,13 +29,17 @@ export async function serialize(
29
29
  return {
30
30
  "@secret":
31
31
  options?.encrypt !== false
32
- ? await encryptWithKey(value.unencrypted, scope.password)
32
+ ? await encrypt(value.unencrypted, scope.password)
33
33
  : value.unencrypted,
34
34
  };
35
35
  } else if (isType(value)) {
36
36
  return {
37
37
  "@schema": value.toJSON(),
38
38
  };
39
+ } else if (value instanceof Date) {
40
+ return {
41
+ "@date": value.toISOString(),
42
+ };
39
43
  } else if (value instanceof Scope) {
40
44
  return undefined;
41
45
  } else if (value && typeof value === "object") {
@@ -44,8 +48,8 @@ export async function serialize(
44
48
  Object.entries(value).map(async ([key, value]) => [
45
49
  key,
46
50
  await serialize(scope, value, options),
47
- ]),
48
- ),
51
+ ])
52
+ )
49
53
  );
50
54
  }
51
55
  return value;
@@ -54,7 +58,7 @@ export async function serialize(
54
58
  export async function deserialize(scope: Scope, value: any): Promise<any> {
55
59
  if (Array.isArray(value)) {
56
60
  return await Promise.all(
57
- value.map(async (item) => await deserialize(scope, item)),
61
+ value.map(async (item) => await deserialize(scope, item))
58
62
  );
59
63
  } else if (value && typeof value === "object") {
60
64
  if (typeof value["@secret"] === "string") {
@@ -64,14 +68,16 @@ export async function deserialize(scope: Scope, value: any): Promise<any> {
64
68
  return new Secret(await decryptWithKey(value["@secret"], scope.password));
65
69
  } else if ("@schema" in value) {
66
70
  return value["@schema"];
71
+ } else if ("@date" in value) {
72
+ return new Date(value["@date"]);
67
73
  } else {
68
74
  return Object.fromEntries(
69
75
  await Promise.all(
70
76
  Object.entries(value).map(async ([key, value]) => [
71
77
  key,
72
78
  await deserialize(scope, value),
73
- ]),
74
- ),
79
+ ])
80
+ )
75
81
  );
76
82
  }
77
83
  }
package/src/test/bun.ts CHANGED
@@ -78,7 +78,7 @@ type test = {
78
78
  name: string,
79
79
  options: TestOptions,
80
80
  fn: (scope: Scope) => Promise<any>,
81
- timeout?: number,
81
+ timeout?: number
82
82
  ): void;
83
83
 
84
84
  /**
@@ -121,6 +121,7 @@ type test = {
121
121
  * ```
122
122
  */
123
123
  export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
124
+ let isFailed = false;
124
125
  defaultOptions = defaultOptions ?? {};
125
126
  if (
126
127
  defaultOptions.stateStore === undefined &&
@@ -129,7 +130,7 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
129
130
  ) {
130
131
  defaultOptions.stateStore = (scope) =>
131
132
  new R2RestStateStore(scope, {
132
- apiKey: process.env.CLOUDFLARE_API_KEY,
133
+ apiKey: alchemy.secret(process.env.CLOUDFLARE_API_KEY),
133
134
  email: process.env.CLOUDFLARE_EMAIL,
134
135
  bucketName: process.env.CLOUDFLARE_BUCKET_NAME!,
135
136
  });
@@ -150,7 +151,7 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
150
151
  {
151
152
  // parent: globalTestScope,
152
153
  stateStore: defaultOptions?.stateStore,
153
- },
154
+ }
154
155
  );
155
156
  test.scope = localTestScope;
156
157
 
@@ -170,7 +171,7 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
170
171
 
171
172
  // Clean up test scope after all tests complete
172
173
  afterAll(async () => {
173
- if (defaultOptions?.destroy !== false) {
174
+ if (defaultOptions?.destroy !== false && !isFailed) {
174
175
  await alchemy.destroy(test.scope);
175
176
  }
176
177
  });
@@ -192,8 +193,8 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
192
193
  obj && typeof obj === "object"
193
194
  ? Object.fromEntries(
194
195
  Object.entries(obj).flatMap(([k, v]) =>
195
- v !== undefined ? [[k, v]] : [],
196
- ),
196
+ v !== undefined ? [[k, v]] : []
197
+ )
197
198
  )
198
199
  : {};
199
200
 
@@ -229,7 +230,7 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
229
230
  }
230
231
  }
231
232
  });
232
- },
233
+ }
233
234
  );
234
235
  }
235
236
  }
@@ -0,0 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function sha256(value: string): string {
4
+ return createHash("sha256").update(value).digest("hex");
5
+ }
@@ -1,11 +1,12 @@
1
1
  import path from "path";
2
- import type { DefaultTheme, ThemeOptions } from "vitepress";
2
+ import type { DefaultTheme, HeadConfig, ThemeOptions } from "vitepress";
3
3
  import { StaticTypeScriptFile } from "../../fs/static-typescript-file";
4
4
 
5
5
  export type VitePressConfigProps = {
6
6
  cwd: string;
7
7
  title?: string;
8
8
  description?: string;
9
+ head?: HeadConfig[];
9
10
  theme?: ThemeOptions;
10
11
  themeConfig: DefaultTheme.Config;
11
12
  };
@@ -23,6 +24,7 @@ import { defineConfig } from "vitepress";
23
24
  export default defineConfig({
24
25
  title: ${JSON.stringify(props.title || "Alchemy")},
25
26
  description: ${JSON.stringify(props.description || "Alchemy Docs")},
27
+ head: ${JSON.stringify(props.head || [])},
26
28
  markdown: {
27
29
  // @ts-ignore
28
30
  codeTransformers: [transformerTwoslash()],
@@ -1,4 +1,5 @@
1
1
  export * from "./config";
2
2
  export * from "./custom-theme";
3
3
  export * from "./home-page";
4
+ export * from "./process-front-matter-files";
4
5
  export * from "./vitepress";