@useairfoil/effect-vcr 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -1,79 +1,237 @@
1
1
  # @useairfoil/effect-vcr
2
2
 
3
- An HttpClient for Effect that records all HTTP interactions and stores them on tape, ready to be replayed for future test runs.
3
+ An `HttpClient` for Effect that records HTTP interactions to cassette files and replays them for future test runs.
4
4
 
5
- The first time the following test is run, it sends the HTTP request to httpbin.org and stores the request/response pair to a cassette. All subsequent tests will replay the response from the cassette, making no HTTP requests.
5
+ The first time the following test is run, it sends the HTTP request to `httpbin.org` and stores the request/response pair to a cassette. All subsequent test runs replay the stored response and make no live HTTP requests.
6
6
 
7
7
  ```ts
8
8
  // my-program.ts
9
+ import { Effect } from "effect";
10
+ import { HttpClient } from "effect/unstable/http";
11
+
9
12
  export const program = Effect.gen(function* () {
10
13
  const client = yield* HttpClient.HttpClient;
11
- const response = yield* client.get("https://httpbin.org");
14
+ const response = yield* client.get("https://httpbin.org/robots.txt");
12
15
  return yield* response.text;
13
16
  });
14
17
 
15
18
  // my-program.test.ts
16
- import { VcrHttpClient, FileSystemCassetteStore } from "@useairfoil/effect-vcr";
19
+ import { NodeServices } from "@effect/platform-node";
20
+ import { FileSystemCassetteStore, VcrHttpClient } from "@useairfoil/effect-vcr";
21
+ import { describe, it } from "@effect/vitest";
22
+ import { Effect, Layer } from "effect";
23
+ import { FetchHttpClient } from "effect/unstable/http";
24
+
25
+ const cassetteStoreLayer = FileSystemCassetteStore.layer().pipe(Layer.provide(NodeServices.layer));
26
+
27
+ const vcrRuntimeLayer = Layer.mergeAll(
28
+ FetchHttpClient.layer,
29
+ NodeServices.layer,
30
+ cassetteStoreLayer,
31
+ );
32
+
33
+ const vcrLayer = VcrHttpClient.layer({
34
+ vcrName: "httpbin",
35
+ mode: "auto",
36
+ }).pipe(Layer.provide(vcrRuntimeLayer));
17
37
 
18
38
  describe("my awesome program", () => {
19
- it.effect("works", () =>
20
- Effect.gen(function* () {
21
- yield* program;
22
- }),
23
- ).pipe(
24
- // Wrap the HttpClient with the VCR
25
- Effect.provide(VcrHttpClient.layer()),
26
- // Store cassettes on the file system
27
- Effect.provide(FileSystemCassetteStore.layer()),
28
- // HttpClient based on fetch
29
- Effect.provide(FetchHttpClient.layer),
30
- // Node is used to access the file system
31
- Effect.provide(NodeServices.layer),
32
- );
39
+ it.effect("works", () => program.pipe(Effect.provide(vcrLayer)));
33
40
  });
34
41
  ```
35
42
 
36
- The main use case for the VCR is to allow open source projects to run tests against a real API endpoint without exposing secrets to forks.
43
+ The main use case for the VCR is to let open source projects run tests against a real upstream API without exposing secrets to forks.
44
+
45
+ - project owners generate cassettes locally by talking to the real API
46
+ - external contributors run tests against committed cassettes
47
+ - CI runs deterministically from cassettes
48
+ - scheduled or manual runs can still hit the live API to catch upstream drift
37
49
 
38
- - Project owners generate cassettes locally by interacting with the upstream API
39
- - External contributors run their tests using the cassettes
40
- - CI runs using cassettes
41
- - Optionally, CI runs against the upstream API periodically, to catch any schema drift
50
+ ## Package shape
51
+
52
+ Root exports:
53
+
54
+ - `CassetteStore`
55
+ - `FileSystemCassetteStore`
56
+ - `VcrHttpClient`
57
+ - `VcrConfig`
58
+ - `VcrMode`
59
+ - `VcrRequest`
60
+ - `VcrResponse`
61
+ - `VcrEntry`
62
+ - `Cassette`
63
+ - `CassetteFile`
64
+
65
+ Focused subpath exports are also available:
66
+
67
+ - `@useairfoil/effect-vcr/cassette-store`
68
+ - `@useairfoil/effect-vcr/file-system-cassette-store`
69
+ - `@useairfoil/effect-vcr/types`
70
+ - `@useairfoil/effect-vcr/vcr-http-client`
42
71
 
43
72
  ## Usage
44
73
 
