Skip to content

Commit 461dd93

Browse files
authored
Merge branch 'main' into copilot/pass-openapi-document-to-context
2 parents 023f8b7 + e9bf98a commit 461dd93

File tree

6 files changed

+63
-34
lines changed

6 files changed

+63
-34
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": minor
3+
---
4+
5+
Export `ContextArgs` type from generated `types/_.context.ts` so that `_.context.ts` files can strongly type the `loadContext` and `readJson` parameters received in the Context constructor. The default `_.context.ts` template now imports and uses `ContextArgs`.

docs/features/repl.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ A scenario function receives a single argument with `{ context, loadContext, rou
7676

7777
```ts
7878
// scenarios/index.ts
79-
import type { Scenario } from "../types/scenario-context.js";
79+
import type { Scenario } from "../types/_.context.js";
8080

8181
export const soldPets: Scenario = ($) => {
8282
// Mutate context directly — same as typing in the REPL
@@ -98,7 +98,7 @@ After the command runs you can immediately use anything stored in `$.routes`:
9898
> routes.findSold.send()
9999
```
100100

101-
The `Scenario` type and `ApplyContext` interface are generated automatically into `types/scenario-context.ts` when you run Counterfact with type generation enabled.
101+
The `Scenario` type and `Scenario$` interface are generated automatically into `types/_.context.ts` when you run Counterfact with type generation enabled.
102102

103103
## See also
104104

src/server/context-registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export class ContextRegistry extends EventTarget {
107107

108108
/**
109109
* Removes the context entry for the given path and dispatches a
110-
* "context-changed" event so that listeners (e.g. the scenario-context type
110+
* "context-changed" event so that listeners (e.g. the _.context type
111111
* generator) can regenerate type files in response to the removal.
112112
*
113113
* @param path - The route path whose context entry should be deleted

src/typescript-generator/generate.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -271,29 +271,41 @@ function buildApplyContextContent(contextFiles: ContextFileInfo[]): string {
271271
"// This file is generated by Counterfact. Do not edit manually.",
272272
...importLines,
273273
"",
274-
"export interface ApplyContext {",
275-
' /** Root context, same as loadContext("/") */',
276-
` context: ${contextType};`,
277-
" /** Load a context object for a specific path */",
274+
"interface LoadContextDefinitions {",
275+
" /* code generator adds additional signatures here */",
278276
...overloadLines,
279277
" loadContext(path: string): Record<string, unknown>;",
278+
"}",
279+
"",
280+
"export interface Scenario$ {",
281+
' /** Root context, same as loadContext("/") */',
282+
` readonly context: ${contextType};`,
283+
' readonly loadContext: LoadContextDefinitions["loadContext"];',
280284
" /** Named route builders stored in the REPL execution context */",
281-
" routes: Record<string, unknown>;",
285+
" readonly routes: Record<string, unknown>;",
282286
" /** Create a new route builder for a given path */",
283-
" route: (path: string) => unknown;",
287+
" readonly route: (path: string) => unknown;",
284288
"}",
285289
"",
286290
"/** A scenario function that receives the live REPL environment */",
287-
"export type Scenario = ($: ApplyContext) => Promise<void> | void;",
291+
"export type Scenario = ($: Scenario$) => Promise<void> | void;",
292+
"",
293+
"/** Interface for Context objects defined in _.context.ts files */",
294+
"export interface Context$ {",
295+
" /** Load a context object for a specific path */",
296+
' readonly loadContext: LoadContextDefinitions["loadContext"];',
297+
" /** Load a JSON file relative to this file's path */",
298+
" readonly readJson: (relativePath: string) => Promise<unknown>;",
299+
"}",
288300
"",
289301
];
290302

291303
return parts.join("\n");
292304
}
293305

294306
/**
295-
* Writes the `types/scenario-context.ts` file, which exports the
296-
* `ApplyContext` interface used to type scenario functions.
307+
* Writes the `types/_.context.ts` file, which exports the
308+
* `Scenario$` interface used to type scenario functions.
297309
*
298310
* The interface is generated from all `_.context.ts` files found under the
299311
* `routes/` directory, providing strongly typed `loadContext()` overloads for
@@ -305,7 +317,7 @@ export async function writeApplyContextType(
305317
destination: string,
306318
): Promise<void> {
307319
const typesDir = nodePath.join(destination, "types");
308-
const filePath = nodePath.join(typesDir, "scenario-context.ts");
320+
const filePath = nodePath.join(typesDir, "_.context.ts");
309321

310322
const contextFiles = await collectContextFiles(destination);
311323
const content = buildApplyContextContent(contextFiles);
@@ -314,7 +326,7 @@ export async function writeApplyContextType(
314326
await fs.writeFile(filePath, content, "utf8");
315327
}
316328

317-
const DEFAULT_SCENARIOS_INDEX = `import type { Scenario } from "../types/scenario-context.js";
329+
const DEFAULT_SCENARIOS_INDEX = `import type { Scenario } from "../types/_.context.js";
318330
319331
/**
320332
* Scenario scripts are plain TypeScript functions that receive the live REPL

src/typescript-generator/repository.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,23 @@ export class Repository {
189189

190190
await fs.writeFile(
191191
contextFilePath,
192-
`/**
193-
* This is the default context for Counterfact.
194-
*
195-
* It defines the context object in the REPL
196-
* and the $.context object in the code.
197-
*
198-
* Add properties and methods to suit your needs.
199-
*
200-
* See https://counterfact.dev/docs/usage.html#working-with-state-the-codecontextcode-object-and-codecontexttscode
201-
*/
202-
export class Context {
192+
`import type { Context$ } from "../types/_.context.js";
203193
194+
/**
195+
* This is the default context for Counterfact.
196+
*
197+
* It defines the context object in the REPL
198+
* and the $.context object in the code.
199+
*
200+
* Add properties and methods to suit your needs.
201+
*
202+
* See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md
203+
*/
204+
205+
export class Context {
206+
constructor($: Context$) {
207+
void $;
208+
}
204209
}
205210
`,
206211
);

test/typescript-generator/generate.test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ describe("end-to-end test", () => {
5959
});
6060
});
6161

