wellactually

Writing principles

Reading project files

A detector can read other files of the project with node:fs, starting from the file of the event. That is how a Dart detector checks the pubspec, or a test detector looks for the test that already exists.

Find the kind of project#

Most detectors want to know one thing first: what kind of project is this? The answer lies in a file somewhere above, and findUp from the SDK finds it:

ts
import path from "node:path";
import { findUp } from "@wellactually/sdk/v4";

findUp("pubspec.yaml", path.dirname(event.file.path)); // for a file
findUp("pubspec.yaml", event.cwd); // for a prompt, a command or an output

It returns the absolute path of the nearest file of that name, in that folder or above, or null.

Read a file#

flutter-only/detector.ts
import fs from "node:fs";
import path from "node:path";
import { findUp, onWrite } from "@wellactually/sdk/v4";

onWrite({
  globs: ["**/*.dart"],
  *detect(event) {
    const pubspec = findUp("pubspec.yaml", path.dirname(event.file.path));
    // Only Flutter apps: a pure Dart package has no business with widgets.
    if (pubspec === null || !fs.readFileSync(pubspec, "utf8").includes("sdk: flutter")) {
      return;
    }
    for (const written of event.written) {
      if (written.text.includes("setState(")) {
        yield { line: written.line, match: "setState(" };
      }
    }
  },
});

What node:fs can do#

CallReturns
readFileSync(path)The file as text. The encoding is always UTF-8.
existsSync(path)Whether a file or folder exists.
statSync(path)isFile(), isDirectory() and size.
readdirSync(path)The names in a folder, or with { withFileTypes: true } entries with name, isFile() and isDirectory().
  • It is read-only, and only the synchronous calls exist, because a detector is synchronous.
  • A file that is binary, larger than 512 KB or unreadable looks like a file that does not exist.
  • node:path has join, dirname, basename, extname, relative, resolve, normalize, isAbsolute and parse, always with forward slashes.

What you read stays in the detector#

A finding names a line of the event, and a match has to be text from the event. So what a detector read is for deciding, not for quoting: use the pubspec to tell a Flutter app from a package, not to show the agent what is in it. Without a network there is nowhere else to send it.

Do not rely on event.project#

event.project is the nearest folder upwards with a .wellactually.json or a .git, and null when that is your home folder or there is none. A file outside a repository, a monorepo, or a Codex session in a subfolder all make it surprising. Walk up from event.file.path or event.cwd instead.

Test it#

A test sees only the file of its event, unless you give it a project. Put a small one into the principle's fixtures folder and pass it to detect:

ts
expect(detect(write("lib/counter.dart", code), { project: "fixtures/flutter_app" })).toEqual([{ line: 4, match: "setState(" }]);

Testing explains how fixtures work.