alchemy 0.8.1 → 0.9.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.
package/lib/alchemy.d.ts CHANGED
@@ -28,7 +28,6 @@ export declare const alchemy: Alchemy;
28
28
  * await app.finalize();
29
29
  */
30
30
  export interface Alchemy {
31
- scope: typeof scope;
32
31
  run: typeof run;
33
32
  destroy: typeof destroy;
34
33
  /**
@@ -52,7 +51,7 @@ export interface Alchemy {
52
51
  * password: process.env.SECRET_PASSPHRASE
53
52
  * });
54
53
  */
55
- (...parameters: Parameters<typeof scope>): Promise<Scope>;
54
+ (appName: string, options?: Omit<AlchemyOptions, "appName">): Promise<Scope>;
56
55
  /**
57
56
  * Template literal tag that supports file interpolation for documentation.
58
57
  * Automatically formats the content and appends file contents as code blocks.
@@ -115,21 +114,15 @@ export interface AlchemyOptions {
115
114
  */
116
115
  password?: string;
117
116
  }
118
- /**
119
- * Enter a new scope synchronously.
120
- *
121
- * @example
122
- * // Create a scope with a password for secret handling
123
- * await using scope = alchemy.scope("my-scope", {
124
- * password: process.env.SECRET_PASSPHRASE
125
- * });
126
- *
127
- * // Use secrets within the scope
128
- * const resource = await Resource("my-resource", {
129
- * apiKey: alchemy.secret(process.env.API_KEY)
130
- * });
131
- */
132
- declare function scope(id: string | undefined, options?: AlchemyOptions): Scope;
117
+ export interface ScopeOptions extends AlchemyOptions {
118
+ enter: boolean;
119
+ }
120
+ export interface RunOptions extends AlchemyOptions {
121
+ /**
122
+ * @default false
123
+ */
124
+ isResource?: boolean;
125
+ }
133
126
  /**
134
127
  * Run a function in a new scope asynchronously.
135
128
  * Useful for isolating secret handling with a specific password.
@@ -147,7 +140,7 @@ declare function scope(id: string | undefined, options?: AlchemyOptions): Scope;
147
140
  */
148
141
  declare function run<T>(...args: [id: string, fn: (this: Scope, scope: Scope) => Promise<T>] | [
149
142
  id: string,
150
- options: AlchemyOptions,
143
+ options: RunOptions,
151
144
  fn: (this: Scope, scope: Scope) => Promise<T>
152
145
  ]): Promise<T>;
153
146
  export declare function env<T = string>(name: string, value?: T | undefined, error?: string): Promise<T>;
package/lib/alchemy.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { DestroyedSignal, destroy } from "./destroy";
3
+ import { destroy, DestroyedSignal } from "./destroy";
4
4
  import { Scope } from "./scope";
5
5
  import { secret } from "./secret";
6
- // TODO: support browser
7
- const DEFAULT_STAGE = process.env.ALCHEMY_STAGE ?? process.env.USER ?? "dev";
8
6
  export const alchemy = _alchemy;