45
- Use the VCR by providing the VcrHttpClient and FileSystemCassetteStore layers to the Effect being tested.
74
+ Use the VCR by providing `VcrHttpClient.layer(...)` to the Effect being tested.
75
+
76
+ - `VcrHttpClient` wraps an existing `HttpClient` and adds record/replay behavior
77
+ - `FileSystemCassetteStore` persists cassette files to disk
78
+
79
+ `VcrHttpClient.layer(...)` depends on:
46
80
 
47
- - VcrHttpClient: this HttpClient wraps another HttpClient, adding support for recording and replaying HTTP interactions.
48
- - FileSystemCassetteStore: stores cassettes on the file system.
81
+ - a live `HttpClient` implementation such as `FetchHttpClient.layer`
82
+ - `Path.Path` for cassette name resolution
83
+ - `CassetteStore.CassetteStore` for persistence
84
+
85
+ `FileSystemCassetteStore.layer(...)` depends on:
86
+
87
+ - `FileSystem.FileSystem`
88
+ - `Path.Path`
89
+
90
+ In Node tests, `NodeServices.layer` satisfies those platform services.
49
91
 
50
92
  ## Configuration and defaults
51
93
 
52
94
  ### VcrHttpClient
53
95
 
54
- - `vcrName?: string`: the VCR name. Used to selectively disable VCRs during test runs (more on this in the runtime configuration section).
55
- - `cassetteName?: string`: the cassette name. If not set, defaults to the current vitest file name.
56
- - `mode: "record" | "replay" | "auto"`: controls the VCR behaviour. Defaults to `auto`.
57
- - `record`: always call the live client and write a cassette.
58
- - `replay`: only serve from cassette; missing entries fail.
59
- - `auto`: replay if cassette exists, otherwise record. If `CI=true`, missing cassette fails.
60
- - `redact`: control which sensitive headers and JSON body keys to remove from requests and responses.
61
- - `.requestHeaders?: string[]`: redact the specified request headers.
62
- - `.responseHeaders?: string[]`: redact the specified response headers.
63
- - `.requestBodyKeys?: string[]`: redact the specified request JSON keys.
64
- - `.responseBodyKeys?: string[]`: redact the specified response JSON keys.
65
- - `matchIgnore`: control which fields are ignored when matching requests.
66
- - `.requestHeaders?: string[]`: ignore the specified request headers.
67
- - `.requestBodyKeys?: string[]`: ignore the specified request JSON keys.
68
- - `match: (request: VcrRequest, entry: VcrEntry) => boolean`: custom request to entry matcher.
96
+ ```ts
97
+ type VcrConfig = {
98
+ readonly vcrName?: string;
99
+ readonly cassetteName?: string;
100
+ readonly mode?: "record" | "replay" | "auto";
101
+ readonly redact?: {
102
+ readonly requestHeaders?: ReadonlyArray<string>;
103
+ readonly responseHeaders?: ReadonlyArray<string>;
104
+ readonly requestBodyKeys?: ReadonlyArray<string>;
105
+ readonly responseBodyKeys?: ReadonlyArray<string>;
106
+ };
107
+ readonly matchIgnore?: {
108
+ readonly requestHeaders?: ReadonlyArray<string>;
109
+ readonly requestBodyKeys?: ReadonlyArray<string>;
110
+ };
111
+ readonly match?: (request: VcrRequest, entry: VcrEntry) => boolean;
112
+ };
113
+ ```
114
+
115
+ - `vcrName?: string`
116
+ - logical VCR name used by `ACK_DISABLE_VCR`
117
+ - `cassetteName?: string`
118
+ - cassette file basename or full file name
119
+ - `users` resolves to `users.cassette`
120
+ - `users.cassette` is preserved as-is
121
+ - when omitted in Vitest, the cassette file defaults to `<test-file>.cassette` and the current test name becomes the export key inside that file
122
+ - `mode?: "record" | "replay" | "auto"`
123
+ - `record`: always call the live client and write a cassette
124
+ - `replay`: only serve from cassette; missing entries fail
125
+ - `auto`: replay if the cassette exists, otherwise record; when `CI=true`, missing cassettes fail instead of recording
126
+ - `redact`
127
+ - remove sensitive headers or JSON body keys before writing to disk
128
+ - `matchIgnore`
129
+ - ignore request headers or JSON body keys when computing the request lookup key
130
+ - `match`
131
+ - custom request matcher for advanced lookup behavior
132
+
133
+ Defaults:
134
+
135
+ - `mode` defaults to `"auto"`
136
+ - `authorization` is ignored for matching by default
137
+ - `authorization` is redacted from recorded request headers by default
69
138
 
70
139
  ### FileSystemCassetteStore
71
140
 
