Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strongly-typed-load-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"counterfact": minor
---

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`.
4 changes: 2 additions & 2 deletions docs/features/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ A scenario function receives a single argument with `{ context, loadContext, rou

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

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

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

## See also

Expand Down
2 changes: 1 addition & 1 deletion src/server/context-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class ContextRegistry extends EventTarget {

/**
* Removes the context entry for the given path and dispatches a
* "context-changed" event so that listeners (e.g. the scenario-context type
* "context-changed" event so that listeners (e.g. the _.context type
* generator) can regenerate type files in response to the removal.
*
* @param path - The route path whose context entry should be deleted
Expand Down
10 changes: 8 additions & 2 deletions src/typescript-generator/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ function buildApplyContextContent(contextFiles: ContextFileInfo[]): string {
"/** A scenario function that receives the live REPL environment */",
"export type Scenario = ($: ApplyContext) => Promise<void> | void;",
"",
"/** Interface for Context objects defined in _.context.ts files */",
"export interface BaseContext {",
' readonly loadContext: ApplyContext["loadContext"];',
" readonly readJson: (relativePath: string) => Promise<unknown>;",
"}",
"",
];

return parts.join("\n");
Expand All @@ -265,7 +271,7 @@ export async function writeApplyContextType(
destination: string,
): Promise<void> {
const typesDir = nodePath.join(destination, "types");
const filePath = nodePath.join(typesDir, "scenario-context.ts");
const filePath = nodePath.join(typesDir, "_.context.ts");

const contextFiles = await collectContextFiles(destination);
const content = buildApplyContextContent(contextFiles);
Expand All @@ -274,7 +280,7 @@ export async function writeApplyContextType(
await fs.writeFile(filePath, content, "utf8");
}

const DEFAULT_SCENARIOS_INDEX = `import type { Scenario } from "../types/scenario-context.js";
const DEFAULT_SCENARIOS_INDEX = `import type { Scenario } from "../types/_.context.js";

/**
* Scenario scripts are plain TypeScript functions that receive the live REPL
Expand Down
12 changes: 10 additions & 2 deletions src/typescript-generator/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ export class Repository {

await fs.writeFile(
contextFilePath,
`/**
`import type { BaseContext } from "../types/_.context.js";

/**
* This is the default context for Counterfact.
*
* It defines the context object in the REPL
Expand All @@ -158,8 +160,14 @@ export class Repository {
*
* See https://counterfact.dev/docs/usage.html#working-with-state-the-codecontextcode-object-and-codecontexttscode
*/
export class Context {
export class Context implements BaseContext {
readonly loadContext: BaseContext["loadContext"];
readonly readJson: BaseContext["readJson"];

constructor({ loadContext, readJson }: BaseContext) {
this.loadContext = loadContext;
this.readJson = readJson;
}
}
`,
);
Expand Down
19 changes: 13 additions & 6 deletions test/typescript-generator/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ describe("end-to-end test", () => {
});
});

describe("scenario-context type generation", () => {
it("generates a fallback scenario-context.ts when no routes directory exists", async () => {
describe("_.context type generation", () => {
it("generates a fallback _.context.ts when no routes directory exists", async () => {
await usingTemporaryFiles(async ($) => {
const basePath = $.path("");
const repository = new Repository();
Expand All @@ -71,7 +71,7 @@ describe("scenario-context type generation", () => {

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

const content = await $.read("types/scenario-context.ts");
const content = await $.read("types/_.context.ts");
expect(content).toContain("context: Record<string, unknown>");
expect(content).toContain(
"loadContext(path: string): Record<string, unknown>;",
Expand All @@ -80,6 +80,13 @@ describe("scenario-context type generation", () => {
expect(content).toContain(
"export type Scenario = ($: ApplyContext) => Promise<void> | void;",
);
expect(content).toContain("export interface BaseContext {");
expect(content).toContain(
' readonly loadContext: ApplyContext["loadContext"];',
);
expect(content).toContain(
" readonly readJson: (relativePath: string) => Promise<unknown>;",
);
});
});

Expand All @@ -96,7 +103,7 @@ describe("scenario-context type generation", () => {

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

const content = await $.read("types/scenario-context.ts");
const content = await $.read("types/_.context.ts");
expect(content).toContain(
'import type { Context } from "../routes/_.context";',
);
Expand Down Expand Up @@ -124,7 +131,7 @@ describe("scenario-context type generation", () => {

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

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

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

const content = await $.read("types/scenario-context.ts");
const content = await $.read("types/_.context.ts");
expect(content).toContain(
'import type { Context as PetsPetIdContext } from "../routes/pets/{petId}/_.context";',
);
Expand Down
Loading