Skip to content

Commit bc4da17

Browse files
authored
1 parent e2f6c4d commit bc4da17

File tree

6 files changed

+27
-27
lines changed

6 files changed

+27
-27
lines changed

docs/adr/001-apply-command-with-function-injection.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,29 @@ Three designs were proposed as working documents in `.github/issue-proposals/`:
2727

2828
**Solution 1 (Minimalist Function Injection) is selected.**
2929

30-
A scenario script is a TypeScript file with one or more named function exports. When `.scenario <path>` is run, Counterfact splits the argument on `/`, uses the last segment as the function name and the rest as the file path (relative to `<basePath>/repl/`), dynamically imports the module, and calls the named function with a live `ApplyContext` (`$`) object:
30+
A scenario script is a TypeScript file with one or more named function exports. When `.scenario <path>` is run, Counterfact splits the argument on `/`, uses the last segment as the function name and the rest as the file path (relative to `<basePath>/repl/`), dynamically imports the module, and calls the named function with a live `Scenario$` (`$`) object:
3131

3232
```ts
3333
// repl/sold-pets.ts
34-
import type { ApplyContext } from "counterfact";
34+
import type { Scenario$ } from "../types/scenario-context.js";
3535

36-
export function soldPets($: ApplyContext) {
36+
export function soldPets($: Scenario$) {
3737
$.context.petService.reset();
3838
$.context.petService.addPet({ id: 1, status: "sold" });
3939

4040
$.routes.getSoldPets = $.route("/pet/findByStatus").method("get").query({ status: "sold" });
4141
}
4242
```
4343

44-
`ApplyContext` exposes `{ context, loadContext, routes, route }`. After the function returns, Counterfact diffs the `routes` object and prints a summary of what was added or removed.
44+
`Scenario$` exposes `{ context, loadContext, routes, route }`. After the function returns, Counterfact diffs the `routes` object and prints a summary of what was added or removed.
4545

4646
Solution 1 was chosen because it introduces the smallest possible API surface, imposes no structural requirements on script authors, and integrates naturally with TypeScript `import` for composability. It is the right foundation to build on before adding lifecycle or tracking features.
4747

4848
## Options
4949

5050
### Solution 1: Minimalist Function Injection (selected)
5151

52-
Scripts export named functions that receive `$: ApplyContext`. Counterfact resolves the file/function from the path argument and calls the function directly. Route changes are diffed and reported; context changes are not automatically tracked.
52+
Scripts export named functions that receive `$: Scenario$`. Counterfact resolves the file/function from the path argument and calls the function directly. Route changes are diffed and reported; context changes are not automatically tracked.
5353

5454
**Why chosen:** Maximum simplicity. No new abstractions, no required boilerplate. Easy to implement, test, and understand. Composability via normal `import`.
5555

@@ -89,7 +89,7 @@ Identical surface syntax to Solution 1, but Counterfact wraps `context` and `rou
8989

9090
- Evaluate whether `teardown` support (from Solution 2) is needed and, if so, define a clean extension point.
9191
- Explore adding proxy-based context diffing (from Solution 3) as an opt-in enhancement once the core command is stable.
92-
- Define `ApplyContext` as a public exported type in `counterfact-types/`.
92+
- Define `Scenario$` as a public exported type in `counterfact-types/`.
9393

9494
## Advice
9595

docs/features/repl.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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/scenario-context.ts` when you run Counterfact with type generation enabled.
102102

103103
## Startup scenario
104104

