alchemy 0.2.5 → 0.3.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.
Files changed (151) hide show
  1. package/README.md +3 -3
  2. package/lib/ai/astro-file.d.ts +137 -0
  3. package/lib/ai/astro-file.js +173 -0
  4. package/lib/ai/client.d.ts +2 -10
  5. package/lib/ai/client.js +8 -24
  6. package/lib/ai/css-file.d.ts +131 -0
  7. package/lib/ai/css-file.js +158 -0
  8. package/lib/ai/data.d.ts +45 -5
  9. package/lib/ai/data.js +45 -10
  10. package/lib/ai/document.d.ts +44 -20
  11. package/lib/ai/document.js +95 -28
  12. package/lib/ai/html-file.d.ts +128 -0
  13. package/lib/ai/html-file.js +155 -0
  14. package/lib/ai/index.d.ts +7 -0
  15. package/lib/ai/index.js +7 -0
  16. package/lib/ai/json-file.d.ts +160 -0
  17. package/lib/ai/json-file.js +208 -0
  18. package/lib/ai/typescript-file.d.ts +140 -0
  19. package/lib/ai/typescript-file.js +176 -0
  20. package/lib/ai/vue-file.d.ts +126 -0
  21. package/lib/ai/vue-file.js +153 -0
  22. package/lib/ai/yaml-file.d.ts +169 -0
  23. package/lib/ai/yaml-file.js +236 -0
  24. package/lib/cloudflare/dns.d.ts +88 -0
  25. package/lib/cloudflare/dns.js +192 -0
  26. package/lib/cloudflare/index.d.ts +2 -1
  27. package/lib/cloudflare/index.js +2 -1
  28. package/lib/cloudflare/r2-rest-state-store.d.ts +125 -0
  29. package/lib/cloudflare/r2-rest-state-store.js +254 -0
  30. package/lib/cloudflare/response.d.ts +12 -0
  31. package/lib/cloudflare/response.js +0 -0
  32. package/lib/cloudflare/zone.d.ts +18 -0
  33. package/lib/cloudflare/zone.js +17 -4
  34. package/lib/dns/godaddy.d.ts +7 -0
  35. package/lib/dns/godaddy.js +21 -0
  36. package/lib/dns/import-dns.d.ts +82 -0
  37. package/lib/dns/import-dns.js +114 -0
  38. package/lib/dns/index.d.ts +2 -0
  39. package/lib/dns/index.js +2 -0
  40. package/lib/dns/record.d.ts +88 -0
  41. package/lib/dns/record.js +13 -0
  42. package/lib/fs/file-system-state-store.d.ts +17 -0
  43. package/lib/fs/file-system-state-store.js +87 -0
  44. package/lib/fs/folder.d.ts +10 -0
  45. package/lib/fs/folder.js +2 -2
  46. package/lib/fs/index.d.ts +4 -4
  47. package/lib/fs/index.js +4 -4
  48. package/lib/fs/{json-file.d.ts → static-json-file.d.ts} +2 -2
  49. package/lib/fs/static-json-file.js +14 -0
  50. package/lib/fs/{text-file.d.ts → static-text-file.d.ts} +2 -2
  51. package/lib/fs/{text-file.js → static-text-file.js} +1 -1
  52. package/lib/fs/{typescript-file.d.ts → static-typescript-file.d.ts} +2 -2
  53. package/lib/fs/{typescript-file.js → static-typescript-file.js} +1 -1
  54. package/lib/fs/{yaml-file.d.ts → static-yaml-file.d.ts} +2 -2
  55. package/lib/fs/{yaml-file.js → static-yaml-file.js} +1 -1
  56. package/lib/internal/getting-started.d.ts +10 -0
  57. package/lib/internal/getting-started.js +94 -0
  58. package/lib/internal/project.d.ts +13 -0
  59. package/lib/internal/project.js +101 -0
  60. package/lib/internal/providers.d.ts +23 -0
  61. package/lib/internal/providers.js +148 -0
  62. package/lib/resource.js +8 -1
  63. package/lib/scope.d.ts +1 -1
  64. package/lib/scope.js +4 -2
  65. package/lib/state.d.ts +1 -16
  66. package/lib/state.js +0 -87
  67. package/lib/test/bun.d.ts +11 -0
  68. package/lib/test/bun.js +36 -8
  69. package/lib/web/astro.d.ts +147 -0
  70. package/lib/web/astro.js +414 -0
  71. package/lib/{shadcn/component.js → web/shadcn-component.js} +2 -9
  72. package/lib/web/shadcn.d.ts +97 -0
  73. package/lib/web/shadcn.js +99 -0
  74. package/lib/web/tailwind.d.ts +64 -0
  75. package/lib/web/tailwind.js +97 -0
  76. package/lib/{vite → web}/vite.d.ts +5 -0
  77. package/lib/{vite → web}/vite.js +44 -44
  78. package/lib/web/vitepress/custom-theme.d.ts +173 -0
  79. package/lib/web/vitepress/custom-theme.js +336 -0
  80. package/lib/{vitepress → web/vitepress}/dependencies.d.ts +2 -2
  81. package/lib/{vitepress → web/vitepress}/dependencies.js +1 -1
  82. package/lib/web/vitepress/home-page.d.ts +244 -0
  83. package/lib/web/vitepress/home-page.js +134 -0
  84. package/lib/{vitepress → web/vitepress}/index.d.ts +1 -0
  85. package/lib/{vitepress → web/vitepress}/index.js +1 -0
  86. package/lib/{vitepress → web/vitepress}/vitepress.d.ts +10 -8
  87. package/lib/{vitepress → web/vitepress}/vitepress.js +11 -12
  88. package/package.json +6 -2
  89. package/src/ai/astro-file.ts +288 -0
  90. package/src/ai/client.ts +8 -28
  91. package/src/ai/css-file.ts +266 -0
  92. package/src/ai/data.ts +53 -16
  93. package/src/ai/document.ts +130 -33
  94. package/src/ai/html-file.ts +263 -0
  95. package/src/ai/index.ts +7 -0
  96. package/src/ai/json-file.ts +349 -0
  97. package/src/ai/typescript-file.ts +293 -0
  98. package/src/ai/vue-file.ts +261 -0
  99. package/src/ai/yaml-file.ts +368 -0
  100. package/src/apply.ts +0 -1
  101. package/src/cloudflare/dns.ts +344 -0
  102. package/src/cloudflare/index.ts +2 -1
  103. package/src/cloudflare/r2-rest-state-store.ts +353 -0
  104. package/src/cloudflare/response.ts +9 -0
  105. package/src/cloudflare/zone.ts +39 -23
  106. package/src/dns/godaddy.ts +29 -0
  107. package/src/dns/import-dns.ts +213 -0
  108. package/src/dns/index.ts +2 -0
  109. package/src/dns/record.ts +121 -0
  110. package/src/fs/file-system-state-store.ts +109 -0
  111. package/src/fs/folder.ts +14 -2
  112. package/src/fs/index.ts +4 -4
  113. package/src/fs/{json-file.ts → static-json-file.ts} +13 -3
  114. package/src/fs/{text-file.ts → static-text-file.ts} +5 -2
  115. package/src/fs/{typescript-file.ts → static-typescript-file.ts} +3 -3
  116. package/src/fs/{yaml-file.ts → static-yaml-file.ts} +5 -2
  117. package/src/internal/getting-started.ts +107 -0
  118. package/src/internal/project.ts +119 -0
  119. package/src/internal/providers.ts +209 -0
  120. package/src/resource.ts +10 -1
  121. package/src/scope.ts +5 -6
  122. package/src/state.ts +1 -111
  123. package/src/test/bun.ts +60 -10
  124. package/src/web/astro.ts +644 -0
  125. package/src/{shadcn/component.ts → web/shadcn-component.ts} +5 -13
  126. package/src/web/shadcn.ts +219 -0
  127. package/src/web/tailwind.ts +167 -0
  128. package/src/{vite → web}/vite.ts +54 -56
  129. package/src/web/vitepress/custom-theme.ts +514 -0
  130. package/src/{vitepress → web/vitepress}/dependencies.ts +2 -2
  131. package/src/web/vitepress/home-page.ts +363 -0
  132. package/src/{vitepress → web/vitepress}/index.ts +1 -0
  133. package/src/{vitepress → web/vitepress}/vitepress.ts +34 -27
  134. package/lib/cloudflare/state.d.ts +0 -82
  135. package/lib/cloudflare/state.js +0 -108
  136. package/lib/fs/json-file.js +0 -7
  137. package/lib/internal/docs.d.ts +0 -5
  138. package/lib/internal/docs.js +0 -287
  139. package/lib/shadcn/index.d.ts +0 -1
  140. package/lib/shadcn/index.js +0 -1
  141. package/lib/vite/index.d.ts +0 -1
  142. package/lib/vite/index.js +0 -1
  143. package/lib/vitepress/home-page.d.ts +0 -133
  144. package/lib/vitepress/home-page.js +0 -11
  145. package/src/cloudflare/state.ts +0 -138
  146. package/src/internal/docs.ts +0 -338
  147. package/src/shadcn/index.ts +0 -1
  148. package/src/vite/index.ts +0 -1
  149. package/src/vitepress/home-page.ts +0 -166
  150. /package/lib/{shadcn/component.d.ts → web/shadcn-component.d.ts} +0 -0
  151. /package/src/{vitepress → web/vitepress}/index.md +0 -0