72
- - `cassetteDir?: string`: change the location of the cassette. By default, the VCR stores cassettes in the `__cassettes__` folder next to the test.
141
+ - `cassetteDir?: string`
142
+ - changes the cassette root directory
143
+ - when omitted in Vitest, cassettes are written to the `__cassettes__` directory beside the test file
144
+ - outside Vitest, provide `cassetteDir` explicitly
145
+
146
+ ## Matching and redaction
147
+
148
+ `matchIgnore` and `redact` solve different problems.
149
+
150
+ - `matchIgnore` changes how requests are matched to cassette entries
151
+ - `redact` changes what is persisted to disk
152
+
153
+ Typical usage:
154
+
155
+ ```ts
156
+ const vcrLayer = VcrHttpClient.layer({
157
+ vcrName: "shopify-products",
158
+ mode: "auto",
159
+ matchIgnore: {
160
+ requestHeaders: ["x-shopify-access-token", "authorization"],
161
+ },
162
+ redact: {
163
+ requestHeaders: ["x-shopify-access-token", "authorization"],
164
+ },
165
+ });
166
+ ```
167
+
168
+ Use `matchIgnore` when a request field should not affect cassette identity.
169
+
170
+ Use `redact` when a field should never be written to disk.
73
171
 
74
172
  ## Runtime configuration
75
173
 
76
174
  The VCR is controlled by the following environment variables:
77
175
 