9
7
  /**
10
8
  * Implementation of the alchemy function that handles both application scoping
@@ -13,11 +11,12 @@ export const alchemy = _alchemy;
13
11
  async function _alchemy(...args) {
14
12
  if (typeof args[0] === "string") {
15
13
  const [appName, options] = args;
16
- const root = scope(undefined, {
14
+ const root = new Scope({
17
15
  ...options,
18
16
  appName,
19
17
  stage: options?.stage,
20
18
  });
19
+ root.enter();
21
20
  if (options?.phase === "destroy") {
22
21
  await destroy(root);
23
22
  return process.exit(0);
@@ -119,37 +118,8 @@ async function _alchemy(...args) {
119
118
  }
120
119
  _alchemy.destroy = destroy;
121
120
  _alchemy.run = run;
122
- _alchemy.scope = scope;
123
121
  _alchemy.secret = secret;
124
122
  _alchemy.env = env;
125
- /**
126
- * Enter a new scope synchronously.
127
- *
128
- * @example
129
- * // Create a scope with a password for secret handling
130
- * await using scope = alchemy.scope("my-scope", {
131
- * password: process.env.SECRET_PASSPHRASE
132
- * });
133
- *
134
- * // Use secrets within the scope
135
- * const resource = await Resource("my-resource", {
136
- * apiKey: alchemy.secret(process.env.API_KEY)
137
- * });
138
- */
139
- function scope(id, options
140
- // TODO: maybe we want to allow using _ = await alchemy.scope(import.meta)
141
- // | [meta: ImportMeta]
142
- ) {
143
- const scope = new Scope({
144
- ...options,
145
- appName: options?.appName,
146
- stage: options?.stage ?? DEFAULT_STAGE,
147
- scopeName: id,
148
- parent: options?.parent ?? Scope.get(),
149
- });
150
- scope.enter();
151
- return scope;
152
- }
153
123
  /**
154
124
  * Run a function in a new scope asynchronously.
155
125
  * Useful for isolating secret handling with a specific password.
@@ -169,20 +139,45 @@ async function run(...args) {
169
139
  const [id, options, fn] = typeof args[1] === "function"
170
140
  ? [args[0], undefined, args[1]]
171
141
  : args;
172
- const scope = alchemy.scope(id, options);
142
+ const _scope = new Scope({
143
+ ...options,
144
+ scopeName: id,
145
+ });
173
146
  try {
174
- return await fn.bind(scope)(scope);
147
+ if (options?.isResource !== true && _scope.parent) {
148
+ // TODO(sam): this is an awful hack to differentiate between naked scopes and resources
149
+ const seq = _scope.parent.seq();
150
+ const output = {
151
+ ID: id,
152
+ FQN: "",
153
+ Kind: "alchemy::Scope",
154
+ Scope: _scope,
155
+ Seq: seq,
156
+ };
157
+ const resource = {
158
+ kind: "scope",
159
+ id,
160
+ seq,
161
+ data: {},
162
+ fqn: "",
163
+ props: {},
164
+ status: "created",
165
+ output,
166
+ };
167
+ await _scope.parent.state.set(id, resource);
168
+ _scope.parent.resources.set(id, Object.assign(Promise.resolve(resource), output));
169
+ }
170
+ return await _scope.run(async () => fn.bind(_scope)(_scope));
175
171
  }
176
172
  catch (error) {
177
173
  if (!(error instanceof DestroyedSignal)) {
178
- scope.fail();
179
- }
180
- else {
174
+ console.log(error);
175
+ _scope.fail();
181
176
  }
182
177
  throw error;
183
178
  }
184
179
  finally {
185
- await scope.finalize();
180
+ await _scope.finalize();
186
181
  }
187
182
  }
188
183
  export async function env(name, value, error) {
package/lib/apply.js CHANGED
@@ -75,7 +75,9 @@ export async function apply(resource, props, options) {
75
75
  isReplaced = true;
76
76
  },
77
77
  });
78
- const output = await alchemy.run(resource.ID, async () => provider.handler.bind(ctx)(resource.ID, props));
78
+ const output = await alchemy.run(resource.ID, {
79
+ isResource: true,
80
+ }, async () => provider.handler.bind(ctx)(resource.ID, props));
79
81
  if (!quiet) {
80
82
  console.log(`${phase === "create" ? "Created" : "Updated"}: "${resource.FQN}"`);
81
83
  }
@@ -15,6 +15,7 @@ export * from "./queue";
15
15
  export * from "./r2-rest-state-store";
16
16
  export * from "./vectorize-index";
17
17
  export * from "./vectorize-metadata-index";
18
+ export * from "./vite-site";
18
19
  export * from "./worker";
19
20
  export { Workflow } from "./workflow";
20
21
  export * from "./wrangler.json";
@@ -15,6 +15,7 @@ export * from "./queue";
15
15
  export * from "./r2-rest-state-store";
16
16
  export * from "./vectorize-index";
17
17
  export * from "./vectorize-metadata-index";
18
+ export * from "./vite-site";
18
19
  export * from "./worker";
19
20
  export { Workflow } from "./workflow";
20
21
  export * from "./wrangler.json";
@@ -71,7 +71,6 @@ export const Pipeline = Resource("cloudflare::Pipeline", async function (id, pro
71
71
  const api = await createCloudflareApi(props);
72
72
  const pipelineName = props.name || id;
73
73
  if (this.phase === "delete") {
74
- console.log("Deleting Cloudflare Pipeline:", pipelineName);
75
74
  if (props.delete !== false) {
76
75
  // Delete Pipeline
77
76
  await deletePipeline(api, pipelineName);
@@ -82,13 +81,11 @@ export const Pipeline = Resource("cloudflare::Pipeline", async function (id, pro
82
81
  else {
83
82
  let pipelineData;
84
83
  if (this.phase === "create") {
85
- console.log("Creating Cloudflare Pipeline:", pipelineName);
86
84
  pipelineData = await createPipeline(api, pipelineName, props);
87
85
  }
88
86
  else {
89
87
  // Update operation
90
88
  if (this.output?.id) {
91
- console.log("Updating Cloudflare Pipeline:", pipelineName);
92
89
  // Check if name is being changed, which is not allowed
93
90
  if (props.name !== this.output.name) {
94
91
  throw new Error("Cannot update Pipeline name after creation. Pipeline name is immutable.");
@@ -0,0 +1,39 @@
1
+ import { Assets } from "./assets";
2
+ import type { Bindings } from "./bindings";
3
+ import { Worker } from "./worker";
4
+ export interface ViteSiteProps<B extends Bindings> {
5
+ /**
6
+ * The command to run to build the site
7
+ */
8
+ command: string;
9
+ /**
10
+ * The name of the worker
11
+ *
12
+ * @default id
13
+ */
14
+ name?: string;
15
+ /**
16
+ * The entrypoint to your server
17
+ *
18
+ * @default - a simple server that serves static assets is generated
19
+ */
20
+ main?: string;
21
+ /**
22
+ * The directory containing your static assets
23
+ */
24
+ assets: string;
25
+ /**
26
+ * The bindings to pass to the worker
27
+ */
28
+ bindings?: B;
29
+ /**
30
+ * @default process.cwd()
31
+ */
32
+ cwd?: string;
33
+ }
34
+ export type ViteSite<B extends Bindings> = Promise<B extends {
35
+ ASSETS: any;
36
+ } ? never : Worker<B & {
37
+ ASSETS: Assets;
38
+ }>>;
39
+ export declare function ViteSite<B extends Bindings>(id: string, props: ViteSiteProps<B>): ViteSite<B>;
@@ -0,0 +1,57 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { alchemy } from "../alchemy";
4
+ import { Exec } from "../os";
5
+ import { Assets } from "./assets";
6
+ import { Worker } from "./worker";
7
+ import { WranglerJson } from "./wrangler.json";
8
+ export async function ViteSite(id, props) {
9
+ if (props.bindings?.ASSETS) {
10
+ throw new Error("ASSETS binding is reserved for internal use");
11
+ }
12
+ // @ts-ignore - we know the types are correct
13
+ return await alchemy.run(id, async () => {
14
+ // Create minimal wrangler.jsonc if it doesn't exist
15
+ // `building the site requires a wrangler.jsonc file to start - so initialize an empty one if it doesn't exist`
16
+ const cwd = props.cwd || process.cwd();
17
+ const wranglerPath = path.join(cwd, "wrangler.jsonc");
18
+ try {
19
+ await fs.access(wranglerPath);
20
+ }
21
+ catch {
22
+ await fs.writeFile(wranglerPath, JSON.stringify({
23
+ name: id,
24
+ main: props.main,
25
+ compatibility_date: new Date().toISOString().split("T")[0],
26
+ }, null, 2));
27
+ }
28
+ await Exec("build", {
29
+ command: props.command,
30
+ });
31
+ const staticAssets = await Assets("assets", {
32
+ path: "./dist",
33
+ });
34
+ const worker = await Worker("worker", {
35
+ name: props.name ?? id,
36
+ entrypoint: props.main,
37
+ script: props.main
38
+ ? undefined
39
+ : `
40
+ export default {
41
+ async fetch(request, env) {
42
+ return env.ASSETS.fetch(request);
43
+ },
44
+ };`,
45
+ url: true,
46
+ adopt: true,
47
+ bindings: {
48
+ ...props.bindings,
49
+ ASSETS: staticAssets,
50
+ },
51
+ });
52
+ await WranglerJson("wrangler.jsonc", {
53
+ worker,
54
+ });
55
+ return worker;
56
+ });
57
+ }
@@ -125,6 +125,14 @@ export interface WranglerJsonSpec {
125
125
  * Plain text bindings (vars)
126
126
  */
127
127
  vars?: Record<string, string>;
128
+ /**
129
+ * D1 database bindings
130
+ */
131
+ d1_databases?: {
132
+ binding: string;
133
+ database_id: string;
134
+ database_name: string;
135
+ }[];
128
136
  /**
129
137
  * Assets bindings
130
138
  */
@@ -5,7 +5,7 @@ import { Resource } from "../resource";
5
5
  */
6
6
  export const WranglerJson = Resource("cloudflare::WranglerJson", async function (id, props) {
7
7
  // Default path is wrangler.json in current directory
8
- const filePath = props.path || "wrangler.json";
8
+ const filePath = props.path || "wrangler.jsonc";
9
9
  if (this.phase === "delete") {
10
10
  return this.destroy();
11
11
  }
@@ -143,6 +143,9 @@ function processBindings(spec, bindings) {
143
143
  if (services.length > 0) {
144
144
  spec.services = services;
145
145
  }
146
+ if (d1Databases.length > 0) {
147
+ spec.d1_databases = d1Databases;
148
+ }
146
149
  if (queues.length > 0) {
147
150
  spec.queues = queues;
148
151
  }
package/lib/destroy.js CHANGED
@@ -33,6 +33,14 @@ export async function destroy(...args) {
33
33
  if (!instance) {
34
34
  return;
35
35
  }
36
+ if (instance.Kind === "alchemy::Scope") {
37
+ const scope = new Scope({
38
+ parent: instance.Scope,
39
+ scopeName: instance.ID,
40
+ });
41
+ console.log("Destroying scope", scope.chain.join("/"));
42
+ return await destroy(scope, options);
43
+ }
36
44
  const Provider = PROVIDERS.get(instance.Kind);
37
45
  if (!Provider) {
38
46
  throw new Error(`Cannot destroy resource "${instance.FQN}" type ${instance.Kind} - no provider found. You may need to import the provider in your alchemy.config.ts.`);
@@ -66,7 +74,11 @@ export async function destroy(...args) {
66
74
  let nestedScope;
67
75
  try {
68
76
  // BUG: this does not restore persisted scope
69
- await alchemy.run(instance.ID, async (scope) => {
77
+ await alchemy.run(instance.ID, {
78
+ // TODO(sam): this is an awful hack to differentiate between naked scopes and resources
79
+ isResource: instance.Kind !== "alchemy::Scope",
80
+ parent: scope,
81
+ }, async (scope) => {
70
82
  nestedScope = scope;
71
83
  return await Provider.handler.bind(ctx)(instance.ID, state.props);
72
84
  });
@@ -97,7 +109,7 @@ export async function destroy(...args) {
97
109
  if (options?.strategy !== "parallel") {
98
110
  const sorted = resources.sort((a, b) => b.Seq - a.Seq);
99
111
  for (const resource of sorted) {
100
- await destroy(resource);
112
+ await destroy(resource, options);
101
113
  }
102
114
  }
103
115
  else {
@@ -3,6 +3,7 @@ import type { State, StateStore } from "../state";
3
3
  export declare class FileSystemStateStore implements StateStore {
4
4
  readonly scope: Scope;
5
5
  readonly dir: string;
6
+ private initialized;
6
7
  constructor(scope: Scope);
7
8
  init(): Promise<void>;
8
9
  deinit(): Promise<void>;
@@ -1,4 +1,4 @@
1
- import fs from "node:fs/promises";
1
+ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { deserialize, serialize } from "../serde";
4
4
  import { ignore } from "../util/ignore";
@@ -6,23 +6,27 @@ const stateRootDir = path.join(process.cwd(), ".alchemy");
6
6
  export class FileSystemStateStore {
7
7
  scope;
8
8
  dir;
9
+ initialized = false;
9
10
  constructor(scope) {
10
11
  this.scope = scope;
11
12
  this.dir = path.join(stateRootDir, ...scope.chain);
12
13
  }
13
14
  async init() {
14
- await fs.mkdir(stateRootDir, { recursive: true });
15
- await fs.mkdir(this.dir, { recursive: true });
15
+ if (this.initialized) {
16
+ return;
17
+ }
18
+ this.initialized = true;
19
+ await fs.promises.mkdir(this.dir, { recursive: true });
16
20
  }
17
21
  async deinit() {
18
- await ignore("ENOENT", () => fs.rmdir(this.dir));
22
+ await ignore("ENOENT", () => fs.promises.rmdir(this.dir));
19
23
  }
20
24
  async count() {
21
25
  return Object.keys(await this.list()).length;
22
26
  }
23
27
  async list() {
24
28
  try {
25
- const files = await fs.readdir(this.dir, {
29
+ const files = await fs.promises.readdir(this.dir, {
26
30
  withFileTypes: true,
27
31
  });
28
32
  return files
@@ -39,7 +43,7 @@ export class FileSystemStateStore {
39
43
  }
40
44
  async get(key) {
41
45
  try {
42
- const content = await fs.readFile(await this.getPath(key), "utf8");
46
+ const content = await fs.promises.readFile(this.getPath(key), "utf8");
43
47
  const state = (await deserialize(this.scope, JSON.parse(content)));
44
48
  if (state.output === undefined) {
45
49
  state.output = {};
@@ -55,10 +59,19 @@ export class FileSystemStateStore {
55
59
  }
56
60
  }
57
61
  async set(key, value) {
58
- return fs.writeFile(await this.getPath(key), JSON.stringify(await serialize(this.scope, value), null, 2));
62
+ await this.init();
63
+ await fs.promises.writeFile(this.getPath(key), JSON.stringify(await serialize(this.scope, value), null, 2));
59
64
  }
60
65
  async delete(key) {
61
- return fs.unlink(await this.getPath(key));
66
+ try {
67
+ return await fs.promises.unlink(this.getPath(key));
68
+ }
69
+ catch (error) {
70
+ if (error.code === "ENOENT") {
71
+ return;
72
+ }
73
+ throw error;
74
+ }
62
75
  }
63
76
  async all() {
64
77
  return this.getBatch(await this.list());
@@ -72,16 +85,13 @@ export class FileSystemStateStore {
72
85
  return [[id, s]];
73
86
  }))).flat());
74
87
  }
75
- async getPath(key) {
88
+ getPath(key) {
76
89
  if (key.includes(":")) {
77
90
  throw new Error(`ID cannot include colons: ${key}`);
78
91
  }
79
92
  if (key.includes("/")) {
80
93
  key = key.replaceAll("/", ":");
81
94
  }
82
- const file = path.join(this.dir, `${key}.json`);
83
- const dir = path.dirname(file);
84
- await fs.mkdir(dir, { recursive: true });
85
- return file;
95
+ return path.join(this.dir, `${key}.json`);
86
96
  }
87
97
  }
package/lib/scope.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { PendingResource, ResourceID } from "./resource";
2
2
  import type { StateStore, StateStoreType } from "./state";
3
3
  export type ScopeOptions = {
4
4
  appName?: string;
5
- stage: string;
5
+ stage?: string;
6
6
  parent?: Scope;
7
7
  scopeName?: string;
8
8
  password?: string;
package/lib/scope.js CHANGED
@@ -2,6 +2,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { destroy } from "./destroy";
3
3
  import { FileSystemStateStore } from "./fs/file-system-state-store";
4
4
  const scopeStorage = new AsyncLocalStorage();
5
+ // TODO: support browser
6
+ const DEFAULT_STAGE = process.env.ALCHEMY_STAGE ?? process.env.USER ?? "dev";
5
7
  export class Scope {
6
8
  static get() {
7
9
  return scopeStorage.getStore();
@@ -24,14 +26,14 @@ export class Scope {
24
26
  isErrored = false;
25
27
  constructor(options) {
26
28
  this.appName = options.appName;
27
- this.stage = options.stage;
29
+ this.stage = options?.stage ?? DEFAULT_STAGE;
28
30
  this.scopeName = options.scopeName ?? null;
29
31
  this.parent = options.parent ?? Scope.get();
30
32
  this.quiet = options.quiet ?? this.parent?.quiet ?? false;
31
33
  if (this.parent && !this.scopeName) {
32
34
  throw new Error("Scope name is required when creating a child scope");
33
35
  }
34
- this.password = options.password;
36
+ this.password = options.password ?? this.parent?.password;
35
37
  this.state = options.stateStore
36
38
  ? options.stateStore(this)
37
39
  : new FileSystemStateStore(this);
@@ -65,6 +67,7 @@ export class Scope {
65
67
  await this.state.init?.();
66
68
  }
67
69
  async deinit() {
70
+ await this.parent?.state.delete(this.scopeName);
68
71
  await this.state.deinit?.();
69
72
  }
70
73
  fqn(resourceID) {
package/lib/state.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Resource, ResourceProps } from "./resource";
2
2
  import type { Scope } from "./scope";
3
- export interface State<Kind extends string = string, Props extends ResourceProps | undefined = ResourceProps | undefined, Out extends Resource = Resource> {
3
+ export type State<Kind extends string = string, Props extends ResourceProps | undefined = ResourceProps | undefined, Out extends Resource = Resource> = {
4
4
  status: `creating` | `created` | `updating` | `updated` | `deleting` | `deleted`;
5
5
  kind: Kind;
6
6
  id: string;
@@ -10,7 +10,7 @@ export interface State<Kind extends string = string, Props extends ResourceProps
10
10
  props: Props;
11
11
  oldProps?: Props;
12
12
  output: Out;
13
- }
13
+ };
14
14
  export type StateStoreType = (scope: Scope) => StateStore;
15
15
  export interface StateStore {
16
16
  /** Initialize the state container if one is required */
package/lib/test/bun.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Scope } from "../scope";
1
+ import { Scope } from "../scope";
2
2
  import type { StateStoreType } from "../state";
3
3
  /**
4
4
  * Extend the Alchemy interface to include test functionality
@@ -12,11 +12,6 @@ declare module "../alchemy" {
12
12
  * Options for configuring test behavior
13
13
  */
14
14
  export interface TestOptions {
15
- /**
16
- * Whether to automatically destroy resources after test.
17
- * @default true.
18
- */
19
- destroy?: boolean;
20
15
  /**
21
16
  * Whether to suppress logging output.
22
17
  * @default false.
package/lib/test/bun.js CHANGED
@@ -3,7 +3,7 @@ import { afterAll, beforeAll, it } from "bun:test";
3
3
  import path from "node:path";
4
4
  import { alchemy } from "../alchemy";
5
5
  import { R2RestStateStore } from "../cloudflare";
6
- import { destroy } from "../destroy";
6
+ import { Scope } from "../scope";
7
7
  /**
8
8
  * Add test functionality to alchemy instance
9
9
  */
@@ -32,7 +32,6 @@ alchemy.test = test;
32
32
  * ```
33
33
  */
34
34
  export function test(meta, defaultOptions) {
35
- let isFailed = false;
36
35
  defaultOptions = defaultOptions ?? {};
37
36
  if (defaultOptions.stateStore === undefined &&
38
37
  // process.env.CI &&
@@ -52,29 +51,17 @@ export function test(meta, defaultOptions) {
52
51
  return test;
53
52
  };
54
53
  // Create local test scope based on filename
55
- const localTestScope = alchemy.scope(`${defaultOptions.prefix ? `${defaultOptions.prefix}-` : ""}${path.basename(meta.filename)}`, {
54
+ const scope = new Scope({
55
+ scopeName: `${defaultOptions.prefix ? `${defaultOptions.prefix}-` : ""}${path.basename(meta.filename)}`,
56
56
  // parent: globalTestScope,
57
57
  stateStore: defaultOptions?.stateStore,
58
58
  });
59
- test.scope = localTestScope;
60
59
  test.beforeAll = (fn) => {
61
- return beforeAll(async () => {
62
- test.scope.enter();
63
- await fn(test.scope);
64
- });
60
+ return beforeAll(() => scope.run(() => fn(scope)));
65
61
  };
66
62
  test.afterAll = (fn) => {
67
- return afterAll(async () => {
68
- test.scope.enter();
69
- await fn(test.scope);
70
- });
63
+ return afterAll(() => scope.run(() => fn(scope)));
71
64
  };
72
- // Clean up test scope after all tests complete
73
- afterAll(async () => {
74
- if (defaultOptions?.destroy !== false && !isFailed) {
75
- await alchemy.destroy(test.scope);
76
- }
77
- });
78
65
  return test;
79
66
  function test(...args) {
80
67
  const testName = args[0];
@@ -87,33 +74,24 @@ export function test(meta, defaultOptions) {
87
74
  : {};
88
75
  // Merge options with defaults
89
76
  const options = {
90
- destroy: true,
91
77
  quiet: false,
92
78
  password: "test-password",
93
79
  ...spread(defaultOptions),
94
80
  ...spread(_options),
95
81
  };
96
82
  const fn = typeof args[1] === "function" ? args[1] : args[2];
97
- return alchemy.run(testName, {
83
+ return it(testName, async () => alchemy.run(testName, {
98
84
  ...options,
99
- parent: localTestScope,
85
+ parent: scope,
100
86
  }, async (scope) => {
101
- return it(testName, async () => {
87
+ try {
102
88
  // Enter test scope since bun calls from different scope
103
- scope.enter();
104
- try {
105
- await fn(scope);
106
- }
107
- catch (err) {
108
- console.error(err);
109
- throw err;
110
- }
111
- finally {
112
- if (options.destroy !== false) {
113
- await destroy(scope);
114
- }
115
- }
116
- }, timeout);
117
- });
89
+ await scope.run(() => fn(scope));
90
+ }
91
+ catch (err) {
92
+ console.error(err);
93
+ throw err;
94
+ }
95
+ }), timeout);
118
96
  }
119
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {