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/giant-badgers-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@interactors/core": minor
---

Use `createMatcher` to matcher declaration
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ jobs:
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- run: deno test -A
- run: |
deno run -A npm:playwright install chromium
deno run -A packages/cli/src/main.ts build
deno test -A
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
.netlify

.vscode
/build/
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"./packages/globals",
"./packages/html",
"./packages/keyboard",
"./packages/material-ui"
"./packages/material-ui",
"./packages/cli"
],
"compilerOptions": {
"lib": ["deno.ns", "esnext", "dom", "dom.iterable", "dom.asynciterable"]
Expand Down
5,513 changes: 2,813 additions & 2,700 deletions deno.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
build
1 change: 1 addition & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @interactors/cli
9 changes: 9 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# @interactors/cli

[![npm](https://img.shields.io/npm/v/@interactors/cli.svg)](https://www.npmjs.com/package/@interactors/cli)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Created by Frontside](https://img.shields.io/badge/created%20by-frontside-26abe8.svg)](https://frontside.com)
[![Chat on Discord](https://img.shields.io/discord/700803887132704931?Label=Discord)](https://discord.gg/mv4uxxcAKd)

A builder tool for agent's script. A special script that is injected in testing environment and exposes interactors.
Learn more at [https://frontside.com/interactors](https://frontside.com/interactors)
22 changes: 22 additions & 0 deletions packages/cli/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@interactors/cli",
"version": "1.0.0",
"exports": "./mod.ts",
"imports": {
"chokidar": "npm:chokidar@^4.0.1",
"effection": "npm:effection@^3.0.3",
"esbuild": "npm:esbuild@^0.24.0",
"playwright": "npm:playwright@^1.49.0",
"yargs": "npm:yargs@^17.7.2",
"zod": "npm:zod@^3.23.8",
"zod-opts": "npm:zod-opts@^0.1.8"
},
"lint": {
"rules": {
"exclude": [
"prefer-const",
"require-yield"
]
}
}
}
79 changes: 79 additions & 0 deletions packages/cli/src/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { call, type Operation } from "effection";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import * as path from "node:path";
import * as esbuild from "esbuild";
import { generateImports } from "./generate-imports.ts";
import { importInteractors } from "./import-interactors.ts";
import { generateConstructors } from "./generate-constructors.ts";

import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@^0.11.0";


export interface BuildOptions {
outDir: string;
modules?: string[];
}

export function* build(options: BuildOptions): Operation<void> {
let outDir = options.outDir;

yield* call(() => mkdir(outDir, { recursive: true }));

let { modules, agentScriptPath, constructorsPath } = buildAttrs(options);

let modulesList = new Set([
// NOTE: Include core by default
// "@interactors/core",
"@interactors/html",
...modules,
]);

let imports: { [moduleName: string]: Record<string, unknown> } = {};

for (let moduleName of modulesList) {
imports[moduleName] = (yield* call(import(moduleName))) as Record<
string,
unknown
>;
}

// TODO use esbuild to agent

let templatePath =
new URL(import.meta.resolve("./templates/agent.ts.template")).pathname;

let agentTemplate = yield* call(readFile(templatePath, "utf8"));

let importedModules = importInteractors(imports);

let importCode = generateImports(importedModules);

let constructorsCode = generateConstructors(importCode, importedModules);

yield* call(
writeFile(`${outDir}/agent.ts`, [importCode, agentTemplate].join("\n")),
);
console.log(`${outDir}/agent.ts`);

yield* call(() =>
esbuild.build({
plugins: [...denoPlugins()],
entryPoints: [`${outDir}/agent.ts`],
bundle: true,
outfile: agentScriptPath,
sourcemap: "inline",
})
);
console.log(agentScriptPath);

yield* call(writeFile(constructorsPath, constructorsCode));
console.log(constructorsPath);
}

export function buildAttrs(options: BuildOptions) {
return {
modules: (options.modules ?? []).map((name) => path.resolve(name)),
agentScriptPath: `${options.outDir}/agent.js`,
constructorsPath: `${options.outDir}/constructors.ts`,
};
}
23 changes: 23 additions & 0 deletions packages/cli/src/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Operation } from "effection";
import { build, buildAttrs, type BuildOptions } from "./build.ts";
import { useWatcher } from "./watcher.ts";
import { useTestPage } from "./test-page.ts";

export interface DevOptions extends BuildOptions {
repl?: string;
}

export function* dev(options: DevOptions): Operation<void> {
let { modules } = buildAttrs(options);

let updates = yield* useWatcher(modules);

let page = options.repl ? yield* useTestPage(options.repl, options) : { *update() {} };

while (true) {
yield* build(options);
yield* page.update();
yield* updates.next();
console.log("changes detected, rebuilding...");
}
}
19 changes: 19 additions & 0 deletions packages/cli/src/generate-constructors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ImportedModules } from "./types.ts";

export function generateConstructors(imports: string, modules: ImportedModules): string {
return [
imports,
...Object.entries(modules).flatMap(([, { interactors, matchers }]) =>
[
...interactors.map(
({ name }) =>
`export const ${name} = ${name}Interactor.builder()`
),
...matchers.map(
({ name }) =>
`export const ${name} = ${name}Matcher.builder()`
)
]
),
].join("\n");
}
19 changes: 19 additions & 0 deletions packages/cli/src/generate-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ImportedModules } from "./types.ts";

export function generateImports(modules: ImportedModules): string {
return [
...Object.entries(modules).map(
([moduleName, { interactors, matchers }]) =>
`import { ${[
...interactors.map(({ name }) => (`${name} as ${name}Interactor`)),
...matchers.map(({ name }) => (`${name} as ${name}Matcher`)),
].join(", ")} } from '${moduleName}'`
),
`const InteractorTable = {${Object.values(modules)
.flatMap(({ interactors }) => interactors.map(({ name }) => `${name}: ${name}Interactor`))
.join(", ")}}`,
`const MatcherTable = {${Object.values(modules)
.flatMap(({ matchers }) => matchers.map(({ name }) => `${name}: ${name}Matcher`))
.join(", ")}}`,
].join("\n");
}
39 changes: 39 additions & 0 deletions packages/cli/src/import-interactors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { InitInteractor, MatcherConstructor } from "@interactors/core";
import { ImportedModules } from "./types.ts";

export function importInteractors(modules: { [moduleName: string]: Record<string, unknown> }): ImportedModules {
let uniqueNames = new Map<string, string>();

let imports: ImportedModules = {}

for (let moduleName in modules) {
imports[moduleName] = {
interactors: [],
matchers: []
}

let { interactors, matchers } = imports[moduleName];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
for (let [name, obj] of Object.entries<any>(modules[moduleName])) {
if (obj instanceof InitInteractor) {
let interactorName = name;
if (uniqueNames.has(interactorName)) {
throw new Error(`Interactor name ${interactorName} from ${moduleName} is conflicted with named import from ${uniqueNames.get(interactorName)}`);
}
interactors.push({ name });
uniqueNames.set(interactorName, moduleName);
}
if (obj instanceof MatcherConstructor) {
let matcherName = name;
if (uniqueNames.has(matcherName)) {
throw new Error(`Matcher name ${matcherName} from ${moduleName} is conflicted with named import from ${uniqueNames.get(matcherName)}`);
}
matchers.push({ name });
uniqueNames.set(matcherName, moduleName);
}
}
}

return imports;
}
45 changes: 45 additions & 0 deletions packages/cli/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { main, type Task, useScope } from "effection";

import { command, parser } from "zod-opts";
import { z } from "npm:zod";

import { build } from "./build.ts";
//import { dev } from "./dev.ts";

//const commands = { build, dev } as const;

await main(function* (argv) {
let task: Task<void> | undefined = undefined;

let scope = yield* useScope();
parser()
.name("interactors")
.description("build and test interactors")
.version("0.0.0")
.subcommand(
command("build").description(
"build an agent for interactors found in MODULES",
).args([
{
name: "modules",
type: z.array(z.string()).optional(),
description: "paths of modules containing interactors to include in the agent "
},
]).options({
"outDir": {
type: z.string().default("./build"),
alias: "o",
description: "the output directory for generated files",
},
}).action((options) => {
task = scope.run(() => build(options));
}),
)
.parse(argv);

if (typeof task !== 'undefined') {
//@ts-expect-error effection is too good.
yield* task;
}
});

Loading
Loading