78
- - `CI=true`: if set and the VCR mode is `auto`, tests with a missing cassette fail.
79
- - `ACK_DISABLE_VCR=<...>`: disable the VCR. If `'*'`, then all VCRs are disabled. To selectively disable VCRs, pass a comma separated list of VCRs to disable (based on their `vcrName` config). Use this to test against the upstream API. In this case, the cassettes are not updated.
176
+ - `CI=true`
177
+ - if the VCR mode is `auto`, tests with a missing cassette fail instead of recording
178
+ - `ACK_DISABLE_VCR=<...>`
179
+ - disables the VCR and falls back to the wrapped live client
180
+ - `*` disables all configured VCRs
181
+ - a comma-separated list disables only matching `vcrName` values
182
+
183
+ When the VCR is disabled, cassette files are not updated.
184
+
185
+ ## Common patterns
186
+
187
+ ### Build the VCR runtime once
188
+
189
+ ```ts
190
+ const cassetteStoreLayer = FileSystemCassetteStore.layer().pipe(Layer.provide(NodeServices.layer));
191
+
192
+ const vcrRuntimeLayer = Layer.mergeAll(
193
+ FetchHttpClient.layer,
194
+ NodeServices.layer,
195
+ cassetteStoreLayer,
196
+ );
197
+
198
+ const vcrLayer = VcrHttpClient.layer({
199
+ vcrName: "example",
200
+ mode: "auto",
201
+ }).pipe(Layer.provide(vcrRuntimeLayer));
202
+ ```
203
+
204
+ This keeps the dependency graph explicit:
205
+
206
+ - `FileSystemCassetteStore.layer(...)` gets its platform services from `NodeServices.layer`
207
+ - `VcrHttpClient.layer(...)` gets its live client, `Path.Path`, and cassette store from `vcrRuntimeLayer`
208
+
209
+ ### Override config in tests
210
+
211
+ ```ts
212
+ import { ConfigProvider, Layer } from "effect";
213
+
214
+ const vcrTestRuntimeLayer = Layer.mergeAll(
215
+ vcrRuntimeLayer,
216
+ ConfigProvider.layer(
217
+ ConfigProvider.fromUnknown({
218
+ CI: false,
219
+ ACK_DISABLE_VCR: "",
220
+ }),
221
+ ),
222
+ );
223
+
224
+ const vcrLayer = VcrHttpClient.layer({
225
+ vcrName: "example",
226
+ mode: "auto",
227
+ }).pipe(Layer.provide(vcrTestRuntimeLayer));
228
+ ```
229
+
230
+ ### Use focused subpath imports
231
+
232
+ ```ts
233
+ import * as VcrHttpClient from "@useairfoil/effect-vcr/vcr-http-client";
234
+ import * as FileSystemCassetteStore from "@useairfoil/effect-vcr/file-system-cassette-store";
235
+ ```
236
+
237
+ Use root imports for normal package consumption and subpath imports when you want a narrower surface.
@@ -0,0 +1,59 @@
1
+ import { n as CassetteFile, t as Cassette } from "./types-DWwkMzxG.js";
2
+ import { Context, Effect } from "effect";
3
+ import * as effect_Types0 from "effect/Types";
4
+ import * as effect_Cause0 from "effect/Cause";
5
+
6
+ //#region src/cassette-store.d.ts
7
+ declare namespace cassette_store_d_exports {
8
+ export { CassetteStore, CassetteStoreError, CassetteStoreOperation, CassetteStoreService, createEmptyCassette, createEmptyCassetteFile, make, parseCassette, toStoreError };
9
+ }
10
+ type CassetteStoreOperation = "exists" | "load" | "save" | "loadOrInit" | "resolveRoot";
11
+ /**
12
+ * Storage contract for VCR cassettes.
13
+ *
14
+ */
15
+ interface CassetteStoreService {
16
+ /** Returns true if a cassette with the given name exists. */
17
+ readonly exists: (name: string) => Effect.Effect<boolean, CassetteStoreError>;
18
+ /** Loads a cassette by name, returning a `CassetteFile` if found. */
19
+ readonly load: (name: string) => Effect.Effect<CassetteFile, CassetteStoreError>;
20
+ /** Saves a cassette by name, overwriting any existing cassette with the same name. */
21
+ readonly save: (name: string, content: CassetteFile) => Effect.Effect<void, CassetteStoreError>;
22
+ /** Loads a cassette by name, initializing a new empty cassette if not found. */
23
+ readonly loadOrInit: (name: string) => Effect.Effect<CassetteFile, CassetteStoreError>;
24
+ }
25
+ declare const CassetteStore_base: Context.ServiceClass<CassetteStore, "@useairfoil/effect-vcr/CassetteStore", CassetteStoreService>;
26
+ /**
27
+ * Effect service tag for the cassette store.
28
+ */
29
+ declare class CassetteStore extends CassetteStore_base {}
30
+ /**
31
+ * Creates a new empty cassette with a timestamp and format version.
32
+ */
33
+ declare const createEmptyCassette: () => Effect.Effect<Cassette, never, never>;
34
+ declare const createEmptyCassetteFile: () => Effect.Effect<CassetteFile>;
35
+ declare const CassetteStoreError_base: new <A extends Record<string, any> = {}>(args: effect_Types0.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => effect_Cause0.YieldableError & {
36
+ readonly _tag: "CassetteStoreError";
37
+ } & Readonly<A>;
38
+ /**
39
+ * Structured error for cassette persistence operations.
40
+ *
41
+ * `operation` indicates which store function failed.
42
+ * `path` is the file or directory path that was targeted.
43
+ */
44
+ declare class CassetteStoreError extends CassetteStoreError_base<{
45
+ readonly operation: CassetteStoreOperation;
46
+ readonly path: string;
47
+ readonly message?: string;
48
+ readonly cause?: unknown;
49
+ }> {}
50
+ declare const toStoreError: (operation: CassetteStoreOperation, path: string, cause?: unknown, message?: string) => CassetteStoreError;
51
+ /**
52
+ * Parses cassette JSON and maps invalid JSON to a store error.
53
+ */
54
+ declare const parseCassette: (content: string, path: string) => Effect.Effect<CassetteFile, CassetteStoreError, never>;
55
+ /** Creates a `CassetteStore` implementation that uses the given `impl` object. */
56
+ declare const make: (impl: CassetteStoreService) => CassetteStoreService;
57
+ //#endregion
58
+ export { cassette_store_d_exports as a, make as c, CassetteStoreService as i, parseCassette as l, CassetteStoreError as n, createEmptyCassette as o, CassetteStoreOperation as r, createEmptyCassetteFile as s, CassetteStore as t, toStoreError as u };
59
+ //# sourceMappingURL=cassette-store-BunkvLoV.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { t as __export } from "./chunk-Bp6m_JJh.js";
2
+ import { Context, Data, DateTime, Effect } from "effect";
3
+
4
+ //#region src/cassette-store.ts
5
+ var cassette_store_exports = /* @__PURE__ */ __export({
6
+ CassetteStore: () => CassetteStore,
7
+ CassetteStoreError: () => CassetteStoreError,
8
+ createEmptyCassette: () => createEmptyCassette,
9
+ createEmptyCassetteFile: () => createEmptyCassetteFile,
10
+ make: () => make,
11
+ parseCassette: () => parseCassette,
12
+ toStoreError: () => toStoreError
13
+ });
14
+ /**
15
+ * Effect service tag for the cassette store.
16
+ */
17
+ var CassetteStore = class extends Context.Service()("@useairfoil/effect-vcr/CassetteStore") {};
18
+ /**
19
+ * Creates a new empty cassette with a timestamp and format version.
20
+ */
21
+ const createEmptyCassette = Effect.fnUntraced(function* () {
22
+ const now = yield* DateTime.now;
23
+ return {
24
+ meta: {
25
+ createdAt: DateTime.formatIso(now),
26
+ version: "1"
27
+ },
28
+ entries: {}
29
+ };
30
+ });
31
+ const createEmptyCassetteFile = () => Effect.map(createEmptyCassette(), (cassette) => ({ exports: { default: cassette } }));
32
+ /**
33
+ * Structured error for cassette persistence operations.
34
+ *
35
+ * `operation` indicates which store function failed.
36
+ * `path` is the file or directory path that was targeted.
37
+ */
38
+ var CassetteStoreError = class extends Data.TaggedError("CassetteStoreError") {};
39
+ const toStoreError = (operation, path, cause, message) => new CassetteStoreError({
40
+ operation,
41
+ path,
42
+ cause,
43
+ message
44
+ });
45
+ /**
46
+ * Parses cassette JSON and maps invalid JSON to a store error.
47
+ */
48
+ const parseCassette = (content, path) => Effect.try({
49
+ try: () => JSON.parse(content),
50
+ catch: (error) => toStoreError("load", path, error, "Invalid JSON")
51
+ }).pipe(Effect.flatMap((parsed) => {
52
+ if (!parsed || typeof parsed !== "object" || !("exports" in parsed)) return Effect.fail(toStoreError("load", path, void 0, "Invalid cassette file format"));
53
+ return Effect.succeed(parsed);
54
+ }));
55
+ /** Creates a `CassetteStore` implementation that uses the given `impl` object. */
56
+ const make = (impl) => CassetteStore.of({ ...impl });
57
+
58
+ //#endregion
59
+ export { createEmptyCassetteFile as a, toStoreError as c, createEmptyCassette as i, CassetteStoreError as n, make as o, cassette_store_exports as r, parseCassette as s, CassetteStore as t };
60
+ //# sourceMappingURL=cassette-store-CnXX5OLB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cassette-store-CnXX5OLB.js","names":[],"sources":["../src/cassette-store.ts"],"sourcesContent":["import { Context, Data, DateTime, Effect } from \"effect\";\n\nimport type { Cassette, CassetteFile } from \"./types\";\n\nexport type CassetteStoreOperation = \"exists\" | \"load\" | \"save\" | \"loadOrInit\" | \"resolveRoot\";\n\n/**\n * Storage contract for VCR cassettes.\n *\n */\nexport interface CassetteStoreService {\n /** Returns true if a cassette with the given name exists. */\n readonly exists: (name: string) => Effect.Effect<boolean, CassetteStoreError>;\n\n /** Loads a cassette by name, returning a `CassetteFile` if found. */\n readonly load: (name: string) => Effect.Effect<CassetteFile, CassetteStoreError>;\n\n /** Saves a cassette by name, overwriting any existing cassette with the same name. */\n readonly save: (name: string, content: CassetteFile) => Effect.Effect<void, CassetteStoreError>;\n\n /** Loads a cassette by name, initializing a new empty cassette if not found. */\n readonly loadOrInit: (name: string) => Effect.Effect<CassetteFile, CassetteStoreError>;\n}\n\n/**\n * Effect service tag for the cassette store.\n */\nexport class CassetteStore extends Context.Service<CassetteStore, CassetteStoreService>()(\n \"@useairfoil/effect-vcr/CassetteStore\",\n) {}\n\n/**\n * Creates a new empty cassette with a timestamp and format version.\n */\nexport const createEmptyCassette = Effect.fnUntraced(function* (): Effect.fn.Return<Cassette> {\n const now = yield* DateTime.now;\n return {\n meta: {\n createdAt: DateTime.formatIso(now),\n version: \"1\",\n },\n entries: {},\n };\n});\n\nexport const createEmptyCassetteFile = (): Effect.Effect<CassetteFile> =>\n Effect.map(createEmptyCassette(), (cassette) => ({\n exports: { default: cassette },\n }));\n\n/**\n * Structured error for cassette persistence operations.\n *\n * `operation` indicates which store function failed.\n * `path` is the file or directory path that was targeted.\n */\nexport class CassetteStoreError extends Data.TaggedError(\"CassetteStoreError\")<{\n readonly operation: CassetteStoreOperation;\n readonly path: string;\n readonly message?: string;\n readonly cause?: unknown;\n}> {}\n\nexport const toStoreError = (\n operation: CassetteStoreOperation,\n path: string,\n cause?: unknown,\n message?: string,\n) =>\n new CassetteStoreError({\n operation,\n path,\n cause,\n message,\n });\n\n/**\n * Parses cassette JSON and maps invalid JSON to a store error.\n */\nexport const parseCassette = (content: string, path: string) =>\n Effect.try({\n try: () => JSON.parse(content) as CassetteFile,\n catch: (error) => toStoreError(\"load\", path, error, \"Invalid JSON\"),\n }).pipe(\n Effect.flatMap((parsed) => {\n if (!parsed || typeof parsed !== \"object\" || !(\"exports\" in parsed)) {\n return Effect.fail(toStoreError(\"load\", path, undefined, \"Invalid cassette file format\"));\n }\n return Effect.succeed(parsed);\n }),\n );\n\n/** Creates a `CassetteStore` implementation that uses the given `impl` object. */\nexport const make = (impl: CassetteStoreService): CassetteStoreService =>\n CassetteStore.of({ ...impl });\n"],"mappings":";;;;;;;;;;;;;;;;AA2BA,IAAa,gBAAb,cAAmC,QAAQ,SAA8C,CACvF,uCACD,CAAC;;;;AAKF,MAAa,sBAAsB,OAAO,WAAW,aAAyC;CAC5F,MAAM,MAAM,OAAO,SAAS;AAC5B,QAAO;EACL,MAAM;GACJ,WAAW,SAAS,UAAU,IAAI;GAClC,SAAS;GACV;EACD,SAAS,EAAE;EACZ;EACD;AAEF,MAAa,gCACX,OAAO,IAAI,qBAAqB,GAAG,cAAc,EAC/C,SAAS,EAAE,SAAS,UAAU,EAC/B,EAAE;;;;;;;AAQL,IAAa,qBAAb,cAAwC,KAAK,YAAY,qBAAqB,CAK3E;AAEH,MAAa,gBACX,WACA,MACA,OACA,YAEA,IAAI,mBAAmB;CACrB;CACA;CACA;CACA;CACD,CAAC;;;;AAKJ,MAAa,iBAAiB,SAAiB,SAC7C,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,QAAQ;CAC9B,QAAQ,UAAU,aAAa,QAAQ,MAAM,OAAO,eAAe;CACpE,CAAC,CAAC,KACD,OAAO,SAAS,WAAW;AACzB,KAAI,CAAC,UAAU,OAAO,WAAW,YAAY,EAAE,aAAa,QAC1D,QAAO,OAAO,KAAK,aAAa,QAAQ,MAAM,QAAW,+BAA+B,CAAC;AAE3F,QAAO,OAAO,QAAQ,OAAO;EAC7B,CACH;;AAGH,MAAa,QAAQ,SACnB,cAAc,GAAG,EAAE,GAAG,MAAM,CAAC"}
@@ -0,0 +1,3 @@
1
+ import "./types-DWwkMzxG.js";
2
+ import { c as make, i as CassetteStoreService, l as parseCassette, n as CassetteStoreError, o as createEmptyCassette, r as CassetteStoreOperation, s as createEmptyCassetteFile, t as CassetteStore, u as toStoreError } from "./cassette-store-BunkvLoV.js";
3
+ export { CassetteStore, CassetteStoreError, CassetteStoreOperation, CassetteStoreService, createEmptyCassette, createEmptyCassetteFile, make, parseCassette, toStoreError };
@@ -0,0 +1,3 @@
1
+ import { a as createEmptyCassetteFile, c as toStoreError, i as createEmptyCassette, n as CassetteStoreError, o as make, s as parseCassette, t as CassetteStore } from "./cassette-store-CnXX5OLB.js";
2
+
3
+ export { CassetteStore, CassetteStoreError, createEmptyCassette, createEmptyCassetteFile, make, parseCassette, toStoreError };
@@ -0,0 +1,19 @@
1
+ import { n as CassetteStoreError, t as CassetteStore } from "./cassette-store-BunkvLoV.js";
2
+ import { FileSystem, Layer, Path } from "effect";
3
+
4
+ //#region src/file-system-cassette-store.d.ts
5
+ declare namespace file_system_cassette_store_d_exports {
6
+ export { FileSystemCassetteStoreConfig, layer };
7
+ }
8
+ type FileSystemCassetteStoreConfig = {
9
+ readonly cassetteDir?: string;
10
+ };
11
+ /**
12
+ * FileSystem-backed cassette store.
13
+ *
14
+ * Requires a platform FileSystem layer (Node/Bun) to be provided by the user.
15
+ */
16
+ declare const layer: (config?: FileSystemCassetteStoreConfig) => Layer.Layer<CassetteStore, CassetteStoreError, Path.Path | FileSystem.FileSystem>;
17
+ //#endregion
18
+ export { file_system_cassette_store_d_exports as n, layer as r, FileSystemCassetteStoreConfig as t };
19
+ //# sourceMappingURL=file-system-cassette-store-LXpwwrw3.d.ts.map
@@ -0,0 +1,42 @@
1
+ import { t as __export } from "./chunk-Bp6m_JJh.js";
2
+ import { a as createEmptyCassetteFile, c as toStoreError, o as make, s as parseCassette, t as CassetteStore } from "./cassette-store-CnXX5OLB.js";
3
+ import { t as getVitestState } from "./vitest-state-BOynp4Va.js";
4
+ import { Effect, FileSystem, Layer, Path } from "effect";
5
+
6
+ //#region src/file-system-cassette-store.ts
7
+ var file_system_cassette_store_exports = /* @__PURE__ */ __export({ layer: () => layer });
8
+ /**
9
+ * FileSystem-backed cassette store.
10
+ *
11
+ * Requires a platform FileSystem layer (Node/Bun) to be provided by the user.
12
+ */
13
+ const layer = (config = {}) => Layer.effect(CassetteStore)(Effect.gen(function* () {
14
+ const fs = yield* FileSystem.FileSystem;
15
+ const path = yield* Path.Path;
16
+ let rootDir;
17
+ if (config.cassetteDir) rootDir = config.cassetteDir;
18
+ else {
19
+ const { testPath } = getVitestState();
20
+ if (!testPath) return yield* Effect.fail(toStoreError("resolveRoot", "__cassettes__", void 0, "VCR cassette directory could not be inferred. Provide cassetteDir when not running in Vitest."));
21
+ const testFolder = path.dirname(testPath);
22
+ rootDir = path.join(testFolder, "__cassettes__");
23
+ }
24
+ const fullPath = (name) => path.join(rootDir, name);
25
+ const load = (name) => fs.readFileString(fullPath(name)).pipe(Effect.mapError((error) => toStoreError("load", name, error)), Effect.flatMap((content) => parseCassette(content, name)));
26
+ const save = (name, cassette) => {
27
+ const filePath = fullPath(name);
28
+ const dir = path.dirname(filePath);
29
+ return (dir === "" ? Effect.void : fs.makeDirectory(dir, { recursive: true }).pipe(Effect.mapError((error) => toStoreError("save", dir, error)))).pipe(Effect.andThen(fs.writeFileString(filePath, JSON.stringify(cassette, null, 2)).pipe(Effect.mapError((error) => toStoreError("save", name, error)))));
30
+ };
31
+ const loadOrInit = (name) => fs.exists(fullPath(name)).pipe(Effect.mapError((error) => toStoreError("loadOrInit", name, error)), Effect.flatMap((exists) => exists ? load(name) : createEmptyCassetteFile().pipe(Effect.tap((cassette) => save(name, cassette)))));
32
+ return make({
33
+ exists: (name) => fs.exists(fullPath(name)).pipe(Effect.mapError((error) => toStoreError("exists", name, error))),
34
+ load,
35
+ save,
36
+ loadOrInit
37
+ });
38
+ }));
39
+
40
+ //#endregion
41
+ export { layer as n, file_system_cassette_store_exports as t };
42
+ //# sourceMappingURL=file-system-cassette-store-zWkrhhlO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-system-cassette-store-zWkrhhlO.js","names":["CassetteStore.CassetteStore","rootDir: string","CassetteStore.toStoreError","CassetteStore.parseCassette","CassetteStore.createEmptyCassetteFile","CassetteStore.make"],"sources":["../src/file-system-cassette-store.ts"],"sourcesContent":["import { Effect, Layer, FileSystem, Path } from \"effect\";\n\nimport type { CassetteFile } from \"./types\";\n\nimport * as CassetteStore from \"./cassette-store\";\nimport { getVitestState } from \"./vitest-state\";\n\nexport type FileSystemCassetteStoreConfig = {\n readonly cassetteDir?: string;\n};\n\n/**\n * FileSystem-backed cassette store.\n *\n * Requires a platform FileSystem layer (Node/Bun) to be provided by the user.\n */\nexport const layer = (config: FileSystemCassetteStoreConfig = {}) =>\n Layer.effect(CassetteStore.CassetteStore)(\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n\n let rootDir: string;\n if (config.cassetteDir) {\n rootDir = config.cassetteDir;\n } else {\n const { testPath } = getVitestState();\n if (!testPath) {\n return yield* Effect.fail(\n CassetteStore.toStoreError(\n \"resolveRoot\",\n \"__cassettes__\",\n undefined,\n \"VCR cassette directory could not be inferred. Provide cassetteDir when not running in Vitest.\",\n ),\n );\n }\n const testFolder = path.dirname(testPath);\n rootDir = path.join(testFolder, \"__cassettes__\");\n }\n\n const fullPath = (name: string) => path.join(rootDir, name);\n\n const load = (name: string) =>\n fs.readFileString(fullPath(name)).pipe(\n Effect.mapError((error) => CassetteStore.toStoreError(\"load\", name, error)),\n Effect.flatMap((content) => CassetteStore.parseCassette(content, name)),\n );\n\n const save = (name: string, cassette: CassetteFile) => {\n const filePath = fullPath(name);\n const dir = path.dirname(filePath);\n const ensureDir =\n dir === \"\"\n ? Effect.void\n : fs\n .makeDirectory(dir, { recursive: true })\n .pipe(Effect.mapError((error) => CassetteStore.toStoreError(\"save\", dir, error)));\n return ensureDir.pipe(\n Effect.andThen(\n fs\n .writeFileString(filePath, JSON.stringify(cassette, null, 2))\n .pipe(Effect.mapError((error) => CassetteStore.toStoreError(\"save\", name, error))),\n ),\n );\n };\n\n const loadOrInit = (name: string) =>\n fs.exists(fullPath(name)).pipe(\n Effect.mapError((error) => CassetteStore.toStoreError(\"loadOrInit\", name, error)),\n Effect.flatMap((exists) =>\n exists\n ? load(name)\n : CassetteStore.createEmptyCassetteFile().pipe(\n Effect.tap((cassette) => save(name, cassette)),\n ),\n ),\n );\n\n return CassetteStore.make({\n exists: (name: string) =>\n fs\n .exists(fullPath(name))\n .pipe(Effect.mapError((error) => CassetteStore.toStoreError(\"exists\", name, error))),\n load,\n save,\n loadOrInit,\n });\n }),\n );\n"],"mappings":";;;;;;;;;;;;AAgBA,MAAa,SAAS,SAAwC,EAAE,KAC9D,MAAM,OAAOA,cAA4B,CACvC,OAAO,IAAI,aAAa;CACtB,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CAEzB,IAAIC;AACJ,KAAI,OAAO,YACT,WAAU,OAAO;MACZ;EACL,MAAM,EAAE,aAAa,gBAAgB;AACrC,MAAI,CAAC,SACH,QAAO,OAAO,OAAO,KACnBC,aACE,eACA,iBACA,QACA,gGACD,CACF;EAEH,MAAM,aAAa,KAAK,QAAQ,SAAS;AACzC,YAAU,KAAK,KAAK,YAAY,gBAAgB;;CAGlD,MAAM,YAAY,SAAiB,KAAK,KAAK,SAAS,KAAK;CAE3D,MAAM,QAAQ,SACZ,GAAG,eAAe,SAAS,KAAK,CAAC,CAAC,KAChC,OAAO,UAAU,UAAUA,aAA2B,QAAQ,MAAM,MAAM,CAAC,EAC3E,OAAO,SAAS,YAAYC,cAA4B,SAAS,KAAK,CAAC,CACxE;CAEH,MAAM,QAAQ,MAAc,aAA2B;EACrD,MAAM,WAAW,SAAS,KAAK;EAC/B,MAAM,MAAM,KAAK,QAAQ,SAAS;AAOlC,UALE,QAAQ,KACJ,OAAO,OACP,GACG,cAAc,KAAK,EAAE,WAAW,MAAM,CAAC,CACvC,KAAK,OAAO,UAAU,UAAUD,aAA2B,QAAQ,KAAK,MAAM,CAAC,CAAC,EACxE,KACf,OAAO,QACL,GACG,gBAAgB,UAAU,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC,CAC5D,KAAK,OAAO,UAAU,UAAUA,aAA2B,QAAQ,MAAM,MAAM,CAAC,CAAC,CACrF,CACF;;CAGH,MAAM,cAAc,SAClB,GAAG,OAAO,SAAS,KAAK,CAAC,CAAC,KACxB,OAAO,UAAU,UAAUA,aAA2B,cAAc,MAAM,MAAM,CAAC,EACjF,OAAO,SAAS,WACd,SACI,KAAK,KAAK,GACVE,yBAAuC,CAAC,KACtC,OAAO,KAAK,aAAa,KAAK,MAAM,SAAS,CAAC,CAC/C,CACN,CACF;AAEH,QAAOC,KAAmB;EACxB,SAAS,SACP,GACG,OAAO,SAAS,KAAK,CAAC,CACtB,KAAK,OAAO,UAAU,UAAUH,aAA2B,UAAU,MAAM,MAAM,CAAC,CAAC;EACxF;EACA;EACA;EACD,CAAC;EACF,CACH"}
@@ -0,0 +1,4 @@
1
+ import "./types-DWwkMzxG.js";
2
+ import "./cassette-store-BunkvLoV.js";
3
+ import { r as layer, t as FileSystemCassetteStoreConfig } from "./file-system-cassette-store-LXpwwrw3.js";
4
+ export { FileSystemCassetteStoreConfig, layer };
@@ -0,0 +1,5 @@
1
+ import "./cassette-store-CnXX5OLB.js";
2
+ import "./vitest-state-BOynp4Va.js";
3
+ import { n as layer } from "./file-system-cassette-store-zWkrhhlO.js";
4
+
5
+ export { layer };