Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# TS toolchain assets staged per platform by stage-ts-assets.sh, next to the
# package that embeds them.
/packages/go/driver/internal/embedded/bin/
/packages/go/driver/internal/typescript/embedded/bin/
/storage/bin*/
/storage/dist/
/storage/dist-test/
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ make check # Go formatter in check mode
```

The first run stages the host TS toolchain assets into
`packages/go/driver/internal/embedded/bin/<os>_<arch>/` (this needs Bun and takes a
`packages/go/driver/internal/typescript/embedded/bin/<os>_<arch>/` (this needs Bun and takes a
few seconds); later runs reuse them and re-stage only when the support scripts,
the tool pins, or the `.oxfmtrc.json` / `.oxlintrc.json` configs change. The
inner loop is then a plain incremental `go build`.
Expand All @@ -293,7 +293,7 @@ fmtkit is one binary with two halves:
- A **Go driver** (`packages/go`) that owns the CLI, finds files, formats Go, runs `go vet`, renders reports, and orchestrates the whole run.
- A **TypeScript sidecar** (`packages/ts/sidecar`), compiled with Bun and embedded in the binary, that formats TS/Vue and the embedded blocks in Markdown/HTML.

The driver runs the sidecar as a child process. Everything that crosses that boundary — the executable name, the modes, the flags, the env vars, the summary lines the driver reads back — is defined once per side (`driver/internal/sidecarproto` in Go, the `cli/` DTOs in TS) and pinned by tests. Change one side and you change the other in the same PR.
The driver runs the sidecar as a child process. Everything that crosses that boundary — the executable name, the modes, the flags, the env vars, the summary lines the driver reads back — is defined once per side (`driver/internal/typescript/proto` in Go, the `cli/` DTOs in TS) and pinned by tests. Change one side and you change the other in the same PR.

### Go side (`packages/go`, module `go.ollin.sh/fmtkit`)

Expand All @@ -309,7 +309,7 @@ The importable library:
| `driver/config` | CLI config. Embeds the formatter config and adds the vet toggle; the `config.yml` schema is a public contract. |
| `driver/report` | Typed output modes and the renderer; the JSON/agent shapes are a public contract. |

The CLI internals (`driver/internal/...`), one job each: `command` holds the one dispatch table both binaries share; `app` only wires things together; `gotool` is the Go check/format use case returning a typed `Outcome`; `pipeline` runs generic steps whose summaries come from typed results (nothing scrapes rendered text); `console` owns terminal colors and printing; `gitfiles`, `filetypes`, and `prettierignore` each own one kind of file selection, composed by `sourcefiles`; `tsruntime` extracts and spawns the sidecar; `embedded` holds the `go:embed` assets (its `bin/` folder is where staging writes — do not move it).
The CLI internals (`driver/internal/...`), one job each: `command` holds the one dispatch table both binaries share; `app` only wires things together, registering the language lanes with `toolchain` — the contract and registry that turn `--ts`/`--go` into an ordered set of lanes to run (no flags means all, TS before Go); `pipeline` runs generic steps whose summaries come from typed results (nothing scrapes rendered text); `console` owns terminal colors and printing; `gitfiles` owns git-backed file selection. Each language then owns its own behaviour in its own package: `golang` is the Go check/format use case (returning a typed `Outcome`) plus its format step; `typescript` builds the TS/Vue lint and format steps and splits its machinery across subpackages — `typescript/runtime` extracts and spawns the sidecar, `typescript/proto` is the frozen wire protocol, `typescript/filetypes` and `typescript/prettierignore` each own one kind of file selection composed by `typescript/sourcefiles`, and `typescript/embedded` holds the `go:embed` assets (its `bin/` folder is where staging writes — do not move it).

### TS side (`packages/ts/sidecar/src`)

Expand Down
6 changes: 3 additions & 3 deletions infra/task.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ sidecar_is_stale() {
run_fmtkit() {
local support_dir sidecar bin

support_dir="${REPO_ROOT}/packages/go/driver/internal/embedded/bin/$(host_target)"
support_dir="${REPO_ROOT}/packages/go/driver/internal/typescript/embedded/bin/$(host_target)"
sidecar="${support_dir}/fmtkit-ts-sidecar"

if sidecar_is_stale "$sidecar"; then
Expand Down Expand Up @@ -201,8 +201,8 @@ run_coverage() {
# shared test helpers are excluded — nothing else, so the number stays
# honest. The threshold is a ratchet: it holds at today's coverage and goes
# up as the under-tested packages (driver/config, internal/app,
# internal/sourcefiles) gain tests; it never goes down.
grep -vE '^go\.ollin\.sh/fmtkit/(driver/cmd/fmtkit/|driver/internal/embedded/|driver/testutil/)' \
# internal/typescript/sourcefiles) gain tests; it never goes down.
grep -vE '^go\.ollin\.sh/fmtkit/(driver/cmd/fmtkit/|driver/internal/typescript/embedded/|driver/testutil/)' \
"${GO_WORKDIR}/coverage.out" > "${GO_WORKDIR}/coverage.gate.out"

go_coverage="$(go -C "$GO_WORKDIR" tool cover -func=coverage.gate.out | awk '/^total:/ { gsub(/%/, "", $3); print $3 }')"
Expand Down
23 changes: 18 additions & 5 deletions packages/go/driver/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"io"

"go.ollin.sh/fmtkit/driver/internal/command"
"go.ollin.sh/fmtkit/driver/internal/gotool"
"go.ollin.sh/fmtkit/driver/internal/sourcefiles"
"go.ollin.sh/fmtkit/driver/internal/golang"
"go.ollin.sh/fmtkit/driver/internal/toolchain"
"go.ollin.sh/fmtkit/driver/internal/typescript"
"go.ollin.sh/fmtkit/driver/internal/typescript/sourcefiles"
report "go.ollin.sh/fmtkit/driver/report"
)

Expand All @@ -19,6 +21,10 @@ type deps struct {
stdout io.Writer
stderr io.Writer

// toolchains are the language lanes the format pipeline runs, in execution
// order (ts before go). The --ts/--go flags select among them.
toolchains toolchain.Registry

// usage prints the enclosing Set's usage text; wired after the Set exists so
// the flag-parsing handlers can reprint it on a bad argument.
usage func(io.Writer)
Expand All @@ -31,7 +37,14 @@ const umbrellaHeader = "usage: fmtkit <format|format-all|go|ts|lint|check|versio
// Umbrella builds the fmtkit command surface: the pipeline commands plus the
// embedded Go formatter CLI reached through `fmtkit go`.
func Umbrella(version string, stdout, stderr io.Writer) command.Set {
d := &deps{version: version, stdout: stdout, stderr: stderr}
// Register the language lanes explicitly, in execution order: TS (lint then
// format) runs before Go, matching the pipeline the driver has always run.
d := &deps{
version: version,
stdout: stdout,
stderr: stderr,
toolchains: toolchain.NewRegistry(typescript.New(), golang.New()),
}

// The Go CLI reached through `fmtkit go` prints "fmtkit go ..." usage and
// adopts the umbrella's exit code for bad subcommands.
Expand Down Expand Up @@ -140,8 +153,8 @@ func (d *deps) goCommandSet(name string, errExit int) command.Set {

// goRunner is the unscoped Go formatter runner shared by `check` and the
// standalone `format`.
func (d *deps) goRunner() gotool.Runner {
return gotool.Runner{Stdout: d.stdout, Stderr: d.stderr}
func (d *deps) goRunner() golang.Runner {
return golang.Runner{Stdout: d.stdout, Stderr: d.stderr}
}

func (d *deps) runCheck(ctx context.Context, args []string) int {
Expand Down
2 changes: 1 addition & 1 deletion packages/go/driver/internal/app/doc.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Package app implements the fmtkit command surface: the pipeline
// orchestration that infra/bin/fmtkit provides in the container images, fused
// with the Go formatter CLI and the embedded TS toolchain (see
// internal/tsruntime).
// internal/typescript).
package app
13 changes: 11 additions & 2 deletions packages/go/driver/internal/app/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"go.ollin.sh/fmtkit/driver/internal/console"
"go.ollin.sh/fmtkit/driver/internal/gitfiles"
"go.ollin.sh/fmtkit/driver/internal/pipeline"
"go.ollin.sh/fmtkit/driver/internal/toolchain"
)

// runFormat formats what diverges from HEAD — modified files, staged or not,
Expand Down Expand Up @@ -47,7 +48,7 @@ func (d *deps) runFormatAll(ctx context.Context, args []string) int {
}

// runPipeline frames the format run (target header, completion footer) around
// the typed steps it builds for the selection, handing them to the generic
// the typed steps the selected lanes contribute, handing them to the generic
// pipeline. Color is resolved once here, at the composition root.
func (d *deps) runPipeline(ctx context.Context, paths []string, opts formatOptions, selection gitfiles.Selection) int {
if len(paths) == 0 {
Expand All @@ -59,8 +60,16 @@ func (d *deps) runPipeline(ctx context.Context, paths []string, opts formatOptio
printer.Section("Formatting target(s)")
printer.Detail("paths", strings.Join(paths, " "))

req := toolchain.Request{Version: d.version, Paths: paths, Selection: selection}

var steps []pipeline.Step

for _, chain := range d.toolchains.Select(opts.toolchains...) {
steps = append(steps, chain.Steps(req)...)
}

pipe := pipeline.Pipeline{
Steps: d.formatSteps(paths, opts.steps, selection),
Steps: steps,
Quiet: opts.quiet,
Printer: printer,
Stderr: d.stderr,
Expand Down
12 changes: 7 additions & 5 deletions packages/go/driver/internal/app/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import (
)

type formatOptions struct {
steps stepSelection
quiet bool
// toolchains names the lanes to run, as the registry selects them. Empty
// means every lane (the no-flag default); --ts and --go narrow it.
toolchains []string
quiet bool
}

// parseFormatArgs splits the format/format-all flags from the paths. With no
// step flags the whole pipeline runs; --ts and --go narrow it.
// lane flags every lane runs; --ts and --go narrow it.
func parseFormatArgs(args []string) (formatOptions, []string, error) {
var opts formatOptions

Expand All @@ -20,9 +22,9 @@ func parseFormatArgs(args []string) (formatOptions, []string, error) {
for _, arg := range args {
switch arg {
case "--ts":
opts.steps.TS = true
opts.toolchains = append(opts.toolchains, "ts")
case "--go":
opts.steps.Go = true
opts.toolchains = append(opts.toolchains, "go")
case "--quiet", "-q":
opts.quiet = true
default:
Expand Down
Loading
Loading