@@ -0,0 +1,209 @@
1
+ import alchemy from "alchemy";
2
+ import { Data, Document } from "alchemy/ai";
3
+ import { Folder } from "alchemy/fs";
4
+ import { type } from "arktype";
5
+ import fs from "fs/promises";
6
+ import path from "path";
7
+
8
+ export interface DocsProps {
9
+ /**
10
+ * The output directory for the docs.
11
+ */
12
+ outDir: string | Folder;
13
+ /**
14
+ * Whether to filter the docs.
15
+ * If true, include all docs.
16
+ * If false, include none.
17
+ * If a number, include that many providers.
18
+ *
19
+ * @default true (all docs)
20
+ */
21
+ filter?: boolean | number;
22
+ }
23
+
24
+ export type AlchemyProviderDocs = {
25
+ dir: string;
26
+ provider: string;
27
+ documents: Document[];
28
+ }[];
29
+
30
+ export async function AlchemyProviderDocs({
31
+ outDir,
32
+ filter,
33
+ }: DocsProps): Promise<AlchemyProviderDocs> {
34
+ outDir = typeof outDir === "string" ? outDir : outDir.path;
35
+ const providersDir = (await Folder(path.join(outDir, "providers"))).path;
36
+
37
+ const exclude = ["util", "test", "vitepress", "vite", "shadcn", "internal"];
38
+
39
+ // Get all folders in the alchemy/src directory
40
+ let providers = (
41
+ await fs.readdir(path.resolve("..", "alchemy", "src"), {
42
+ withFileTypes: true,
43
+ })
44
+ )
45
+ .filter((dirent) => dirent.isDirectory() && !exclude.includes(dirent.name))
46
+ .map((dirent) => path.join(dirent.parentPath, dirent.name));
47
+
48
+ // For each provider, list all files
49
+ if (filter === false) {
50
+ return [];
51
+ } else if (typeof filter === "number") {
52
+ providers = providers.slice(0, filter);
53
+ }
54
+
55
+ return await Promise.all(
56
+ providers.map(async (provider) => {
57
+ const providerName = path.basename(provider);
58
+ const files = (
59
+ await fs.readdir(path.resolve(provider), {
60
+ withFileTypes: true,
61
+ })
62
+ )
63
+ .filter((dirent) => dirent.isFile())
64
+ .map((dirent) =>
65
+ path.relative(process.cwd(), path.resolve(provider, dirent.name)),
66
+ )
67
+ .filter((file) => file.endsWith(".ts") && !file.endsWith("index.ts"));
68
+
69
+ const {
70
+ object: { groups },
71
+ } = await Data(`docs/${providerName}`, {
72
+ model: {
73
+ id: "gpt-4o",
74
+ provider: "openai",
75
+ // options: {
76
+ // reasoningEffort: "high",
77
+ // },
78
+ },
79
+ temperature: 0.1,
80
+ schema: type({
81
+ groups: type({
82
+ title: type("string").describe(
83
+ "The title of the group, should be the Resource Name exactly as it's defined in code (const ResourceName translates to 'Resource Name') without spaces, e.g. Bucket or Static Site.",
84
+ ),
85
+ filename: type("string").describe(
86
+ "The filename of the Resource's Document, e.g. bucket.md or static-site.md",
87
+ ),
88
+ category: type("'Resource'|'Client'|'Utility'|'Types'").describe(
89
+ "The classification of the Resource's Document, one of: Resource, Client, Utility, or Types.",
90
+ ),
91
+ }).array(),
92
+ }),
93
+ system: await alchemy`
94
+ 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.
95
+ You will be provided with a list of documents and instructions on how to classify them.
96
+ Each document has a title, file name, and category.
97
+ `,
98
+ prompt: await alchemy`
99
+ Identify and classify the documents that need to be written for the '${provider}' Service's Alchemy Resources.
100
+ For background knowledge on Alchemy, see ${alchemy.file("../README.md")}.
101
+ For background knowledge on the structure of an Alchemy Resource, see ${alchemy.file("../.cursorrules")}.
102
+
103
+ The ${provider} Service has the following resources:
104
+ ${alchemy.files(files)}
105
+
106
+ 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(...).
107
+ A file is considered a "Client" if it exposes a wrapper around creating a SDK client or fetch.
108
+ A file is considered a "Utility" if it contains utility functions that are not resources or clients.
109
+ A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
110
+
111
+ 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.
112
+
113
+ // "Resource Name"
114
+ const ResourceName = Resource(...)
115
+ `,
116
+ });
117
+
118
+ // console.log(groups);
119
+
120
+ const providerDocsDir = (
121
+ await Folder(path.join(providersDir, providerName))
122
+ ).path;
123
+
124
+ const documents = await Promise.all(
125
+ groups
126
+ .filter((g) => g.category === "Resource")
127
+ .map(async (g) =>
128
+ Document(`docs/${providerName}/${g.title}`, {
129
+ title: g.title,
130
+ path: path.join(
131
+ providerDocsDir,
132
+ `${g.filename.replace(".ts", "").replace(".md", "")}.md`,
133
+ ),
134
+ prompt: await alchemy`
135
+ You are a technical writer writing API documentation for an Alchemy IaC Resource.
136
+ See ${alchemy.file("../README.md")} to understand the overview of Alchemy.
137
+ See ${alchemy.file("../.cursorrules")} to better understand the structure and convention of an Alchemy Resource.
138
+
139
+ Relevant files for the ${providerName} Service:
140
+ ${alchemy.files(files)}
141
+
142
+ Write concise documentation for the "${g.title}" Resource.
143
+
144
+ > [!CAUTION]
145
+ > Avoid the temptation to over explain or over describe. Focus on concise, simple, high value snippets. One heading and 0-1 descriptions per snippet.
146
+
147
+ > [!TIP]
148
+ > Make sure the examples follow a natural progression from the minimal example to logical next steps of how the Resource might be used.
149
+
150
+ Each document must follow the following format:
151
+
152
+ # ${g.title}
153
+
154
+ (simple description with an external link to the provider's website)
155
+ e.g.
156
+ The Efs component lets you add [Amazon Elastic File System (EFS)](https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html) to your app.
157
+
158
+ # Minimal Example
159
+
160
+ \`\`\`ts
161
+ import { ${g.title.replaceAll(" ", "")} } from "alchemy/${providerName}";
162
+
163
+ (example)
164
+ \`\`\`
165
+
166
+
167
+ # Create the ${g.title}
168
+
169
+ \`\`\`ts
170
+ import { ${g.title.replaceAll(" ", "")} } from "alchemy/${providerName}";
171
+
172
+ (example)
173
+ \`\`\`
174
+
175
+ ${
176
+ providerName === "cloudflare"
177
+ ? await alchemy`# Bind to a Worker
178
+ (if it is a Cloudflare Resource)
179
+
180
+ \`\`\`ts
181
+ import { Worker, ${g.title.replaceAll(" ", "")} } from "alchemy/${providerName}";
182
+
183
+ const myResource = await ${g.title.replaceAll(" ", "")}("my-resource", {
184
+ // ...
185
+ });
186
+
187
+ await Worker("my-worker", {
188
+ name: "my-worker",
189
+ script: "console.log('Hello, world!')",
190
+ bindings: {
191
+ myResource,
192
+ },
193
+ });
194
+ \`\`\``
195
+ : ""
196
+ }
197
+ `,
198
+ }),
199
+ ),
200
+ );
201
+
202
+ return {
203
+ dir: providerDocsDir,
204
+ provider: providerName,
205
+ documents,
206
+ } as const;
207
+ }),
208
+ );
209
+ }
package/src/resource.ts CHANGED
@@ -112,7 +112,16 @@ export function Resource<
112
112
  if (scope.resources.has(resourceID)) {
113
113
  // TODO(sam): do we want to throw?
114
114
  // it's kind of awesome that you can re-create a resource and call apply
115
- // console.warn(`Resource ${id} already exists in the stack: ${stack.id}`);
115
+ const otherResource = scope.resources.get(resourceID);
116
+ if (otherResource?.Kind !== type) {
117
+ scope.fail();
118
+ throw new Error(
119
+ `Resource ${resourceID} already exists in the stack and is of a different type: '${otherResource?.Kind}' !== '${type}'`,
120
+ );
121
+ }
122
+ // console.warn(
123
+ // `Resource ${resourceID} already exists in the stack: ${scope.chain.join("/")}`,
124
+ // );
116
125
  }
117
126
 
118
127
  // get a sequence number (unique within the scope) for the resource
package/src/scope.ts CHANGED
@@ -1,11 +1,8 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { destroy } from "./destroy";
3
+ import { FileSystemStateStore } from "./fs/file-system-state-store";
3
4
  import type { PendingResource, ResourceID } from "./resource";
4
- import {
5
- FileSystemStateStore,
6
- type StateStore,
7
- type StateStoreType,
8
- } from "./state";
5
+ import type { StateStore, StateStoreType } from "./state";
9
6
 
10
7
  const scopeStorage = new AsyncLocalStorage<Scope>();
11
8
 
@@ -53,7 +50,9 @@ export class Scope {
53
50
  throw new Error("Scope name is required when creating a child scope");
54
51
  }
55
52
  this.password = options.password;
56
- this.state = new (options.stateStore ?? FileSystemStateStore)(this);
53
+ this.state = options.stateStore
54
+ ? options.stateStore(this)
55
+ : new FileSystemStateStore(this);
57
56
  }
58
57
 
59
58
  public async delete(resourceID: ResourceID) {
package/src/state.ts CHANGED
@@ -1,9 +1,5 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
1
  import type { Resource, ResourceProps } from "./resource";
4
2
  import type { Scope } from "./scope";
5
- import { ignore } from "./util/ignore";
6
- import { deserialize, serialize } from "./util/serde";
7
3
 
8
4
  export interface State<
9
5
  Kind extends string = string,
@@ -28,7 +24,7 @@ export interface State<
28
24
  output: Out;
29
25
  }
30
26
 
31
- export type StateStoreType = new (scope: Scope) => StateStore;
27
+ export type StateStoreType = (scope: Scope) => StateStore;
32
28
 
33
29
  export interface StateStore {
34
30
  /** Initialize the state container if one is required */
@@ -45,109 +41,3 @@ export interface StateStore {
45
41
  set(key: string, value: State): Promise<void>;
46
42
  delete(key: string): Promise<void>;
47
43
  }
48
-
49
- const stateRootDir = path.join(process.cwd(), ".alchemy");
50
-
51
- export class FileSystemStateStore implements StateStore {
52
- public readonly dir: string;
53
- constructor(public readonly scope: Scope) {
54
- this.dir = path.join(stateRootDir, ...scope.chain);
55
- }
56
-
57
- async init(): Promise<void> {
58
- await fs.promises.mkdir(stateRootDir, { recursive: true });
59
- await fs.promises.mkdir(this.dir, { recursive: true });
60
- }
61
-
62
- async deinit(): Promise<void> {
63
- await ignore("ENOENT", () => fs.promises.rmdir(this.dir));
64
- }
65
-
66
- async count(): Promise<number> {
67
- return Object.keys(await this.list()).length;
68
- }
69
-
70
- async list(): Promise<string[]> {
71
- try {
72
- const files = await fs.promises.readdir(this.dir, {
73
- withFileTypes: true,
74
- });
75
- return files
76
- .filter((dirent) => dirent.isFile() && dirent.name.endsWith(".json"))
77
- .map((dirent) => dirent.name.replace(/\.json$/, ""))
78
- .map((key) => key.replaceAll(":", "/"));
79
- } catch (error: any) {
80
- if (error.code === "ENOENT") {
81
- return [];
82
- }
83
- throw error;
84
- }
85
- }
86
-
87
- async get(key: string): Promise<State | undefined> {
88
- try {
89
- const content = await fs.promises.readFile(
90
- await this.getPath(key),
91
- "utf8",
92
- );
93
- const state = (await deserialize(
94
- this.scope,
95
- JSON.parse(content),
96
- )) as State;
97
- if (state.output === undefined) {
98
- state.output = {} as any;
99
- }
100
- state.output.Scope = this.scope;
101
- return state;
102
- } catch (error: any) {
103
- if (error.code === "ENOENT") {
104
- return undefined;
105
- }
106
- throw error;
107
- }
108
- }
109
-
110
- async set(key: string, value: State): Promise<void> {
111
- return fs.promises.writeFile(
112
- await this.getPath(key),
113
- JSON.stringify(await serialize(this.scope, value), null, 2),
114
- );
115
- }
116
-
117
- async delete(key: string): Promise<void> {
118
- return fs.promises.unlink(await this.getPath(key));
119
- }
120
-
121
- async all(): Promise<Record<string, State>> {
122
- return this.getBatch(await this.list());
123
- }
124
-
125
- async getBatch(ids: string[]): Promise<Record<string, State>> {
126
- return Object.fromEntries(
127
- (
128
- await Promise.all(
129
- Array.from(ids).flatMap(async (id) => {
130
- const s = await this.get(id);
131
- if (s === undefined) {
132
- return [] as const;
133
- }
134
- return [[id, s]] as const;
135
- }),
136
- )
137
- ).flat(),
138
- );
139
- }
140
-
141
- private async getPath(key: string): Promise<string> {
142
- if (key.includes(":")) {
143
- throw new Error(`ID cannot include colons: ${key}`);
144
- }
145
- if (key.includes("/")) {
146
- key = key.replaceAll("/", ":");
147
- }
148
- const file = path.join(this.dir, `${key}.json`);
149
- const dir = path.dirname(file);
150
- await fs.promises.mkdir(dir, { recursive: true });
151
- return file;
152
- }
153
- }
package/src/test/bun.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  /// <reference types="bun" />
2
2
 
3
- import { afterAll, it } from "bun:test";
3
+ import { afterAll, beforeAll, it } from "bun:test";
4
4
  import path from "node:path";
5
5
  import { alchemy } from "../alchemy";
6
+ import { R2RestStateStore } from "../cloudflare";
6
7
  import { destroy } from "../destroy";
7
8
  import type { Scope } from "../scope";
9
+ import type { StateStoreType } from "../state";
8
10
 
9
11
  /**
10
12
  * Extend the Alchemy interface to include test functionality
@@ -41,12 +43,17 @@ export interface TestOptions {
41
43
  * @default "test-password".
42
44
  */
43
45
  password?: string;
44
- }
45
46
 
46
- /**
47
- * Global test scope for all tests
48
- */
49
- const globalTestScope = alchemy.scope("test");
47
+ /**
48
+ * Override the default state store for the test.
49
+ */
50
+ stateStore?: StateStoreType;
51
+
52
+ /**
53
+ * Prefix to use for the scope to isolate tests and environments.
54
+ */
55
+ prefix?: string;
56
+ }
50
57
 
51
58
  /**
52
59
  * Test function type definition with overloads
@@ -80,6 +87,10 @@ type test = {
80
87
  */
81
88
  skipIf(condition: boolean): test;
82
89
 
90
+ beforeAll(fn: (scope: Scope) => Promise<void>): void;
91
+
92
+ afterAll(fn: (scope: Scope) => Promise<void>): void;
93
+
83
94
  /**
84
95
  * Current test scope
85
96
  */
@@ -110,6 +121,20 @@ type test = {
110
121
  * ```
111
122
  */
112
123
  export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
124
+ defaultOptions = defaultOptions ?? {};
125
+ if (
126
+ defaultOptions.stateStore === undefined &&
127
+ // process.env.CI &&
128
+ process.env.ALCHEMY_STATE_STORE === "cloudflare"
129
+ ) {
130
+ defaultOptions.stateStore = (scope) =>
131
+ new R2RestStateStore(scope, {
132
+ apiKey: process.env.CLOUDFLARE_API_KEY,
133
+ email: process.env.CLOUDFLARE_EMAIL,
134
+ bucketName: process.env.CLOUDFLARE_BUCKET_NAME!,
135
+ });
136
+ }
137
+
113
138
  // Add skipIf functionality
114
139
  test.skipIf = (condition: boolean) => {
115
140
  if (condition) {
@@ -120,13 +145,35 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
120
145
  };
121
146
 
122
147
  // Create local test scope based on filename
123
- const localTestScope = alchemy.scope(path.basename(meta.filename), {
124
- parent: globalTestScope,
125
- });
148
+ const localTestScope = alchemy.scope(
149
+ `${defaultOptions.prefix ? `${defaultOptions.prefix}-` : ""}${path.basename(meta.filename)}`,
150
+ {
151
+ // parent: globalTestScope,
152
+ stateStore: defaultOptions?.stateStore,
153
+ },
154
+ );
126
155
  test.scope = localTestScope;
127
156
 
157
+ test.beforeAll = (fn: (scope: Scope) => Promise<void>) => {
158
+ return beforeAll(async () => {
159
+ test.scope.enter();
160
+ await fn(test.scope);
161
+ });
162
+ };
163
+
164
+ test.afterAll = (fn: (scope: Scope) => Promise<void>) => {
165
+ return afterAll(async () => {
166
+ test.scope.enter();
167
+ await fn(test.scope);
168
+ });
169
+ };
170
+
128
171
  // Clean up test scope after all tests complete
129
- afterAll(() => alchemy.destroy(test.scope));
172
+ afterAll(async () => {
173
+ if (defaultOptions?.destroy !== false) {
174
+ await alchemy.destroy(test.scope);
175
+ }
176
+ });
130
177
 
131
178
  return test as any;
132
179
 
@@ -173,6 +220,9 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
173
220
  scope.enter();
174
221
  try {
175
222
  await fn(scope);
223
+ } catch (err) {
224
+ console.error(err);
225
+ throw err;
176
226
  } finally {
177
227
  if (options.destroy !== false) {
178
228
  await destroy(scope);