@@ -130,9 +130,9 @@ export const startup: Scenario = ($) => {
130130

131131
```ts
132132
// scenarios/pets.ts
133-
import type { ApplyContext } from "../types/scenario-context.js";
133+
import type { Scenario$ } from "../types/scenario-context.js";
134134

135-
export function addPets($: ApplyContext, count: number, species: string) {
135+
export function addPets($: Scenario$, count: number, species: string) {
136136
for (let i = 0; i < count; i++) {
137137
$.context.addPet({ name: `${species} ${i + 1}`, status: "available", photoUrls: [] });
138138
}

docs/patterns/scenario-scripts.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ export const startup: Scenario = ($) => {
8787

8888
```ts
8989
// scenarios/pets.ts
90-
import type { ApplyContext } from "../types/scenario-context.js";
90+
import type { Scenario$ } from "../types/scenario-context.js";
9191

92-
export function addPets($: ApplyContext, count: number, species: string) {
92+
export function addPets($: Scenario$, count: number, species: string) {
9393
for (let i = 0; i < count; i++) {
9494
$.context.addPet({
9595
name: `${species} ${i + 1}`,
@@ -102,9 +102,9 @@ export function addPets($: ApplyContext, count: number, species: string) {
102102

103103
```ts
104104
// scenarios/orders.ts
105-
import type { ApplyContext } from "../types/scenario-context.js";
105+
import type { Scenario$ } from "../types/scenario-context.js";
106106

107-
export function addOrders($: ApplyContext, count: number) {
107+
export function addOrders($: Scenario$, count: number) {
108108
for (let i = 0; i < count; i++) {
109109
$.context.addOrder({ petId: i + 1, quantity: 1, status: "placed" });
110110
}

src/app.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import { Registry } from "./server/registry.js";
1616
import { ScenarioRegistry } from "./server/scenario-registry.js";
1717
import { Transpiler } from "./server/transpiler.js";
1818
import { CodeGenerator } from "./typescript-generator/code-generator.js";
19-
import { writeApplyContextType } from "./typescript-generator/generate.js";
19+
import { writeScenarioContextType } from "./typescript-generator/generate.js";
2020
import { runtimeCanExecuteErasableTs } from "./util/runtime-can-execute-erasable-ts.js";
2121

2222
export { loadOpenApiDocument } from "./server/load-openapi-document.js";
2323

24-
type ApplyContext = {
24+
type Scenario$ = {
2525
context: Record<string, unknown>;
2626
loadContext: (path: string) => Record<string, unknown>;
2727
route: (path: string) => unknown;
@@ -40,16 +40,16 @@ export async function runStartupScenario(
4040
return;
4141
}
4242

43-
const applyContext: ApplyContext = {
43+
const scenario$: Scenario$ = {
4444
context: contextRegistry.find("/") as Record<string, unknown>,
4545
loadContext: (path: string) =>
4646
contextRegistry.find(path) as Record<string, unknown>,
4747
route: createRouteFunction(config.port, "localhost", openApiDocument),
4848
routes: {},
4949
};
5050

51-
await (indexModule["startup"] as (ctx: ApplyContext) => Promise<void> | void)(
52-
applyContext,
51+
await (indexModule["startup"] as (ctx: Scenario$) => Promise<void> | void)(
52+
scenario$,
5353
);
5454
}
5555

@@ -221,7 +221,7 @@ export async function counterfact(config: Config) {
221221
);
222222

223223
contextRegistry.addEventListener("context-changed", () => {
224-
void writeApplyContextType(modulesPath);
224+
void writeScenarioContextType(modulesPath);
225225
});
226226

227227
const middleware = koaMiddleware(dispatcher, config);

src/typescript-generator/generate.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export async function generate(
146146
debug("finished writing the files");
147147

148148
if (generateOptions.types) {
149-
await writeApplyContextType(destination);
149+
await writeScenarioContextType(destination);
150150
await writeDefaultScenariosIndex(destination);
151151
}
152152
}
@@ -251,7 +251,7 @@ function buildLoadContextOverload(routePath: string, alias: string): string {
251251
return ` loadContext(path: \`${templatePath}\`): ${alias};`;
252252
}
253253

254-
function buildApplyContextContent(contextFiles: ContextFileInfo[]): string {
254+
function buildScenarioContextContent(contextFiles: ContextFileInfo[]): string {
255255
const rootContext = contextFiles.find((f) => f.routePath === "/");
256256
const contextType = rootContext
257257
? rootContext.alias
@@ -271,7 +271,7 @@ function buildApplyContextContent(contextFiles: ContextFileInfo[]): string {
271271
"// This file is generated by Counterfact. Do not edit manually.",
272272
...importLines,
273273
"",
274-
"export interface ApplyContext {",
274+
"export interface Scenario$ {",
275275
' /** Root context, same as loadContext("/") */',
276276
` context: ${contextType};`,
277277
" /** Load a context object for a specific path */",
@@ -284,7 +284,7 @@ function buildApplyContextContent(contextFiles: ContextFileInfo[]): string {
284284
"}",
285285
"",
286286
"/** A scenario function that receives the live REPL environment */",
287-
"export type Scenario = ($: ApplyContext) => Promise<void> | void;",
287+
"export type Scenario = ($: Scenario$) => Promise<void> | void;",
288288
"",
289289
];
290290

@@ -293,22 +293,22 @@ function buildApplyContextContent(contextFiles: ContextFileInfo[]): string {
293293

294294
/**
295295
* Writes the `types/scenario-context.ts` file, which exports the
296-
* `ApplyContext` interface used to type scenario functions.
296+
* `Scenario$` interface used to type scenario functions.
297297
*
298298
* The interface is generated from all `_.context.ts` files found under the
299299
* `routes/` directory, providing strongly typed `loadContext()` overloads for
300300
* every route path that has a context file.
301301
*
302302
* @param destination - Root output directory.
303303
*/
304-
export async function writeApplyContextType(
304+
export async function writeScenarioContextType(
305305
destination: string,
306306
): Promise<void> {
307307
const typesDir = nodePath.join(destination, "types");
308308
const filePath = nodePath.join(typesDir, "scenario-context.ts");
309309

310310
const contextFiles = await collectContextFiles(destination);
311-
const content = buildApplyContextContent(contextFiles);
311+
const content = buildScenarioContextContent(contextFiles);
312312

313313
await fs.mkdir(typesDir, { recursive: true });
314314
await fs.writeFile(filePath, content, "utf8");

test/typescript-generator/generate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe("scenario-context type generation", () => {
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;",
8282
);
8383
});
8484
});

0 commit comments

Comments
 (0)