62-
describe("scenario-context type generation", () => {
63-
it("generates a fallback scenario-context.ts when no routes directory exists", async () => {
62+
describe("_.context type generation", () => {
63+
it("generates a fallback _.context.ts when no routes directory exists", async () => {
6464
await usingTemporaryFiles(async ($) => {
6565
const basePath = $.path("");
6666
const repository = new Repository();
@@ -71,14 +71,21 @@ describe("scenario-context type generation", () => {
7171

7272
await generate("./petstore.yaml", basePath, { types: true }, repository);
7373

74-
const content = await $.read("types/scenario-context.ts");
75-
expect(content).toContain("context: Record<string, unknown>");
74+
const content = await $.read("types/_.context.ts");
75+
expect(content).toContain("readonly context: Record<string, unknown>");
7676
expect(content).toContain(
7777
"loadContext(path: string): Record<string, unknown>;",
7878
);
7979
expect(content).not.toContain("import type");
8080
expect(content).toContain(
81-
"export type Scenario = ($: ApplyContext) => Promise<void> | void;",
81+
"export type Scenario = ($: Scenario$) => Promise<void> | void;",
82+
);
83+
expect(content).toContain("export interface Context$ {");
84+
expect(content).toContain(
85+
' readonly loadContext: LoadContextDefinitions["loadContext"];',
86+
);
87+
expect(content).toContain(
88+
" readonly readJson: (relativePath: string) => Promise<unknown>;",
8289
);
8390
});
8491
});
@@ -96,11 +103,11 @@ describe("scenario-context type generation", () => {
96103

97104
await generate("./petstore.yaml", basePath, { types: true }, repository);
98105

99-
const content = await $.read("types/scenario-context.ts");
106+
const content = await $.read("types/_.context.ts");
100107
expect(content).toContain(
101108
'import type { Context } from "../routes/_.context";',
102109
);
103-
expect(content).toContain("context: Context;");
110+
expect(content).toContain("readonly context: Context;");
104111
expect(content).toContain(
105112
'loadContext(path: "/" | `/${string}`): Context;',
106113
);
@@ -124,7 +131,7 @@ describe("scenario-context type generation", () => {
124131

125132
await generate("./petstore.yaml", basePath, { types: true }, repository);
126133

127-
const content = await $.read("types/scenario-context.ts");
134+
const content = await $.read("types/_.context.ts");
128135
expect(content).toContain(
129136
'import type { Context } from "../routes/_.context";',
130137
);
@@ -161,7 +168,7 @@ describe("scenario-context type generation", () => {
161168

162169
await generate("./petstore.yaml", basePath, { types: true }, repository);
163170

164-
const content = await $.read("types/scenario-context.ts");
171+
const content = await $.read("types/_.context.ts");
165172
expect(content).toContain(
166173
'import type { Context as PetsPetIdContext } from "../routes/pets/{petId}/_.context";',
167174
);

0 commit comments

Comments
 (0)