From 314d413d4134fd0173c7c771b407e2b8776089e2 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 24 Jul 2026 12:18:34 +0800 Subject: [PATCH 1/6] =?UTF-8?q?refactor(go):=20G7=20=E2=80=94=20add=20the?= =?UTF-8?q?=20toolchain=20contract=20and=20registry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../go/driver/internal/toolchain/toolchain.go | 71 ++++++++++++++ .../internal/toolchain/toolchain_test.go | 98 +++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 packages/go/driver/internal/toolchain/toolchain.go create mode 100644 packages/go/driver/internal/toolchain/toolchain_test.go diff --git a/packages/go/driver/internal/toolchain/toolchain.go b/packages/go/driver/internal/toolchain/toolchain.go new file mode 100644 index 0000000..86e0b5b --- /dev/null +++ b/packages/go/driver/internal/toolchain/toolchain.go @@ -0,0 +1,71 @@ +// Package toolchain is the contract and registry that separate the format +// pipeline into per-language lanes. Each Toolchain contributes the ordered +// pipeline steps for one language (TS, Go); the Registry holds them in +// registration order, which is the order they run, and resolves the --ts/--go +// selection down to the lanes that should execute. +package toolchain + +import ( + "go.ollin.sh/fmtkit/driver/internal/gitfiles" + "go.ollin.sh/fmtkit/driver/internal/pipeline" +) + +// Request carries what a lane needs to build its steps: the binary version +// (the TS lane extracts a per-version toolchain cache from it), the target +// paths, and how much of the working tree the run scopes to. +type Request struct { + Version string + Paths []string + Selection gitfiles.Selection +} + +// A Toolchain contributes the pipeline steps for one language lane. Name is the +// lane's selector, matching the --ts/--go flags; Steps builds the ordered steps +// for a request (TS returns [lint, format]; Go returns [format]). +type Toolchain interface { + Name() string + Steps(req Request) []pipeline.Step +} + +// Registry holds the registered lanes in registration order, which is also +// their execution order. +type Registry struct { + chains []Toolchain +} + +// NewRegistry registers the given lanes in order. The composition root +// constructs and registers them explicitly; there is no init()-based +// self-registration, so registration order is whatever the caller passes. +func NewRegistry(chains ...Toolchain) Registry { + return Registry{chains: chains} +} + +// Select resolves a set of lane names to the lanes that should run. With no +// names it returns every registered lane (the no-flag "everything" default); +// otherwise it returns the registered lanes whose Name is among names. Either +// way the result preserves registration order, and names that match no +// registered lane are ignored. +func (r Registry) Select(names ...string) []Toolchain { + if len(names) == 0 { + out := make([]Toolchain, len(r.chains)) + copy(out, r.chains) + + return out + } + + want := make(map[string]struct{}, len(names)) + + for _, name := range names { + want[name] = struct{}{} + } + + var out []Toolchain + + for _, chain := range r.chains { + if _, ok := want[chain.Name()]; ok { + out = append(out, chain) + } + } + + return out +} diff --git a/packages/go/driver/internal/toolchain/toolchain_test.go b/packages/go/driver/internal/toolchain/toolchain_test.go new file mode 100644 index 0000000..df547eb --- /dev/null +++ b/packages/go/driver/internal/toolchain/toolchain_test.go @@ -0,0 +1,98 @@ +package toolchain + +import ( + "context" + "fmt" + "io" + "testing" + + "go.ollin.sh/fmtkit/driver/internal/pipeline" +) + +// fakeChain is a minimal Toolchain that records its name and a single labelled +// step, enough to assert the registry's selection and ordering. +type fakeChain struct { + name string +} + +// labelStep is a Step whose Label is its name, so a selection can be read back +// as a list of names. +type labelStep string + +func (c fakeChain) Name() string { return c.name } + +func (c fakeChain) Steps(Request) []pipeline.Step { + return []pipeline.Step{labelStep(c.name)} +} + +func (s labelStep) Label() string { return string(s) } + +func (s labelStep) Run(context.Context, io.Writer) pipeline.Result { return pipeline.Result{} } + +func names(chains []Toolchain) []string { + out := make([]string, 0, len(chains)) + + for _, chain := range chains { + out = append(out, chain.Name()) + } + + return out +} + +func TestSelectEmptyReturnsAllInOrder(t *testing.T) { + reg := NewRegistry(fakeChain{"ts"}, fakeChain{"go"}) + + if got := fmt.Sprint(names(reg.Select())); got != fmt.Sprint([]string{"ts", "go"}) { + t.Fatalf("Select() = %s, want [ts go]", got) + } +} + +func TestSelectByNamePreservesRegistrationOrder(t *testing.T) { + reg := NewRegistry(fakeChain{"ts"}, fakeChain{"go"}) + + // Ask in the opposite order; the registry still returns registration order. + if got := fmt.Sprint(names(reg.Select("go", "ts"))); got != fmt.Sprint([]string{"ts", "go"}) { + t.Fatalf("Select(go, ts) = %s, want [ts go]", got) + } +} + +func TestSelectSingleName(t *testing.T) { + reg := NewRegistry(fakeChain{"ts"}, fakeChain{"go"}) + + if got := fmt.Sprint(names(reg.Select("ts"))); got != fmt.Sprint([]string{"ts"}) { + t.Fatalf("Select(ts) = %s, want [ts]", got) + } + + if got := fmt.Sprint(names(reg.Select("go"))); got != fmt.Sprint([]string{"go"}) { + t.Fatalf("Select(go) = %s, want [go]", got) + } +} + +func TestSelectUnknownNamesAreIgnored(t *testing.T) { + reg := NewRegistry(fakeChain{"ts"}, fakeChain{"go"}) + + if got := names(reg.Select("rust")); len(got) != 0 { + t.Fatalf("Select(rust) = %v, want empty", got) + } + + // A known name mixed with an unknown one keeps only the known lane. + if got := fmt.Sprint(names(reg.Select("go", "rust"))); got != fmt.Sprint([]string{"go"}) { + t.Fatalf("Select(go, rust) = %s, want [go]", got) + } +} + +func TestSelectStepsComeFromChosenLanes(t *testing.T) { + reg := NewRegistry(fakeChain{"ts"}, fakeChain{"go"}) + + var labels []string + + for _, chain := range reg.Select() { + for _, step := range chain.Steps(Request{}) { + labels = append(labels, step.Label()) + } + } + + if got := fmt.Sprint(labels); got != fmt.Sprint([]string{"ts", "go"}) { + t.Fatalf("step labels = %s, want [ts go]", got) + } +} From 0aaa6923d50f1bafe206042a34305c2404b68dcb Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 24 Jul 2026 12:21:43 +0800 Subject: [PATCH 2/6] =?UTF-8?q?refactor(go):=20G7=20=E2=80=94=20move=20got?= =?UTF-8?q?ool=20to=20the=20golang=20lane=20with=20its=20format=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/go/driver/internal/app/app.go | 6 +- packages/go/driver/internal/app/steps.go | 119 +------------- packages/go/driver/internal/app/steps_test.go | 104 ------------- .../internal/{gotool => golang}/execute.go | 2 +- .../{gotool => golang}/execute_test.go | 2 +- .../internal/{gotool => golang}/parser.go | 2 +- .../{gotool => golang}/parser_test.go | 2 +- .../internal/{gotool => golang}/runner.go | 2 +- .../{gotool => golang}/runner_test.go | 2 +- packages/go/driver/internal/golang/step.go | 145 +++++++++++++++++ .../go/driver/internal/golang/step_test.go | 146 ++++++++++++++++++ 11 files changed, 302 insertions(+), 230 deletions(-) rename packages/go/driver/internal/{gotool => golang}/execute.go (99%) rename packages/go/driver/internal/{gotool => golang}/execute_test.go (99%) rename packages/go/driver/internal/{gotool => golang}/parser.go (99%) rename packages/go/driver/internal/{gotool => golang}/parser_test.go (99%) rename packages/go/driver/internal/{gotool => golang}/runner.go (99%) rename packages/go/driver/internal/{gotool => golang}/runner_test.go (99%) create mode 100644 packages/go/driver/internal/golang/step.go create mode 100644 packages/go/driver/internal/golang/step_test.go diff --git a/packages/go/driver/internal/app/app.go b/packages/go/driver/internal/app/app.go index 370dd34..fdb6e3f 100644 --- a/packages/go/driver/internal/app/app.go +++ b/packages/go/driver/internal/app/app.go @@ -6,7 +6,7 @@ import ( "io" "go.ollin.sh/fmtkit/driver/internal/command" - "go.ollin.sh/fmtkit/driver/internal/gotool" + "go.ollin.sh/fmtkit/driver/internal/golang" "go.ollin.sh/fmtkit/driver/internal/sourcefiles" report "go.ollin.sh/fmtkit/driver/report" ) @@ -140,8 +140,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 { diff --git a/packages/go/driver/internal/app/steps.go b/packages/go/driver/internal/app/steps.go index e8bb8fe..e88b2b9 100644 --- a/packages/go/driver/internal/app/steps.go +++ b/packages/go/driver/internal/app/steps.go @@ -10,13 +10,10 @@ import ( "strings" "go.ollin.sh/fmtkit/driver/internal/gitfiles" - "go.ollin.sh/fmtkit/driver/internal/gotool" + "go.ollin.sh/fmtkit/driver/internal/golang" "go.ollin.sh/fmtkit/driver/internal/pipeline" "go.ollin.sh/fmtkit/driver/internal/sidecarproto" "go.ollin.sh/fmtkit/driver/internal/tsruntime" - report "go.ollin.sh/fmtkit/driver/report" - formatterengine "go.ollin.sh/fmtkit/formatter/engine" - "go.ollin.sh/fmtkit/vet" ) // stepSelection selects which parts of the format pipeline run; the zero value @@ -41,13 +38,6 @@ type tsFormatStep struct { selection gitfiles.Selection } -// goFormatStep formats Go files and runs go vet, deriving its details from the -// typed outcome rather than the rendered report text. -type goFormatStep struct { - paths []string - selection gitfiles.Selection -} - func (s stepSelection) normalized() stepSelection { if !s.TS && !s.Go { return stepSelection{TS: true, Go: true} @@ -71,7 +61,7 @@ func (d *deps) formatSteps(paths []string, selected stepSelection, selection git } if selected.Go { - steps = append(steps, goFormatStep{paths: paths, selection: selection}) + steps = append(steps, golang.FormatStep(paths, selection)) } return steps @@ -117,20 +107,6 @@ func (s tsFormatStep) Run(ctx context.Context, output io.Writer) pipeline.Result return pipeline.Result{Details: tsFormatDetails(captured.String())} } -func (s goFormatStep) Label() string { return "Running Go formatting" } - -func (s goFormatStep) Run(ctx context.Context, output io.Writer) pipeline.Result { - outcome, code := gotool. - Runner{Stdout: output, Stderr: output, Scope: s.selection}. - RunReport(ctx, report.ModeFormat, s.paths) - - if code != 0 { - return pipeline.Result{ExitCode: code} - } - - return pipeline.Result{Details: goFormatDetails(outcome)} -} - // invokeTS resolves the TS toolchain and invokes it through spawn, which // receives the constructed Invoker and the writer to stream tool output to. func invokeTS(version string, output io.Writer, spawn func(tsruntime.Invoker, io.Writer) error) error { @@ -216,94 +192,3 @@ func tsFormatDetails(log string) []pipeline.Detail { return details } - -// goFormatDetails computes the Go step's detail lines from the typed outcome, -// reproducing the exact strings the text report renders (which the pipeline -// previously scraped back out of that rendered text). -func goFormatDetails(outcome gotool.Outcome) []pipeline.Detail { - fm := outcome.Combined.Formatter - vt := outcome.Combined.Vet - - var details []pipeline.Detail - - if summary := goFileSummary(fm, outcome.Mode); summary != "" { - details = append(details, pipeline.Detail{Label: "fmtkit", Value: summary}) - } - - // The formatter renders a Result line unless it found no files and hit no - // errors; the vet Result line always renders. The "result" detail is the - // first Result line (the formatter's when present, else the vet's), matching - // the text report's top-to-bottom order. - formatterResult := "" - - if fm.Files != 0 || len(fm.Errors) != 0 { - formatterResult = fmt.Sprintf("%s. %d changed, %d violation(s), %d error(s).", fm.Result, fm.Changed, fm.ViolationCount(), fm.ErrorCount()) - } - - vetResult := fmt.Sprintf("%s. %d error(s).", goVetStatus(vt), vt.ErrorCount()) - - resultLine := formatterResult - - if resultLine == "" { - resultLine = vetResult - } - - details = append(details, pipeline.Detail{Label: "result", Value: resultLine}) - - if summary := goVetSummary(vt); summary != "" { - details = append(details, pipeline.Detail{Label: "vet", Value: summary}) - } - - if vetResult != resultLine { - details = append(details, pipeline.Detail{Label: "vet result", Value: vetResult}) - } - - return details -} - -// goFileSummary is the formatter's file-count line: "No Go files found." when it -// owns none, otherwise the mode's verb and count. -func goFileSummary(fm formatterengine.Report, mode report.Mode) string { - if fm.Files == 0 { - return "No Go files found." - } - - action := "Checked" - - if mode == report.ModeFormat { - action = "Formatted" - } - - return fmt.Sprintf("%s %d file(s).", action, fm.Files) -} - -// goVetStatus classifies the vet report the same way the text report does. -func goVetStatus(vt vet.Report) string { - switch { - case vt.Skipped || vt.Root == "": - return "skipped" - case vt.ErrorCount() > 0: - return "fail" - default: - return "pass" - } -} - -// goVetSummary is the vet status line, or "" for a failure (whose per-error -// lines the text report shows instead of a one-line summary). -func goVetSummary(vt vet.Report) string { - switch goVetStatus(vt) { - case "skipped": - reason := "no Go module or workspace was detected" - - if vt.Skipped { - reason = "the Go toolchain is not available" - } - - return "Skipped automatic go vet ./... because " + reason + "." - case "pass": - return "go vet ./... passed." - default: - return "" - } -} diff --git a/packages/go/driver/internal/app/steps_test.go b/packages/go/driver/internal/app/steps_test.go index c9d5725..2b3e010 100644 --- a/packages/go/driver/internal/app/steps_test.go +++ b/packages/go/driver/internal/app/steps_test.go @@ -6,11 +6,7 @@ import ( "fmt" "testing" - "go.ollin.sh/fmtkit/driver/internal/gotool" "go.ollin.sh/fmtkit/driver/internal/pipeline" - report "go.ollin.sh/fmtkit/driver/report" - formatterengine "go.ollin.sh/fmtkit/formatter/engine" - "go.ollin.sh/fmtkit/vet" ) func detailStrings(details []pipeline.Detail) []string { @@ -31,106 +27,6 @@ func assertDetails(t *testing.T, got []pipeline.Detail, want ...string) { } } -// goOutcome builds a Go outcome for the given mode, formatter report, and vet -// report. -func goOutcome(mode report.Mode, fm formatterengine.Report, vt vet.Report) gotool.Outcome { - return gotool.Outcome{Mode: mode, Combined: report.Combined{Formatter: fm, Vet: vt}} -} - -func TestGoFormatDetailsPass(t *testing.T) { - outcome := goOutcome( - report.ModeFormat, - formatterengine.Report{Result: formatterengine.ResultPass, Files: 2}, - vet.Report{Root: "/work"}, - ) - - assertDetails(t, goFormatDetails(outcome), - "fmtkit|Formatted 2 file(s).", - "result|pass. 0 changed, 0 violation(s), 0 error(s).", - "vet|go vet ./... passed.", - "vet result|pass. 0 error(s).", - ) -} - -func TestGoFormatDetailsCheckModeVerb(t *testing.T) { - outcome := goOutcome( - report.ModeCheck, - formatterengine.Report{Result: formatterengine.ResultPass, Files: 3}, - vet.Report{Root: "/work"}, - ) - - assertDetails(t, goFormatDetails(outcome), - "fmtkit|Checked 3 file(s).", - "result|pass. 0 changed, 0 violation(s), 0 error(s).", - "vet|go vet ./... passed.", - "vet result|pass. 0 error(s).", - ) -} - -// TestGoFormatDetailsNoFiles reproduces the scraper's quirk: with no formatter -// Result line rendered, the "result" detail borrows the vet Result line and the -// separate "vet result" line is suppressed (they are identical). -func TestGoFormatDetailsNoFiles(t *testing.T) { - outcome := goOutcome( - report.ModeFormat, - formatterengine.Report{Result: formatterengine.ResultPass, Files: 0}, - vet.Report{Root: "/work"}, - ) - - assertDetails(t, goFormatDetails(outcome), - "fmtkit|No Go files found.", - "result|pass. 0 error(s).", - "vet|go vet ./... passed.", - ) -} - -func TestGoFormatDetailsVetSkippedNoModule(t *testing.T) { - outcome := goOutcome( - report.ModeFormat, - formatterengine.Report{Result: formatterengine.ResultPass, Files: 1}, - vet.Report{Root: ""}, - ) - - assertDetails(t, goFormatDetails(outcome), - "fmtkit|Formatted 1 file(s).", - "result|pass. 0 changed, 0 violation(s), 0 error(s).", - "vet|Skipped automatic go vet ./... because no Go module or workspace was detected.", - "vet result|skipped. 0 error(s).", - ) -} - -func TestGoFormatDetailsVetSkippedToolchain(t *testing.T) { - outcome := goOutcome( - report.ModeFormat, - formatterengine.Report{Result: formatterengine.ResultPass, Files: 1}, - vet.Report{Root: "/work", Skipped: true}, - ) - - assertDetails(t, goFormatDetails(outcome), - "fmtkit|Formatted 1 file(s).", - "result|pass. 0 changed, 0 violation(s), 0 error(s).", - "vet|Skipped automatic go vet ./... because the Go toolchain is not available.", - "vet result|skipped. 0 error(s).", - ) -} - -// TestGoFormatDetailsVetFailure: a vet failure renders per-error lines instead -// of a status summary, so there is no "vet" detail, but the differing vet Result -// line still appears. -func TestGoFormatDetailsVetFailure(t *testing.T) { - outcome := goOutcome( - report.ModeFormat, - formatterengine.Report{Result: formatterengine.ResultPass, Files: 2}, - vet.Report{Root: "/work", Errors: []vet.ErrorResult{{File: "a.go", Message: "boom"}}}, - ) - - assertDetails(t, goFormatDetails(outcome), - "fmtkit|Formatted 2 file(s).", - "result|pass. 0 changed, 0 violation(s), 0 error(s).", - "vet result|fail. 1 error(s).", - ) -} - func TestTSFormatDetails(t *testing.T) { log := "[blank-lines] processed 3 file(s) in /work, 0 changed\n" + "Finished in 10ms on 3 files using 8 threads.\n" + diff --git a/packages/go/driver/internal/gotool/execute.go b/packages/go/driver/internal/golang/execute.go similarity index 99% rename from packages/go/driver/internal/gotool/execute.go rename to packages/go/driver/internal/golang/execute.go index b89ccc6..da16ceb 100644 --- a/packages/go/driver/internal/gotool/execute.go +++ b/packages/go/driver/internal/golang/execute.go @@ -1,4 +1,4 @@ -package gotool +package golang import ( "context" diff --git a/packages/go/driver/internal/gotool/execute_test.go b/packages/go/driver/internal/golang/execute_test.go similarity index 99% rename from packages/go/driver/internal/gotool/execute_test.go rename to packages/go/driver/internal/golang/execute_test.go index cded312..8c0c9ac 100644 --- a/packages/go/driver/internal/gotool/execute_test.go +++ b/packages/go/driver/internal/golang/execute_test.go @@ -1,4 +1,4 @@ -package gotool +package golang import ( "context" diff --git a/packages/go/driver/internal/gotool/parser.go b/packages/go/driver/internal/golang/parser.go similarity index 99% rename from packages/go/driver/internal/gotool/parser.go rename to packages/go/driver/internal/golang/parser.go index 3b846e5..bd789f0 100644 --- a/packages/go/driver/internal/gotool/parser.go +++ b/packages/go/driver/internal/golang/parser.go @@ -1,4 +1,4 @@ -package gotool +package golang import ( "flag" diff --git a/packages/go/driver/internal/gotool/parser_test.go b/packages/go/driver/internal/golang/parser_test.go similarity index 99% rename from packages/go/driver/internal/gotool/parser_test.go rename to packages/go/driver/internal/golang/parser_test.go index d0405d1..3d0463e 100644 --- a/packages/go/driver/internal/gotool/parser_test.go +++ b/packages/go/driver/internal/golang/parser_test.go @@ -1,4 +1,4 @@ -package gotool +package golang import ( "io" diff --git a/packages/go/driver/internal/gotool/runner.go b/packages/go/driver/internal/golang/runner.go similarity index 99% rename from packages/go/driver/internal/gotool/runner.go rename to packages/go/driver/internal/golang/runner.go index 8895dca..3aee0c4 100644 --- a/packages/go/driver/internal/gotool/runner.go +++ b/packages/go/driver/internal/golang/runner.go @@ -1,4 +1,4 @@ -package gotool +package golang import ( "context" diff --git a/packages/go/driver/internal/gotool/runner_test.go b/packages/go/driver/internal/golang/runner_test.go similarity index 99% rename from packages/go/driver/internal/gotool/runner_test.go rename to packages/go/driver/internal/golang/runner_test.go index 942ef37..dd9f82d 100644 --- a/packages/go/driver/internal/gotool/runner_test.go +++ b/packages/go/driver/internal/golang/runner_test.go @@ -1,4 +1,4 @@ -package gotool +package golang import ( "bytes" diff --git a/packages/go/driver/internal/golang/step.go b/packages/go/driver/internal/golang/step.go new file mode 100644 index 0000000..6c504da --- /dev/null +++ b/packages/go/driver/internal/golang/step.go @@ -0,0 +1,145 @@ +package golang + +import ( + "context" + "fmt" + "io" + + "go.ollin.sh/fmtkit/driver/internal/gitfiles" + "go.ollin.sh/fmtkit/driver/internal/pipeline" + "go.ollin.sh/fmtkit/driver/internal/toolchain" + report "go.ollin.sh/fmtkit/driver/report" + formatterengine "go.ollin.sh/fmtkit/formatter/engine" + "go.ollin.sh/fmtkit/vet" +) + +// Toolchain is the Go lane: it formats Go files and runs go vet, contributing a +// single format step to the pipeline. +type Toolchain struct{} + +type formatStep struct { + paths []string + selection gitfiles.Selection +} + +// New builds the Go toolchain. +func New() Toolchain { return Toolchain{} } + +// Name is the lane's selector, matching the --go flag. +func (Toolchain) Name() string { return "go" } + +// Steps returns the Go lane's ordered steps: just the format step. +func (Toolchain) Steps(req toolchain.Request) []pipeline.Step { + return []pipeline.Step{FormatStep(req.Paths, req.Selection)} +} + +// FormatStep builds the pipeline step that formats Go files and runs go vet, +// deriving its details from the typed outcome rather than the rendered report +// text. +func FormatStep(paths []string, selection gitfiles.Selection) pipeline.Step { + return formatStep{paths: paths, selection: selection} +} + +func (s formatStep) Label() string { return "Running Go formatting" } + +func (s formatStep) Run(ctx context.Context, output io.Writer) pipeline.Result { + outcome, code := Runner{Stdout: output, Stderr: output, Scope: s.selection}. + RunReport(ctx, report.ModeFormat, s.paths) + + if code != 0 { + return pipeline.Result{ExitCode: code} + } + + return pipeline.Result{Details: formatDetails(outcome)} +} + +// formatDetails computes the Go step's detail lines from the typed outcome, +// reproducing the exact strings the text report renders (which the pipeline +// previously scraped back out of that rendered text). +func formatDetails(outcome Outcome) []pipeline.Detail { + fm := outcome.Combined.Formatter + vt := outcome.Combined.Vet + + var details []pipeline.Detail + + if summary := fileSummary(fm, outcome.Mode); summary != "" { + details = append(details, pipeline.Detail{Label: "fmtkit", Value: summary}) + } + + // The formatter renders a Result line unless it found no files and hit no + // errors; the vet Result line always renders. The "result" detail is the + // first Result line (the formatter's when present, else the vet's), matching + // the text report's top-to-bottom order. + formatterResult := "" + + if fm.Files != 0 || len(fm.Errors) != 0 { + formatterResult = fmt.Sprintf("%s. %d changed, %d violation(s), %d error(s).", fm.Result, fm.Changed, fm.ViolationCount(), fm.ErrorCount()) + } + + vetResult := fmt.Sprintf("%s. %d error(s).", vetStatus(vt), vt.ErrorCount()) + + resultLine := formatterResult + + if resultLine == "" { + resultLine = vetResult + } + + details = append(details, pipeline.Detail{Label: "result", Value: resultLine}) + + if summary := vetSummary(vt); summary != "" { + details = append(details, pipeline.Detail{Label: "vet", Value: summary}) + } + + if vetResult != resultLine { + details = append(details, pipeline.Detail{Label: "vet result", Value: vetResult}) + } + + return details +} + +// fileSummary is the formatter's file-count line: "No Go files found." when it +// owns none, otherwise the mode's verb and count. +func fileSummary(fm formatterengine.Report, mode report.Mode) string { + if fm.Files == 0 { + return "No Go files found." + } + + action := "Checked" + + if mode == report.ModeFormat { + action = "Formatted" + } + + return fmt.Sprintf("%s %d file(s).", action, fm.Files) +} + +// vetStatus classifies the vet report the same way the text report does. +func vetStatus(vt vet.Report) string { + switch { + case vt.Skipped || vt.Root == "": + return "skipped" + case vt.ErrorCount() > 0: + return "fail" + default: + return "pass" + } +} + +// vetSummary is the vet status line, or "" for a failure (whose per-error lines +// the text report shows instead of a one-line summary). +func vetSummary(vt vet.Report) string { + switch vetStatus(vt) { + case "skipped": + reason := "no Go module or workspace was detected" + + if vt.Skipped { + reason = "the Go toolchain is not available" + } + + return "Skipped automatic go vet ./... because " + reason + "." + case "pass": + return "go vet ./... passed." + default: + return "" + } +} diff --git a/packages/go/driver/internal/golang/step_test.go b/packages/go/driver/internal/golang/step_test.go new file mode 100644 index 0000000..b404714 --- /dev/null +++ b/packages/go/driver/internal/golang/step_test.go @@ -0,0 +1,146 @@ +package golang + +import ( + "fmt" + "testing" + + "go.ollin.sh/fmtkit/driver/internal/pipeline" + "go.ollin.sh/fmtkit/driver/internal/toolchain" + report "go.ollin.sh/fmtkit/driver/report" + formatterengine "go.ollin.sh/fmtkit/formatter/engine" + "go.ollin.sh/fmtkit/vet" +) + +func detailStrings(details []pipeline.Detail) []string { + out := make([]string, 0, len(details)) + + for _, d := range details { + out = append(out, d.Label+"|"+d.Value) + } + + return out +} + +func assertDetails(t *testing.T, got []pipeline.Detail, want ...string) { + t.Helper() + + if g := fmt.Sprint(detailStrings(got)); g != fmt.Sprint(want) { + t.Fatalf("details mismatch\n--- got ---\n%s\n--- want ---\n%s", g, fmt.Sprint(want)) + } +} + +// outcomeFor builds a Go outcome for the given mode, formatter report, and vet +// report. +func outcomeFor(mode report.Mode, fm formatterengine.Report, vt vet.Report) Outcome { + return Outcome{Mode: mode, Combined: report.Combined{Formatter: fm, Vet: vt}} +} + +func TestFormatDetailsPass(t *testing.T) { + outcome := outcomeFor( + report.ModeFormat, + formatterengine.Report{Result: formatterengine.ResultPass, Files: 2}, + vet.Report{Root: "/work"}, + ) + + assertDetails(t, formatDetails(outcome), + "fmtkit|Formatted 2 file(s).", + "result|pass. 0 changed, 0 violation(s), 0 error(s).", + "vet|go vet ./... passed.", + "vet result|pass. 0 error(s).", + ) +} + +func TestFormatDetailsCheckModeVerb(t *testing.T) { + outcome := outcomeFor( + report.ModeCheck, + formatterengine.Report{Result: formatterengine.ResultPass, Files: 3}, + vet.Report{Root: "/work"}, + ) + + assertDetails(t, formatDetails(outcome), + "fmtkit|Checked 3 file(s).", + "result|pass. 0 changed, 0 violation(s), 0 error(s).", + "vet|go vet ./... passed.", + "vet result|pass. 0 error(s).", + ) +} + +// TestFormatDetailsNoFiles reproduces the scraper's quirk: with no formatter +// Result line rendered, the "result" detail borrows the vet Result line and the +// separate "vet result" line is suppressed (they are identical). +func TestFormatDetailsNoFiles(t *testing.T) { + outcome := outcomeFor( + report.ModeFormat, + formatterengine.Report{Result: formatterengine.ResultPass, Files: 0}, + vet.Report{Root: "/work"}, + ) + + assertDetails(t, formatDetails(outcome), + "fmtkit|No Go files found.", + "result|pass. 0 error(s).", + "vet|go vet ./... passed.", + ) +} + +func TestFormatDetailsVetSkippedNoModule(t *testing.T) { + outcome := outcomeFor( + report.ModeFormat, + formatterengine.Report{Result: formatterengine.ResultPass, Files: 1}, + vet.Report{Root: ""}, + ) + + assertDetails(t, formatDetails(outcome), + "fmtkit|Formatted 1 file(s).", + "result|pass. 0 changed, 0 violation(s), 0 error(s).", + "vet|Skipped automatic go vet ./... because no Go module or workspace was detected.", + "vet result|skipped. 0 error(s).", + ) +} + +func TestFormatDetailsVetSkippedToolchain(t *testing.T) { + outcome := outcomeFor( + report.ModeFormat, + formatterengine.Report{Result: formatterengine.ResultPass, Files: 1}, + vet.Report{Root: "/work", Skipped: true}, + ) + + assertDetails(t, formatDetails(outcome), + "fmtkit|Formatted 1 file(s).", + "result|pass. 0 changed, 0 violation(s), 0 error(s).", + "vet|Skipped automatic go vet ./... because the Go toolchain is not available.", + "vet result|skipped. 0 error(s).", + ) +} + +// TestFormatDetailsVetFailure: a vet failure renders per-error lines instead of +// a status summary, so there is no "vet" detail, but the differing vet Result +// line still appears. +func TestFormatDetailsVetFailure(t *testing.T) { + outcome := outcomeFor( + report.ModeFormat, + formatterengine.Report{Result: formatterengine.ResultPass, Files: 2}, + vet.Report{Root: "/work", Errors: []vet.ErrorResult{{File: "a.go", Message: "boom"}}}, + ) + + assertDetails(t, formatDetails(outcome), + "fmtkit|Formatted 2 file(s).", + "result|pass. 0 changed, 0 violation(s), 0 error(s).", + "vet result|fail. 1 error(s).", + ) +} + +func TestSteps(t *testing.T) { + steps := New().Steps(toolchain.Request{Paths: []string{"."}}) + + if len(steps) != 1 { + t.Fatalf("Steps len = %d, want 1", len(steps)) + } + + if got := steps[0].Label(); got != "Running Go formatting" { + t.Fatalf("step label = %q, want %q", got, "Running Go formatting") + } + + if got := New().Name(); got != "go" { + t.Fatalf("Name = %q, want go", got) + } +} From 878b0b273a750222123a7fdfb08d70e681ff25cd Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 24 Jul 2026 12:25:38 +0800 Subject: [PATCH 3/6] =?UTF-8?q?refactor(go):=20G7=20=E2=80=94=20move=20the?= =?UTF-8?q?=20TS=20lane=20under=20typescript/=20with=20its=20steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/go/driver/internal/app/app.go | 2 +- packages/go/driver/internal/app/doc.go | 2 +- packages/go/driver/internal/app/steps.go | 156 +------------- packages/go/driver/internal/app/steps_test.go | 77 ------- packages/go/driver/internal/app/ts.go | 10 +- .../{ => typescript}/filetypes/filetypes.go | 0 .../filetypes/filetypes_test.go | 0 .../prettierignore/prettierignore.go | 0 .../prettierignore/prettierignore_test.go | 0 .../proto}/command.go | 2 +- .../proto}/command_test.go | 2 +- .../proto}/sidecarproto.go | 4 +- .../proto}/sidecarproto_test.go | 2 +- .../proto}/summary.go | 2 +- .../proto}/summary_test.go | 2 +- .../runtime}/assets.go | 22 +- .../runtime}/invoker.go | 14 +- .../runtime}/prettier.go | 10 +- .../runtime}/prettier_internal_test.go | 10 +- .../runtime}/prettier_test.go | 30 +-- .../runtime}/run_test.go | 26 +-- .../runtime}/support_test.go | 22 +- .../{ => typescript}/sourcefiles/command.go | 0 .../sourcefiles/command_test.go | 0 .../sourcefiles/prettierignore_test.go | 0 .../sourcefiles/sourcefiles.go | 4 +- .../sourcefiles/sourcefiles_test.go | 0 .../go/driver/internal/typescript/step.go | 192 ++++++++++++++++++ .../driver/internal/typescript/step_test.go | 104 ++++++++++ 29 files changed, 382 insertions(+), 313 deletions(-) rename packages/go/driver/internal/{ => typescript}/filetypes/filetypes.go (100%) rename packages/go/driver/internal/{ => typescript}/filetypes/filetypes_test.go (100%) rename packages/go/driver/internal/{ => typescript}/prettierignore/prettierignore.go (100%) rename packages/go/driver/internal/{ => typescript}/prettierignore/prettierignore_test.go (100%) rename packages/go/driver/internal/{sidecarproto => typescript/proto}/command.go (99%) rename packages/go/driver/internal/{sidecarproto => typescript/proto}/command_test.go (99%) rename packages/go/driver/internal/{sidecarproto => typescript/proto}/sidecarproto.go (96%) rename packages/go/driver/internal/{sidecarproto => typescript/proto}/sidecarproto_test.go (99%) rename packages/go/driver/internal/{sidecarproto => typescript/proto}/summary.go (99%) rename packages/go/driver/internal/{sidecarproto => typescript/proto}/summary_test.go (98%) rename packages/go/driver/internal/{tsruntime => typescript/runtime}/assets.go (88%) rename packages/go/driver/internal/{tsruntime => typescript/runtime}/invoker.go (95%) rename packages/go/driver/internal/{tsruntime => typescript/runtime}/prettier.go (95%) rename packages/go/driver/internal/{tsruntime => typescript/runtime}/prettier_internal_test.go (90%) rename packages/go/driver/internal/{tsruntime => typescript/runtime}/prettier_test.go (90%) rename packages/go/driver/internal/{tsruntime => typescript/runtime}/run_test.go (93%) rename packages/go/driver/internal/{tsruntime => typescript/runtime}/support_test.go (82%) rename packages/go/driver/internal/{ => typescript}/sourcefiles/command.go (100%) rename packages/go/driver/internal/{ => typescript}/sourcefiles/command_test.go (100%) rename packages/go/driver/internal/{ => typescript}/sourcefiles/prettierignore_test.go (100%) rename packages/go/driver/internal/{ => typescript}/sourcefiles/sourcefiles.go (96%) rename packages/go/driver/internal/{ => typescript}/sourcefiles/sourcefiles_test.go (100%) create mode 100644 packages/go/driver/internal/typescript/step.go create mode 100644 packages/go/driver/internal/typescript/step_test.go diff --git a/packages/go/driver/internal/app/app.go b/packages/go/driver/internal/app/app.go index fdb6e3f..7176aa3 100644 --- a/packages/go/driver/internal/app/app.go +++ b/packages/go/driver/internal/app/app.go @@ -7,7 +7,7 @@ import ( "go.ollin.sh/fmtkit/driver/internal/command" "go.ollin.sh/fmtkit/driver/internal/golang" - "go.ollin.sh/fmtkit/driver/internal/sourcefiles" + "go.ollin.sh/fmtkit/driver/internal/typescript/sourcefiles" report "go.ollin.sh/fmtkit/driver/report" ) diff --git a/packages/go/driver/internal/app/doc.go b/packages/go/driver/internal/app/doc.go index b84e779..0dc9bd9 100644 --- a/packages/go/driver/internal/app/doc.go +++ b/packages/go/driver/internal/app/doc.go @@ -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 diff --git a/packages/go/driver/internal/app/steps.go b/packages/go/driver/internal/app/steps.go index e88b2b9..e109501 100644 --- a/packages/go/driver/internal/app/steps.go +++ b/packages/go/driver/internal/app/steps.go @@ -1,19 +1,10 @@ package app import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "os/exec" - "strings" - "go.ollin.sh/fmtkit/driver/internal/gitfiles" "go.ollin.sh/fmtkit/driver/internal/golang" "go.ollin.sh/fmtkit/driver/internal/pipeline" - "go.ollin.sh/fmtkit/driver/internal/sidecarproto" - "go.ollin.sh/fmtkit/driver/internal/tsruntime" + "go.ollin.sh/fmtkit/driver/internal/typescript" ) // stepSelection selects which parts of the format pipeline run; the zero value @@ -23,21 +14,6 @@ type stepSelection struct { Go bool } -// tsLintStep lints TS/Vue files, applying oxlint's safe fixes (--fix). -type tsLintStep struct { - version string - paths []string - selection gitfiles.Selection -} - -// tsFormatStep runs the full TS/Vue formatting pipeline (oxfmt plus the project -// passes). -type tsFormatStep struct { - version string - paths []string - selection gitfiles.Selection -} - func (s stepSelection) normalized() stepSelection { if !s.TS && !s.Go { return stepSelection{TS: true, Go: true} @@ -55,8 +31,8 @@ func (d *deps) formatSteps(paths []string, selected stepSelection, selection git if selected.TS { steps = append(steps, - tsLintStep{version: d.version, paths: paths, selection: selection}, - tsFormatStep{version: d.version, paths: paths, selection: selection}, + typescript.LintStep(d.version, paths, selection), + typescript.FormatStep(d.version, paths, selection), ) } @@ -66,129 +42,3 @@ func (d *deps) formatSteps(paths []string, selected stepSelection, selection git return steps } - -// Driver-owned bookkeeping lines the TS steps recognize in their captured -// output. The sidecar's own wire lines are parsed by sidecarproto; these are -// notices the Go driver prints around the sidecar, so they stay here. -const ( - sourcesMissingPrefix = "[sources] path not found, skipping:" - lintNothingToLintLine = "[lint] no TS/Vue files to lint." -) - -func (s tsLintStep) Label() string { return "Running TS/Vue lint" } - -func (s tsLintStep) Run(ctx context.Context, output io.Writer) pipeline.Result { - var captured bytes.Buffer - - err := invokeTS(s.version, io.MultiWriter(output, &captured), func(invoker tsruntime.Invoker, w io.Writer) error { - return invoker.RunLint(ctx, tsruntime.Request{Scopes: s.paths, Selection: s.selection, Fix: true, Stdout: w, Stderr: w}) - }) - - if code := tsExitCode(err, output); code != 0 { - return pipeline.Result{ExitCode: code} - } - - return pipeline.Result{Details: tsLintDetails(captured.String())} -} - -func (s tsFormatStep) Label() string { return "Running TS/Vue formatting" } - -func (s tsFormatStep) Run(ctx context.Context, output io.Writer) pipeline.Result { - var captured bytes.Buffer - - err := invokeTS(s.version, io.MultiWriter(output, &captured), func(invoker tsruntime.Invoker, w io.Writer) error { - return invoker.RunPipeline(ctx, tsruntime.Request{Scopes: s.paths, Selection: s.selection, Stdout: w, Stderr: w}) - }) - - if code := tsExitCode(err, output); code != 0 { - return pipeline.Result{ExitCode: code} - } - - return pipeline.Result{Details: tsFormatDetails(captured.String())} -} - -// invokeTS resolves the TS toolchain and invokes it through spawn, which -// receives the constructed Invoker and the writer to stream tool output to. -func invokeTS(version string, output io.Writer, spawn func(tsruntime.Invoker, io.Writer) error) error { - assets, err := tsruntime.Resolve(version) - - if err != nil { - return err - } - - return spawn(tsruntime.NewInvoker(assets), output) -} - -// tsExitCode maps a TS step error to its exit code. Failures that never -// produced tool output (a missing sidecar, an unreadable working tree) surface -// their message through output so they are visible both live and in the quiet -// failure dump. -func tsExitCode(err error, output io.Writer) int { - if err == nil { - return 0 - } - - var exit *exec.ExitError - - if errors.As(err, &exit) { - return exit.ExitCode() - } - - _, _ = io.WriteString(output, err.Error()+"\n") - - return 1 -} - -// tsLintDetails derives the oxlint summary line. A driver "no files" notice -// wins; otherwise oxlint's own result line; otherwise a clean fallback. -func tsLintDetails(log string) []pipeline.Detail { - for _, line := range strings.Split(log, "\n") { - if strings.HasPrefix(line, lintNothingToLintLine) { - return []pipeline.Detail{{Label: "oxlint", Value: strings.TrimPrefix(lintNothingToLintLine, "[lint] ")}} - } - } - - if result := sidecarproto.ParseLintSummary(log).Result; result != "" { - return []pipeline.Detail{{Label: "oxlint", Value: result}} - } - - return []pipeline.Detail{{Label: "oxlint", Value: "no issues found"}} -} - -// tsFormatDetails derives the TS pipeline's detail lines from the sidecar's -// progress output plus the driver's missing-source notices. -func tsFormatDetails(log string) []pipeline.Detail { - summary := sidecarproto.ParsePipelineSummary(log) - - var details []pipeline.Detail - - if summary.BlankLines != "" { - details = append(details, pipeline.Detail{Label: "blank-lines", Value: summary.BlankLines}) - } - - missing := 0 - - for _, line := range strings.Split(log, "\n") { - if strings.HasPrefix(line, sourcesMissingPrefix) { - missing++ - } - } - - if missing > 0 { - details = append(details, pipeline.Detail{Label: "skipped", Value: fmt.Sprintf("%d missing tracked file(s)", missing)}) - } - - if summary.Oxfmt != "" { - details = append(details, pipeline.Detail{Label: "oxfmt", Value: summary.Oxfmt}) - } - - if summary.FluentChains != "" { - details = append(details, pipeline.Detail{Label: "fluent", Value: summary.FluentChains}) - } - - if summary.ValidateSyntax != "" { - details = append(details, pipeline.Detail{Label: "validated", Value: summary.ValidateSyntax}) - } - - return details -} diff --git a/packages/go/driver/internal/app/steps_test.go b/packages/go/driver/internal/app/steps_test.go index 2b3e010..efcf4da 100644 --- a/packages/go/driver/internal/app/steps_test.go +++ b/packages/go/driver/internal/app/steps_test.go @@ -1,89 +1,12 @@ package app import ( - "bytes" - "errors" "fmt" "testing" "go.ollin.sh/fmtkit/driver/internal/pipeline" ) -func detailStrings(details []pipeline.Detail) []string { - out := make([]string, 0, len(details)) - - for _, d := range details { - out = append(out, d.Label+"|"+d.Value) - } - - return out -} - -func assertDetails(t *testing.T, got []pipeline.Detail, want ...string) { - t.Helper() - - if g := fmt.Sprint(detailStrings(got)); g != fmt.Sprint(want) { - t.Fatalf("details mismatch\n--- got ---\n%s\n--- want ---\n%s", g, fmt.Sprint(want)) - } -} - -func TestTSFormatDetails(t *testing.T) { - log := "[blank-lines] processed 3 file(s) in /work, 0 changed\n" + - "Finished in 10ms on 3 files using 8 threads.\n" + - "[fluent-chains] processed 3 file(s) in /work, 1 changed\n" + - "[validate-syntax] checked 3 file(s).\n" - - assertDetails(t, tsFormatDetails(log), - "blank-lines|processed 3 file(s) in /work, 0 changed", - "oxfmt|Finished in 10ms on 3 files using 8 threads.", - "fluent|processed 3 file(s) in /work, 1 changed", - "validated|checked 3 file(s).", - ) -} - -func TestTSFormatDetailsCountsMissing(t *testing.T) { - log := "[sources] path not found, skipping: /work/a\n" + - "[sources] path not found, skipping: /work/b\n" - - assertDetails(t, tsFormatDetails(log), "skipped|2 missing tracked file(s)") -} - -func TestTSLintDetailsResult(t *testing.T) { - assertDetails(t, tsLintDetails("Found 0 warnings and 0 errors.\n"), "oxlint|Found 0 warnings and 0 errors.") -} - -func TestTSLintDetailsNoFiles(t *testing.T) { - assertDetails(t, tsLintDetails("[lint] no TS/Vue files to lint.\n"), "oxlint|no TS/Vue files to lint.") -} - -func TestTSLintDetailsFallback(t *testing.T) { - assertDetails(t, tsLintDetails("nothing interesting\n"), "oxlint|no issues found") -} - -func TestTSExitCodePlainErrorWritesToOutput(t *testing.T) { - var buf bytes.Buffer - - if code := tsExitCode(errors.New("boom"), &buf); code != 1 { - t.Fatalf("tsExitCode = %d, want 1", code) - } - - if buf.String() != "boom\n" { - t.Fatalf("tsExitCode output = %q, want %q", buf.String(), "boom\n") - } -} - -func TestTSExitCodeNil(t *testing.T) { - var buf bytes.Buffer - - if code := tsExitCode(nil, &buf); code != 0 { - t.Fatalf("tsExitCode(nil) = %d, want 0", code) - } - - if buf.Len() != 0 { - t.Fatalf("tsExitCode(nil) wrote %q", buf.String()) - } -} - func TestStepSelectionNormalized(t *testing.T) { if got := (stepSelection{}).normalized(); !got.TS || !got.Go { t.Fatalf("zero selection = %+v, want both set", got) diff --git a/packages/go/driver/internal/app/ts.go b/packages/go/driver/internal/app/ts.go index 65ab0f4..520496f 100644 --- a/packages/go/driver/internal/app/ts.go +++ b/packages/go/driver/internal/app/ts.go @@ -3,25 +3,25 @@ package app import ( "context" - "go.ollin.sh/fmtkit/driver/internal/tsruntime" + "go.ollin.sh/fmtkit/driver/internal/typescript/runtime" ) func (d *deps) runTS(ctx context.Context, paths []string) int { - assets, err := tsruntime.Resolve(d.version) + assets, err := runtime.Resolve(d.version) if err != nil { return d.reportError(err) } - return d.reportError(tsruntime.NewInvoker(assets).RunPipeline(ctx, tsruntime.Request{Scopes: paths, Stdout: d.stdout, Stderr: d.stderr})) + return d.reportError(runtime.NewInvoker(assets).RunPipeline(ctx, runtime.Request{Scopes: paths, Stdout: d.stdout, Stderr: d.stderr})) } func (d *deps) runLint(ctx context.Context, paths []string) int { - assets, err := tsruntime.Resolve(d.version) + assets, err := runtime.Resolve(d.version) if err != nil { return d.reportError(err) } - return d.reportError(tsruntime.NewInvoker(assets).RunLint(ctx, tsruntime.Request{Scopes: paths, Stdout: d.stdout, Stderr: d.stderr})) + return d.reportError(runtime.NewInvoker(assets).RunLint(ctx, runtime.Request{Scopes: paths, Stdout: d.stdout, Stderr: d.stderr})) } diff --git a/packages/go/driver/internal/filetypes/filetypes.go b/packages/go/driver/internal/typescript/filetypes/filetypes.go similarity index 100% rename from packages/go/driver/internal/filetypes/filetypes.go rename to packages/go/driver/internal/typescript/filetypes/filetypes.go diff --git a/packages/go/driver/internal/filetypes/filetypes_test.go b/packages/go/driver/internal/typescript/filetypes/filetypes_test.go similarity index 100% rename from packages/go/driver/internal/filetypes/filetypes_test.go rename to packages/go/driver/internal/typescript/filetypes/filetypes_test.go diff --git a/packages/go/driver/internal/prettierignore/prettierignore.go b/packages/go/driver/internal/typescript/prettierignore/prettierignore.go similarity index 100% rename from packages/go/driver/internal/prettierignore/prettierignore.go rename to packages/go/driver/internal/typescript/prettierignore/prettierignore.go diff --git a/packages/go/driver/internal/prettierignore/prettierignore_test.go b/packages/go/driver/internal/typescript/prettierignore/prettierignore_test.go similarity index 100% rename from packages/go/driver/internal/prettierignore/prettierignore_test.go rename to packages/go/driver/internal/typescript/prettierignore/prettierignore_test.go diff --git a/packages/go/driver/internal/sidecarproto/command.go b/packages/go/driver/internal/typescript/proto/command.go similarity index 99% rename from packages/go/driver/internal/sidecarproto/command.go rename to packages/go/driver/internal/typescript/proto/command.go index b2934b3..3a398bc 100644 --- a/packages/go/driver/internal/sidecarproto/command.go +++ b/packages/go/driver/internal/typescript/proto/command.go @@ -1,4 +1,4 @@ -package sidecarproto +package proto // The command types below build the exact argument vectors each sidecar mode // expects. The bin resolution (which executable to spawn) is the caller's diff --git a/packages/go/driver/internal/sidecarproto/command_test.go b/packages/go/driver/internal/typescript/proto/command_test.go similarity index 99% rename from packages/go/driver/internal/sidecarproto/command_test.go rename to packages/go/driver/internal/typescript/proto/command_test.go index 1d2c62a..aca383d 100644 --- a/packages/go/driver/internal/sidecarproto/command_test.go +++ b/packages/go/driver/internal/typescript/proto/command_test.go @@ -1,4 +1,4 @@ -package sidecarproto +package proto import ( "reflect" diff --git a/packages/go/driver/internal/sidecarproto/sidecarproto.go b/packages/go/driver/internal/typescript/proto/sidecarproto.go similarity index 96% rename from packages/go/driver/internal/sidecarproto/sidecarproto.go rename to packages/go/driver/internal/typescript/proto/sidecarproto.go index 4cc3679..1066382 100644 --- a/packages/go/driver/internal/sidecarproto/sidecarproto.go +++ b/packages/go/driver/internal/typescript/proto/sidecarproto.go @@ -1,4 +1,4 @@ -// Package sidecarproto is the single source of truth for the stringly-typed +// Package proto is the single source of truth for the stringly-typed // wire protocol between the Go driver and the bun-compiled TS sidecar: the // asset filenames, the sidecar's dispatch modes, the environment variables that // override toolchain resolution, the exact argument vectors each mode expects, @@ -8,7 +8,7 @@ // forms and reads these environment names, and CI's smoke test plus the Go // fake-bin tests prove both ends agree. Change a constant here only in lockstep // with packages/ts/sidecar. -package sidecarproto +package proto import "os" diff --git a/packages/go/driver/internal/sidecarproto/sidecarproto_test.go b/packages/go/driver/internal/typescript/proto/sidecarproto_test.go similarity index 99% rename from packages/go/driver/internal/sidecarproto/sidecarproto_test.go rename to packages/go/driver/internal/typescript/proto/sidecarproto_test.go index 6e5ed3b..d5f3a8a 100644 --- a/packages/go/driver/internal/sidecarproto/sidecarproto_test.go +++ b/packages/go/driver/internal/typescript/proto/sidecarproto_test.go @@ -1,4 +1,4 @@ -package sidecarproto +package proto import "testing" diff --git a/packages/go/driver/internal/sidecarproto/summary.go b/packages/go/driver/internal/typescript/proto/summary.go similarity index 99% rename from packages/go/driver/internal/sidecarproto/summary.go rename to packages/go/driver/internal/typescript/proto/summary.go index 25dc007..daaecff 100644 --- a/packages/go/driver/internal/sidecarproto/summary.go +++ b/packages/go/driver/internal/typescript/proto/summary.go @@ -1,4 +1,4 @@ -package sidecarproto +package proto import ( "regexp" diff --git a/packages/go/driver/internal/sidecarproto/summary_test.go b/packages/go/driver/internal/typescript/proto/summary_test.go similarity index 98% rename from packages/go/driver/internal/sidecarproto/summary_test.go rename to packages/go/driver/internal/typescript/proto/summary_test.go index 7b2db58..abbf1b6 100644 --- a/packages/go/driver/internal/sidecarproto/summary_test.go +++ b/packages/go/driver/internal/typescript/proto/summary_test.go @@ -1,4 +1,4 @@ -package sidecarproto +package proto import "testing" diff --git a/packages/go/driver/internal/tsruntime/assets.go b/packages/go/driver/internal/typescript/runtime/assets.go similarity index 88% rename from packages/go/driver/internal/tsruntime/assets.go rename to packages/go/driver/internal/typescript/runtime/assets.go index e737431..94b2b62 100644 --- a/packages/go/driver/internal/tsruntime/assets.go +++ b/packages/go/driver/internal/typescript/runtime/assets.go @@ -1,4 +1,4 @@ -// Package tsruntime manages the self-contained TS toolchain shipped inside +// Package runtime manages the self-contained TS toolchain shipped inside // release binaries: a bun-compiled sidecar plus the oxc-parser, oxfmt, and // oxlint napi bindings. On first use the embedded assets are extracted to a // per-version cache directory and spawned as child processes from there. @@ -6,8 +6,8 @@ // The type split mirrors the three responsibilities: Assets owns the extracted // directory (extraction, caching, lookup); Invoker spawns the toolchain; and // PrettierMigration derives an oxfmt config from a project's Prettier setup. All -// argv and environment construction goes through the sidecarproto package. -package tsruntime +// argv and environment construction goes through the proto package. +package runtime import ( "crypto/sha256" @@ -21,7 +21,7 @@ import ( "sort" "go.ollin.sh/fmtkit/driver/internal/embedded" - "go.ollin.sh/fmtkit/driver/internal/sidecarproto" + "go.ollin.sh/fmtkit/driver/internal/typescript/proto" ) // Assets locates the extracted TS toolchain on disk. @@ -35,19 +35,19 @@ const sentinelName = ".fmtkit-complete" // Sidecar returns the path of the multiplexed toolchain executable. func (a Assets) Sidecar() string { - return filepath.Join(a.Dir, sidecarproto.SidecarName) + return filepath.Join(a.Dir, proto.SidecarName) } // OxfmtConfig returns the bundled oxfmt configuration path, or "" when the // support directory carries none. func (a Assets) OxfmtConfig() string { - return existingFile(filepath.Join(a.Dir, sidecarproto.OxfmtRCName)) + return existingFile(filepath.Join(a.Dir, proto.OxfmtRCName)) } // OxlintConfig returns the bundled oxlint configuration path, or "" when the // support directory carries none. func (a Assets) OxlintConfig() string { - return existingFile(filepath.Join(a.Dir, sidecarproto.OxlintRCName)) + return existingFile(filepath.Join(a.Dir, proto.OxlintRCName)) } func existingFile(path string) string { @@ -62,11 +62,11 @@ func existingFile(path string) string { // user cache on first use. version tells extractions of different releases // apart; dev builds derive a digest from the assets instead. func Resolve(version string) (Assets, error) { - if dir := os.Getenv(sidecarproto.SupportDirEnv); dir != "" { + if dir := os.Getenv(proto.SupportDirEnv); dir != "" { assets := Assets{Dir: dir} if existingFile(assets.Sidecar()) == "" { - return Assets{}, fmt.Errorf("%s (%s) does not contain %s", sidecarproto.SupportDirEnv, dir, sidecarproto.SidecarName) + return Assets{}, fmt.Errorf("%s (%s) does not contain %s", proto.SupportDirEnv, dir, proto.SidecarName) } return assets, nil @@ -77,7 +77,7 @@ func Resolve(version string) (Assets, error) { if !ok { return Assets{}, errors.New( "this fmtkit build carries no TS toolchain (built without the fmtkit_sidecar tag); " + - "point " + sidecarproto.SupportDirEnv + " at a staged toolchain directory " + + "point " + proto.SupportDirEnv + " at a staged toolchain directory " + "(see packages/ts/infra/stage-ts-assets.sh), or use a release binary", ) } @@ -160,7 +160,7 @@ func extract(dst string, assets fs.FS) error { mode := os.FileMode(0o644) - if entry.Name() == sidecarproto.SidecarName { + if entry.Name() == proto.SidecarName { mode = 0o755 } diff --git a/packages/go/driver/internal/tsruntime/invoker.go b/packages/go/driver/internal/typescript/runtime/invoker.go similarity index 95% rename from packages/go/driver/internal/tsruntime/invoker.go rename to packages/go/driver/internal/typescript/runtime/invoker.go index d53142a..82e19c9 100644 --- a/packages/go/driver/internal/tsruntime/invoker.go +++ b/packages/go/driver/internal/typescript/runtime/invoker.go @@ -1,4 +1,4 @@ -package tsruntime +package runtime import ( "context" @@ -9,8 +9,8 @@ import ( "path/filepath" "go.ollin.sh/fmtkit/driver/internal/gitfiles" - "go.ollin.sh/fmtkit/driver/internal/sidecarproto" - "go.ollin.sh/fmtkit/driver/internal/sourcefiles" + "go.ollin.sh/fmtkit/driver/internal/typescript/proto" + "go.ollin.sh/fmtkit/driver/internal/typescript/sourcefiles" ) // Request describes one TS toolchain invocation. @@ -35,13 +35,13 @@ type Request struct { // deep in the call paths. type Invoker struct { Assets Assets - Env sidecarproto.Overrides + Env proto.Overrides } // NewInvoker builds an Invoker for the given assets, reading the environment // overrides once. func NewInvoker(a Assets) Invoker { - return Invoker{Assets: a, Env: sidecarproto.ReadOverrides()} + return Invoker{Assets: a, Env: proto.ReadOverrides()} } // RunPipeline runs the full TS/Vue formatting pipeline (blank-lines -> oxfmt @@ -77,7 +77,7 @@ func (i Invoker) RunPipeline(ctx context.Context, req Request) error { oxfmtBin = i.Assets.Sidecar() } - command := sidecarproto.PipelineCommand{ + command := proto.PipelineCommand{ OxfmtBin: oxfmtBin, OxfmtConfig: i.oxfmtConfigFor(ctx, cwd, req.Stderr), FormatFiles: formatFiles, @@ -119,7 +119,7 @@ func (i Invoker) RunLint(ctx context.Context, req Request) error { bin = i.Assets.Sidecar() } - command := sidecarproto.OxlintCommand{ + command := proto.OxlintCommand{ ViaSidecar: viaSidecar, Fix: req.Fix, Config: i.oxlintConfigFor(cwd), diff --git a/packages/go/driver/internal/tsruntime/prettier.go b/packages/go/driver/internal/typescript/runtime/prettier.go similarity index 95% rename from packages/go/driver/internal/tsruntime/prettier.go rename to packages/go/driver/internal/typescript/runtime/prettier.go index 326604c..9901291 100644 --- a/packages/go/driver/internal/tsruntime/prettier.go +++ b/packages/go/driver/internal/typescript/runtime/prettier.go @@ -1,4 +1,4 @@ -package tsruntime +package runtime import ( "context" @@ -11,7 +11,7 @@ import ( "os/exec" "path/filepath" - "go.ollin.sh/fmtkit/driver/internal/sidecarproto" + "go.ollin.sh/fmtkit/driver/internal/typescript/proto" ) // PrettierMigration derives an oxfmt config from a project's Prettier setup by @@ -20,7 +20,7 @@ import ( // OXFMT_BIN override). type PrettierMigration struct { Assets Assets - Env sidecarproto.Overrides + Env proto.Overrides } // prettierConfigNames are the standalone Prettier configuration filenames, in @@ -145,7 +145,7 @@ func (m PrettierMigration) migrate(ctx context.Context, source string) ([]byte, } bin, viaSidecar := m.oxfmtExecutable() - args := sidecarproto.MigrateCommand{ViaSidecar: viaSidecar}.Argv() + args := proto.MigrateCommand{ViaSidecar: viaSidecar}.Argv() cmd := exec.CommandContext(ctx, bin, args...) cmd.Dir = dir @@ -156,7 +156,7 @@ func (m PrettierMigration) migrate(ctx context.Context, source string) ([]byte, return nil, fmt.Errorf("oxfmt --migrate=prettier: %w", err) } - derived, err := os.ReadFile(filepath.Join(dir, sidecarproto.OxfmtRCName)) + derived, err := os.ReadFile(filepath.Join(dir, proto.OxfmtRCName)) if err != nil { return nil, fmt.Errorf("read migrated config: %w", err) diff --git a/packages/go/driver/internal/tsruntime/prettier_internal_test.go b/packages/go/driver/internal/typescript/runtime/prettier_internal_test.go similarity index 90% rename from packages/go/driver/internal/tsruntime/prettier_internal_test.go rename to packages/go/driver/internal/typescript/runtime/prettier_internal_test.go index b909fe3..dd6fb8b 100644 --- a/packages/go/driver/internal/tsruntime/prettier_internal_test.go +++ b/packages/go/driver/internal/typescript/runtime/prettier_internal_test.go @@ -1,4 +1,4 @@ -package tsruntime +package runtime import ( "context" @@ -7,7 +7,7 @@ import ( "strings" "testing" - "go.ollin.sh/fmtkit/driver/internal/sidecarproto" + "go.ollin.sh/fmtkit/driver/internal/typescript/proto" ) func TestOxfmtExecutableDefaultsToSidecar(t *testing.T) { @@ -25,7 +25,7 @@ func TestOxfmtExecutableDefaultsToSidecar(t *testing.T) { } func TestOxfmtExecutableHonorsOxfmtBin(t *testing.T) { - migration := PrettierMigration{Env: sidecarproto.Overrides{OxfmtBin: "/usr/bin/oxfmt"}} + migration := PrettierMigration{Env: proto.Overrides{OxfmtBin: "/usr/bin/oxfmt"}} bin, viaSidecar := migration.oxfmtExecutable() @@ -82,7 +82,7 @@ func TestDerivedConfigWarnsWhenCacheWriteFails(t *testing.T) { t.Fatalf("write prettier config: %v", err) } - migration := PrettierMigration{Assets: support, Env: sidecarproto.Overrides{OxfmtBin: oxfmt}} + migration := PrettierMigration{Assets: support, Env: proto.Overrides{OxfmtBin: oxfmt}} var stderr strings.Builder @@ -111,7 +111,7 @@ func TestDerivedConfigWarnsWhenMigrationWritesNoConfig(t *testing.T) { t.Fatalf("write prettier config: %v", err) } - migration := PrettierMigration{Assets: support, Env: sidecarproto.Overrides{OxfmtBin: silent}} + migration := PrettierMigration{Assets: support, Env: proto.Overrides{OxfmtBin: silent}} var stderr strings.Builder diff --git a/packages/go/driver/internal/tsruntime/prettier_test.go b/packages/go/driver/internal/typescript/runtime/prettier_test.go similarity index 90% rename from packages/go/driver/internal/tsruntime/prettier_test.go rename to packages/go/driver/internal/typescript/runtime/prettier_test.go index 517465a..ec84b2a 100644 --- a/packages/go/driver/internal/tsruntime/prettier_test.go +++ b/packages/go/driver/internal/typescript/runtime/prettier_test.go @@ -1,4 +1,4 @@ -package tsruntime +package runtime import ( "bytes" @@ -8,7 +8,7 @@ import ( "strings" "testing" - "go.ollin.sh/fmtkit/driver/internal/sidecarproto" + "go.ollin.sh/fmtkit/driver/internal/typescript/proto" ) // writeMigrateStub creates a fake oxfmt that, on --migrate=prettier, writes an @@ -154,9 +154,9 @@ func TestOxfmtConfigForDerivesFromPrettier(t *testing.T) { t.Fatalf("write prettier config: %v", err) } - t.Setenv(sidecarproto.OxfmtBinEnv, oxfmt) + t.Setenv(proto.OxfmtBinEnv, oxfmt) - env := sidecarproto.ReadOverrides() + env := proto.ReadOverrides() var stderr bytes.Buffer @@ -193,9 +193,9 @@ func TestOxfmtConfigForCachesDerivedConfig(t *testing.T) { t.Fatalf("write prettier config: %v", err) } - t.Setenv(sidecarproto.OxfmtBinEnv, oxfmt) + t.Setenv(proto.OxfmtBinEnv, oxfmt) - env := sidecarproto.ReadOverrides() + env := proto.ReadOverrides() var stderr bytes.Buffer @@ -225,9 +225,9 @@ func TestOxfmtConfigForRemigratesWhenConfigChanges(t *testing.T) { t.Fatalf("write prettier config: %v", err) } - t.Setenv(sidecarproto.OxfmtBinEnv, oxfmt) + t.Setenv(proto.OxfmtBinEnv, oxfmt) - env := sidecarproto.ReadOverrides() + env := proto.ReadOverrides() var stderr bytes.Buffer @@ -254,7 +254,7 @@ func TestOxfmtConfigForPrecedence(t *testing.T) { oxfmt := filepath.Join(t.TempDir(), "oxfmt") writeMigrateStub(t, oxfmt) - t.Setenv(sidecarproto.OxfmtBinEnv, oxfmt) + t.Setenv(proto.OxfmtBinEnv, oxfmt) t.Run("project .oxfmtrc beats prettier", func(t *testing.T) { cwd := t.TempDir() @@ -269,7 +269,7 @@ func TestOxfmtConfigForPrecedence(t *testing.T) { var stderr bytes.Buffer - if got := (Invoker{Assets: support, Env: sidecarproto.ReadOverrides()}).oxfmtConfigFor(context.Background(), cwd, &stderr); got != "" { + if got := (Invoker{Assets: support, Env: proto.ReadOverrides()}).oxfmtConfigFor(context.Background(), cwd, &stderr); got != "" { t.Fatalf("expected auto-discovery signal for project config, got %q", got) } }) @@ -283,7 +283,7 @@ func TestOxfmtConfigForPrecedence(t *testing.T) { var stderr bytes.Buffer - got := Invoker{Assets: support, Env: sidecarproto.ReadOverrides()}.oxfmtConfigFor(context.Background(), cwd, &stderr) + got := Invoker{Assets: support, Env: proto.ReadOverrides()}.oxfmtConfigFor(context.Background(), cwd, &stderr) if !strings.HasPrefix(got, filepath.Join(support.Dir, "prettier-derived")) { t.Fatalf("expected derived config, got %q", got) @@ -295,7 +295,7 @@ func TestOxfmtConfigForPrecedence(t *testing.T) { var stderr bytes.Buffer - if got := (Invoker{Assets: support, Env: sidecarproto.ReadOverrides()}).oxfmtConfigFor(context.Background(), cwd, &stderr); got != support.OxfmtConfig() { + if got := (Invoker{Assets: support, Env: proto.ReadOverrides()}).oxfmtConfigFor(context.Background(), cwd, &stderr); got != support.OxfmtConfig() { t.Fatalf("expected bundled config %q, got %q", support.OxfmtConfig(), got) } }) @@ -313,7 +313,7 @@ func TestOxfmtConfigForPrecedence(t *testing.T) { t.Fatalf("write override config: %v", err) } - env := sidecarproto.ReadOverrides() + env := proto.ReadOverrides() env.OxfmtConfig = override if got := (Invoker{Assets: support, Env: env}).oxfmtConfigFor(context.Background(), cwd, &bytes.Buffer{}); got != override { @@ -342,11 +342,11 @@ func TestOxfmtConfigForFallsBackWhenMigrationFails(t *testing.T) { t.Fatalf("write prettier config: %v", err) } - t.Setenv(sidecarproto.OxfmtBinEnv, failing) + t.Setenv(proto.OxfmtBinEnv, failing) var stderr bytes.Buffer - got := Invoker{Assets: support, Env: sidecarproto.ReadOverrides()}.oxfmtConfigFor(context.Background(), cwd, &stderr) + got := Invoker{Assets: support, Env: proto.ReadOverrides()}.oxfmtConfigFor(context.Background(), cwd, &stderr) if got != support.OxfmtConfig() { t.Fatalf("expected bundled fallback %q, got %q", support.OxfmtConfig(), got) diff --git a/packages/go/driver/internal/tsruntime/run_test.go b/packages/go/driver/internal/typescript/runtime/run_test.go similarity index 93% rename from packages/go/driver/internal/tsruntime/run_test.go rename to packages/go/driver/internal/typescript/runtime/run_test.go index 0faabae..cea8122 100644 --- a/packages/go/driver/internal/tsruntime/run_test.go +++ b/packages/go/driver/internal/typescript/runtime/run_test.go @@ -1,4 +1,4 @@ -package tsruntime +package runtime import ( "bytes" @@ -10,7 +10,7 @@ import ( "strings" "testing" - "go.ollin.sh/fmtkit/driver/internal/sidecarproto" + "go.ollin.sh/fmtkit/driver/internal/typescript/proto" ) // writeStub creates an executable that echoes its argv, one per line, so @@ -55,7 +55,7 @@ func supportWithStub(t *testing.T) Assets { dir := t.TempDir() - writeStub(t, filepath.Join(dir, sidecarproto.SidecarName)) + writeStub(t, filepath.Join(dir, proto.SidecarName)) return Assets{Dir: dir} } @@ -74,7 +74,7 @@ func TestRunPipelineInvokesSidecar(t *testing.T) { t.Fatalf("write bundled config: %v", err) } - t.Setenv(sidecarproto.SourcesCwdEnv, repo) + t.Setenv(proto.SourcesCwdEnv, repo) var stdout, stderr bytes.Buffer @@ -122,7 +122,7 @@ func TestRunPipelineSkipsBundledConfigWhenProjectHasOne(t *testing.T) { t.Fatalf("write bundled config: %v", err) } - t.Setenv(sidecarproto.SourcesCwdEnv, repo) + t.Setenv(proto.SourcesCwdEnv, repo) var stdout, stderr bytes.Buffer @@ -140,7 +140,7 @@ func TestRunPipelineReportsMissingScopes(t *testing.T) { support := supportWithStub(t) - t.Setenv(sidecarproto.SourcesCwdEnv, repo) + t.Setenv(proto.SourcesCwdEnv, repo) var stdout, stderr bytes.Buffer @@ -170,7 +170,7 @@ func TestRunLintInvokesOxlintMode(t *testing.T) { t.Fatalf("write bundled config: %v", err) } - t.Setenv(sidecarproto.SourcesCwdEnv, repo) + t.Setenv(proto.SourcesCwdEnv, repo) var stdout, stderr bytes.Buffer @@ -206,7 +206,7 @@ func TestRunLintFixPassesFixFlag(t *testing.T) { t.Fatalf("write bundled config: %v", err) } - t.Setenv(sidecarproto.SourcesCwdEnv, repo) + t.Setenv(proto.SourcesCwdEnv, repo) var stdout, stderr bytes.Buffer @@ -246,7 +246,7 @@ func TestRunLintSkipsBundledConfigWhenProjectHasOne(t *testing.T) { t.Fatalf("write bundled config: %v", err) } - t.Setenv(sidecarproto.SourcesCwdEnv, repo) + t.Setenv(proto.SourcesCwdEnv, repo) var stdout, stderr bytes.Buffer @@ -264,7 +264,7 @@ func TestRunLintSkipsSpawnWithoutFiles(t *testing.T) { support := Assets{Dir: t.TempDir()} // no sidecar: spawning would fail - t.Setenv(sidecarproto.SourcesCwdEnv, repo) + t.Setenv(proto.SourcesCwdEnv, repo) var stdout, stderr bytes.Buffer @@ -288,7 +288,7 @@ func TestRunLintSkipsSpawnForFormatOnlyDocuments(t *testing.T) { support := Assets{Dir: t.TempDir()} // no sidecar: spawning would fail - t.Setenv(sidecarproto.SourcesCwdEnv, repo) + t.Setenv(proto.SourcesCwdEnv, repo) var stdout, stderr bytes.Buffer @@ -310,8 +310,8 @@ func TestRunLintHonorsOxlintBinOverride(t *testing.T) { writeStub(t, override) - t.Setenv(sidecarproto.SourcesCwdEnv, repo) - t.Setenv(sidecarproto.OxlintBinEnv, override) + t.Setenv(proto.SourcesCwdEnv, repo) + t.Setenv(proto.OxlintBinEnv, override) var stdout, stderr bytes.Buffer diff --git a/packages/go/driver/internal/tsruntime/support_test.go b/packages/go/driver/internal/typescript/runtime/support_test.go similarity index 82% rename from packages/go/driver/internal/tsruntime/support_test.go rename to packages/go/driver/internal/typescript/runtime/support_test.go index 89af442..f058664 100644 --- a/packages/go/driver/internal/tsruntime/support_test.go +++ b/packages/go/driver/internal/typescript/runtime/support_test.go @@ -1,4 +1,4 @@ -package tsruntime +package runtime import ( "os" @@ -6,19 +6,19 @@ import ( "testing" "testing/fstest" - "go.ollin.sh/fmtkit/driver/internal/sidecarproto" + "go.ollin.sh/fmtkit/driver/internal/typescript/proto" ) // fakeAssets mirrors a directory staged by stage-ts-assets.sh: the bindings // and the sidecar, plus the configs that ride along with them. func fakeAssets() fstest.MapFS { return fstest.MapFS{ - sidecarproto.SidecarName: &fstest.MapFile{Data: []byte("#!/bin/sh\n"), Mode: 0o755}, - "oxc-parser.node": &fstest.MapFile{Data: []byte("parser")}, - "oxfmt.node": &fstest.MapFile{Data: []byte("fmt")}, - "oxlint.node": &fstest.MapFile{Data: []byte("lint")}, - ".oxfmtrc.json": &fstest.MapFile{Data: []byte("{}")}, - ".oxlintrc.json": &fstest.MapFile{Data: []byte("{}")}, + proto.SidecarName: &fstest.MapFile{Data: []byte("#!/bin/sh\n"), Mode: 0o755}, + "oxc-parser.node": &fstest.MapFile{Data: []byte("parser")}, + "oxfmt.node": &fstest.MapFile{Data: []byte("fmt")}, + "oxlint.node": &fstest.MapFile{Data: []byte("lint")}, + ".oxfmtrc.json": &fstest.MapFile{Data: []byte("{}")}, + ".oxlintrc.json": &fstest.MapFile{Data: []byte("{}")}, } } @@ -99,11 +99,11 @@ func TestExtractOnceLosingRaceKeepsWinner(t *testing.T) { func TestResolvePrefersSupportDirEnv(t *testing.T) { dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, sidecarproto.SidecarName), []byte("#!/bin/sh\n"), 0o755); err != nil { + if err := os.WriteFile(filepath.Join(dir, proto.SidecarName), []byte("#!/bin/sh\n"), 0o755); err != nil { t.Fatalf("write sidecar: %v", err) } - t.Setenv(sidecarproto.SupportDirEnv, dir) + t.Setenv(proto.SupportDirEnv, dir) support, err := Resolve("v1.0.0") @@ -117,7 +117,7 @@ func TestResolvePrefersSupportDirEnv(t *testing.T) { } func TestResolveRejectsSupportDirWithoutSidecar(t *testing.T) { - t.Setenv(sidecarproto.SupportDirEnv, t.TempDir()) + t.Setenv(proto.SupportDirEnv, t.TempDir()) if _, err := Resolve("v1.0.0"); err == nil { t.Fatal("expected error for support dir without sidecar") diff --git a/packages/go/driver/internal/sourcefiles/command.go b/packages/go/driver/internal/typescript/sourcefiles/command.go similarity index 100% rename from packages/go/driver/internal/sourcefiles/command.go rename to packages/go/driver/internal/typescript/sourcefiles/command.go diff --git a/packages/go/driver/internal/sourcefiles/command_test.go b/packages/go/driver/internal/typescript/sourcefiles/command_test.go similarity index 100% rename from packages/go/driver/internal/sourcefiles/command_test.go rename to packages/go/driver/internal/typescript/sourcefiles/command_test.go diff --git a/packages/go/driver/internal/sourcefiles/prettierignore_test.go b/packages/go/driver/internal/typescript/sourcefiles/prettierignore_test.go similarity index 100% rename from packages/go/driver/internal/sourcefiles/prettierignore_test.go rename to packages/go/driver/internal/typescript/sourcefiles/prettierignore_test.go diff --git a/packages/go/driver/internal/sourcefiles/sourcefiles.go b/packages/go/driver/internal/typescript/sourcefiles/sourcefiles.go similarity index 96% rename from packages/go/driver/internal/sourcefiles/sourcefiles.go rename to packages/go/driver/internal/typescript/sourcefiles/sourcefiles.go index 07a7367..775e16f 100644 --- a/packages/go/driver/internal/sourcefiles/sourcefiles.go +++ b/packages/go/driver/internal/typescript/sourcefiles/sourcefiles.go @@ -11,9 +11,9 @@ import ( "path/filepath" "slices" - "go.ollin.sh/fmtkit/driver/internal/filetypes" "go.ollin.sh/fmtkit/driver/internal/gitfiles" - "go.ollin.sh/fmtkit/driver/internal/prettierignore" + "go.ollin.sh/fmtkit/driver/internal/typescript/filetypes" + "go.ollin.sh/fmtkit/driver/internal/typescript/prettierignore" ) // Collector composes git discovery, the extension taxonomy, and the diff --git a/packages/go/driver/internal/sourcefiles/sourcefiles_test.go b/packages/go/driver/internal/typescript/sourcefiles/sourcefiles_test.go similarity index 100% rename from packages/go/driver/internal/sourcefiles/sourcefiles_test.go rename to packages/go/driver/internal/typescript/sourcefiles/sourcefiles_test.go diff --git a/packages/go/driver/internal/typescript/step.go b/packages/go/driver/internal/typescript/step.go new file mode 100644 index 0000000..a9f5c82 --- /dev/null +++ b/packages/go/driver/internal/typescript/step.go @@ -0,0 +1,192 @@ +// Package typescript is the TS/Vue lane: it lints (oxlint) and formats (the +// oxfmt pipeline plus the project passes) TS, Vue, HTML, and Markdown files, +// contributing the lint and format steps to the pipeline. The lane's machinery +// is split across subpackages — runtime (toolchain extraction and spawning), +// proto (the wire protocol), sourcefiles/filetypes/prettierignore (file +// discovery), and embedded (the assets baked into release binaries) — while +// this package builds the pipeline steps that drive them. +package typescript + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os/exec" + "strings" + + "go.ollin.sh/fmtkit/driver/internal/gitfiles" + "go.ollin.sh/fmtkit/driver/internal/pipeline" + "go.ollin.sh/fmtkit/driver/internal/toolchain" + "go.ollin.sh/fmtkit/driver/internal/typescript/proto" + "go.ollin.sh/fmtkit/driver/internal/typescript/runtime" +) + +// Toolchain is the TS/Vue lane. +type Toolchain struct{} + +type lintStep struct { + version string + paths []string + selection gitfiles.Selection +} + +type formatStep struct { + version string + paths []string + selection gitfiles.Selection +} + +// New builds the TS toolchain. +func New() Toolchain { return Toolchain{} } + +// Name is the lane's selector, matching the --ts flag. +func (Toolchain) Name() string { return "ts" } + +// Steps returns the TS lane's ordered steps. Lint runs first so the formatting +// passes normalize whatever oxlint rewrites. +func (Toolchain) Steps(req toolchain.Request) []pipeline.Step { + return []pipeline.Step{ + LintStep(req.Version, req.Paths, req.Selection), + FormatStep(req.Version, req.Paths, req.Selection), + } +} + +// LintStep builds the step that lints TS/Vue files, applying oxlint's safe +// fixes (--fix). +func LintStep(version string, paths []string, selection gitfiles.Selection) pipeline.Step { + return lintStep{version: version, paths: paths, selection: selection} +} + +// FormatStep builds the step that runs the full TS/Vue formatting pipeline +// (oxfmt plus the project passes). +func FormatStep(version string, paths []string, selection gitfiles.Selection) pipeline.Step { + return formatStep{version: version, paths: paths, selection: selection} +} + +// Driver-owned bookkeeping lines the TS steps recognize in their captured +// output. The sidecar's own wire lines are parsed by the proto package; these +// are notices the Go driver prints around the sidecar, so they stay here. +const ( + sourcesMissingPrefix = "[sources] path not found, skipping:" + lintNothingToLintLine = "[lint] no TS/Vue files to lint." +) + +func (s lintStep) Label() string { return "Running TS/Vue lint" } + +func (s lintStep) Run(ctx context.Context, output io.Writer) pipeline.Result { + var captured bytes.Buffer + + err := invoke(s.version, io.MultiWriter(output, &captured), func(invoker runtime.Invoker, w io.Writer) error { + return invoker.RunLint(ctx, runtime.Request{Scopes: s.paths, Selection: s.selection, Fix: true, Stdout: w, Stderr: w}) + }) + + if code := exitCode(err, output); code != 0 { + return pipeline.Result{ExitCode: code} + } + + return pipeline.Result{Details: lintDetails(captured.String())} +} + +func (s formatStep) Label() string { return "Running TS/Vue formatting" } + +func (s formatStep) Run(ctx context.Context, output io.Writer) pipeline.Result { + var captured bytes.Buffer + + err := invoke(s.version, io.MultiWriter(output, &captured), func(invoker runtime.Invoker, w io.Writer) error { + return invoker.RunPipeline(ctx, runtime.Request{Scopes: s.paths, Selection: s.selection, Stdout: w, Stderr: w}) + }) + + if code := exitCode(err, output); code != 0 { + return pipeline.Result{ExitCode: code} + } + + return pipeline.Result{Details: formatDetails(captured.String())} +} + +// invoke resolves the TS toolchain and invokes it through spawn, which receives +// the constructed Invoker and the writer to stream tool output to. +func invoke(version string, output io.Writer, spawn func(runtime.Invoker, io.Writer) error) error { + assets, err := runtime.Resolve(version) + + if err != nil { + return err + } + + return spawn(runtime.NewInvoker(assets), output) +} + +// exitCode maps a TS step error to its exit code. Failures that never produced +// tool output (a missing sidecar, an unreadable working tree) surface their +// message through output so they are visible both live and in the quiet failure +// dump. +func exitCode(err error, output io.Writer) int { + if err == nil { + return 0 + } + + var exit *exec.ExitError + + if errors.As(err, &exit) { + return exit.ExitCode() + } + + _, _ = io.WriteString(output, err.Error()+"\n") + + return 1 +} + +// lintDetails derives the oxlint summary line. A driver "no files" notice wins; +// otherwise oxlint's own result line; otherwise a clean fallback. +func lintDetails(log string) []pipeline.Detail { + for _, line := range strings.Split(log, "\n") { + if strings.HasPrefix(line, lintNothingToLintLine) { + return []pipeline.Detail{{Label: "oxlint", Value: strings.TrimPrefix(lintNothingToLintLine, "[lint] ")}} + } + } + + if result := proto.ParseLintSummary(log).Result; result != "" { + return []pipeline.Detail{{Label: "oxlint", Value: result}} + } + + return []pipeline.Detail{{Label: "oxlint", Value: "no issues found"}} +} + +// formatDetails derives the TS pipeline's detail lines from the sidecar's +// progress output plus the driver's missing-source notices. +func formatDetails(log string) []pipeline.Detail { + summary := proto.ParsePipelineSummary(log) + + var details []pipeline.Detail + + if summary.BlankLines != "" { + details = append(details, pipeline.Detail{Label: "blank-lines", Value: summary.BlankLines}) + } + + missing := 0 + + for _, line := range strings.Split(log, "\n") { + if strings.HasPrefix(line, sourcesMissingPrefix) { + missing++ + } + } + + if missing > 0 { + details = append(details, pipeline.Detail{Label: "skipped", Value: fmt.Sprintf("%d missing tracked file(s)", missing)}) + } + + if summary.Oxfmt != "" { + details = append(details, pipeline.Detail{Label: "oxfmt", Value: summary.Oxfmt}) + } + + if summary.FluentChains != "" { + details = append(details, pipeline.Detail{Label: "fluent", Value: summary.FluentChains}) + } + + if summary.ValidateSyntax != "" { + details = append(details, pipeline.Detail{Label: "validated", Value: summary.ValidateSyntax}) + } + + return details +} diff --git a/packages/go/driver/internal/typescript/step_test.go b/packages/go/driver/internal/typescript/step_test.go new file mode 100644 index 0000000..de24053 --- /dev/null +++ b/packages/go/driver/internal/typescript/step_test.go @@ -0,0 +1,104 @@ +package typescript + +import ( + "bytes" + "errors" + "fmt" + "testing" + + "go.ollin.sh/fmtkit/driver/internal/pipeline" + "go.ollin.sh/fmtkit/driver/internal/toolchain" +) + +func detailStrings(details []pipeline.Detail) []string { + out := make([]string, 0, len(details)) + + for _, d := range details { + out = append(out, d.Label+"|"+d.Value) + } + + return out +} + +func assertDetails(t *testing.T, got []pipeline.Detail, want ...string) { + t.Helper() + + if g := fmt.Sprint(detailStrings(got)); g != fmt.Sprint(want) { + t.Fatalf("details mismatch\n--- got ---\n%s\n--- want ---\n%s", g, fmt.Sprint(want)) + } +} + +func TestFormatDetails(t *testing.T) { + log := "[blank-lines] processed 3 file(s) in /work, 0 changed\n" + + "Finished in 10ms on 3 files using 8 threads.\n" + + "[fluent-chains] processed 3 file(s) in /work, 1 changed\n" + + "[validate-syntax] checked 3 file(s).\n" + + assertDetails(t, formatDetails(log), + "blank-lines|processed 3 file(s) in /work, 0 changed", + "oxfmt|Finished in 10ms on 3 files using 8 threads.", + "fluent|processed 3 file(s) in /work, 1 changed", + "validated|checked 3 file(s).", + ) +} + +func TestFormatDetailsCountsMissing(t *testing.T) { + log := "[sources] path not found, skipping: /work/a\n" + + "[sources] path not found, skipping: /work/b\n" + + assertDetails(t, formatDetails(log), "skipped|2 missing tracked file(s)") +} + +func TestLintDetailsResult(t *testing.T) { + assertDetails(t, lintDetails("Found 0 warnings and 0 errors.\n"), "oxlint|Found 0 warnings and 0 errors.") +} + +func TestLintDetailsNoFiles(t *testing.T) { + assertDetails(t, lintDetails("[lint] no TS/Vue files to lint.\n"), "oxlint|no TS/Vue files to lint.") +} + +func TestLintDetailsFallback(t *testing.T) { + assertDetails(t, lintDetails("nothing interesting\n"), "oxlint|no issues found") +} + +func TestExitCodePlainErrorWritesToOutput(t *testing.T) { + var buf bytes.Buffer + + if code := exitCode(errors.New("boom"), &buf); code != 1 { + t.Fatalf("exitCode = %d, want 1", code) + } + + if buf.String() != "boom\n" { + t.Fatalf("exitCode output = %q, want %q", buf.String(), "boom\n") + } +} + +func TestExitCodeNil(t *testing.T) { + var buf bytes.Buffer + + if code := exitCode(nil, &buf); code != 0 { + t.Fatalf("exitCode(nil) = %d, want 0", code) + } + + if buf.Len() != 0 { + t.Fatalf("exitCode(nil) wrote %q", buf.String()) + } +} + +func TestSteps(t *testing.T) { + steps := New().Steps(toolchain.Request{Version: "dev", Paths: []string{"."}}) + + labels := make([]string, 0, len(steps)) + + for _, s := range steps { + labels = append(labels, s.Label()) + } + + if got := fmt.Sprint(labels); got != fmt.Sprint([]string{"Running TS/Vue lint", "Running TS/Vue formatting"}) { + t.Fatalf("step labels = %s, want [Running TS/Vue lint, Running TS/Vue formatting]", got) + } + + if got := New().Name(); got != "ts" { + t.Fatalf("Name = %q, want ts", got) + } +} From b457b4093204a2c4dc9f9d9b068290a7e415d048 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 24 Jul 2026 12:28:34 +0800 Subject: [PATCH 4/6] =?UTF-8?q?refactor(go):=20G7=20=E2=80=94=20move=20emb?= =?UTF-8?q?edded=20under=20typescript/=20and=20retarget=20staging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- infra/task.sh | 6 +++--- .../go/driver/internal/{ => typescript}/embedded/doc.go | 0 .../{ => typescript}/embedded/sidecar_darwin_amd64.go | 0 .../{ => typescript}/embedded/sidecar_darwin_arm64.go | 0 .../internal/{ => typescript}/embedded/sidecar_dev.go | 0 .../{ => typescript}/embedded/sidecar_linux_amd64.go | 0 .../{ => typescript}/embedded/sidecar_linux_arm64.go | 0 packages/go/driver/internal/typescript/runtime/assets.go | 6 +++--- packages/ts/infra/stage-ts-assets.sh | 6 +++--- 10 files changed, 10 insertions(+), 10 deletions(-) rename packages/go/driver/internal/{ => typescript}/embedded/doc.go (100%) rename packages/go/driver/internal/{ => typescript}/embedded/sidecar_darwin_amd64.go (100%) rename packages/go/driver/internal/{ => typescript}/embedded/sidecar_darwin_arm64.go (100%) rename packages/go/driver/internal/{ => typescript}/embedded/sidecar_dev.go (100%) rename packages/go/driver/internal/{ => typescript}/embedded/sidecar_linux_amd64.go (100%) rename packages/go/driver/internal/{ => typescript}/embedded/sidecar_linux_arm64.go (100%) diff --git a/.gitignore b/.gitignore index 2df9875..c0d5ec1 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/infra/task.sh b/infra/task.sh index 140f56c..ccdcf12 100755 --- a/infra/task.sh +++ b/infra/task.sh @@ -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 @@ -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 }')" diff --git a/packages/go/driver/internal/embedded/doc.go b/packages/go/driver/internal/typescript/embedded/doc.go similarity index 100% rename from packages/go/driver/internal/embedded/doc.go rename to packages/go/driver/internal/typescript/embedded/doc.go diff --git a/packages/go/driver/internal/embedded/sidecar_darwin_amd64.go b/packages/go/driver/internal/typescript/embedded/sidecar_darwin_amd64.go similarity index 100% rename from packages/go/driver/internal/embedded/sidecar_darwin_amd64.go rename to packages/go/driver/internal/typescript/embedded/sidecar_darwin_amd64.go diff --git a/packages/go/driver/internal/embedded/sidecar_darwin_arm64.go b/packages/go/driver/internal/typescript/embedded/sidecar_darwin_arm64.go similarity index 100% rename from packages/go/driver/internal/embedded/sidecar_darwin_arm64.go rename to packages/go/driver/internal/typescript/embedded/sidecar_darwin_arm64.go diff --git a/packages/go/driver/internal/embedded/sidecar_dev.go b/packages/go/driver/internal/typescript/embedded/sidecar_dev.go similarity index 100% rename from packages/go/driver/internal/embedded/sidecar_dev.go rename to packages/go/driver/internal/typescript/embedded/sidecar_dev.go diff --git a/packages/go/driver/internal/embedded/sidecar_linux_amd64.go b/packages/go/driver/internal/typescript/embedded/sidecar_linux_amd64.go similarity index 100% rename from packages/go/driver/internal/embedded/sidecar_linux_amd64.go rename to packages/go/driver/internal/typescript/embedded/sidecar_linux_amd64.go diff --git a/packages/go/driver/internal/embedded/sidecar_linux_arm64.go b/packages/go/driver/internal/typescript/embedded/sidecar_linux_arm64.go similarity index 100% rename from packages/go/driver/internal/embedded/sidecar_linux_arm64.go rename to packages/go/driver/internal/typescript/embedded/sidecar_linux_arm64.go diff --git a/packages/go/driver/internal/typescript/runtime/assets.go b/packages/go/driver/internal/typescript/runtime/assets.go index 94b2b62..5dda7d8 100644 --- a/packages/go/driver/internal/typescript/runtime/assets.go +++ b/packages/go/driver/internal/typescript/runtime/assets.go @@ -20,7 +20,7 @@ import ( "path/filepath" "sort" - "go.ollin.sh/fmtkit/driver/internal/embedded" + "go.ollin.sh/fmtkit/driver/internal/typescript/embedded" "go.ollin.sh/fmtkit/driver/internal/typescript/proto" ) @@ -29,8 +29,8 @@ type Assets struct { Dir string } -// sentinelName marks a completed extraction; it is tsruntime's own bookkeeping, -// not part of the sidecar wire protocol. +// sentinelName marks a completed extraction; it is the runtime's own +// bookkeeping, not part of the sidecar wire protocol. const sentinelName = ".fmtkit-complete" // Sidecar returns the path of the multiplexed toolchain executable. diff --git a/packages/ts/infra/stage-ts-assets.sh b/packages/ts/infra/stage-ts-assets.sh index bfa8116..315e0d8 100755 --- a/packages/ts/infra/stage-ts-assets.sh +++ b/packages/ts/infra/stage-ts-assets.sh @@ -2,7 +2,7 @@ set -euo pipefail # Builds the self-contained TS toolchain assets embedded into the `fmtkit` -# release binary (see packages/go/driver/internal/tsruntime): +# release binary (see packages/go/driver/internal/typescript/runtime): # # - fmtkit-ts-sidecar bun-compiled bundle of packages/ts/sidecar/src/sidecar.ts # - oxc-parser.node napi binding, loaded via NAPI_RS_NATIVE_LIBRARY_PATH @@ -11,7 +11,7 @@ set -euo pipefail # - .oxfmtrc.json repo-root config, the default for projects without one # - .oxlintrc.json repo-root config, the default for projects without one # -# Output lands in packages/go/driver/internal/embedded/bin//, next to the +# Output lands in packages/go/driver/internal/typescript/embedded/bin//, next to the # package that embeds it: go:embed cannot reach outside its own directory. # # Tool versions come from packages/ts/sidecar/package.json devDependencies; @@ -31,7 +31,7 @@ if [[ $# -eq 0 ]]; then fi root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" -dist="${FMTKIT_TS_ASSET_DIR:-${root}/packages/go/driver/internal/embedded/bin}" +dist="${FMTKIT_TS_ASSET_DIR:-${root}/packages/go/driver/internal/typescript/embedded/bin}" source "${root}/infra/lib/host-target.sh" From 1ed083125416585ba69c8512bc5e0a4cccc38465 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 24 Jul 2026 12:29:48 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor(go):=20G7=20=E2=80=94=20rewire=20a?= =?UTF-8?q?pp=20to=20build=20lanes=20through=20the=20toolchain=20registry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/go/driver/internal/app/app.go | 15 +++++- packages/go/driver/internal/app/format.go | 13 ++++- packages/go/driver/internal/app/options.go | 12 +++-- packages/go/driver/internal/app/steps.go | 44 --------------- packages/go/driver/internal/app/steps_test.go | 54 ------------------- 5 files changed, 32 insertions(+), 106 deletions(-) delete mode 100644 packages/go/driver/internal/app/steps.go delete mode 100644 packages/go/driver/internal/app/steps_test.go diff --git a/packages/go/driver/internal/app/app.go b/packages/go/driver/internal/app/app.go index 7176aa3..0d8593d 100644 --- a/packages/go/driver/internal/app/app.go +++ b/packages/go/driver/internal/app/app.go @@ -7,6 +7,8 @@ import ( "go.ollin.sh/fmtkit/driver/internal/command" "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" ) @@ -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) @@ -31,7 +37,14 @@ const umbrellaHeader = "usage: fmtkit Date: Fri, 24 Jul 2026 12:36:10 +0800 Subject: [PATCH 6/6] docs: describe the language-lane driver layout (G7) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 604ac7b..19eee74 100644 --- a/README.md +++ b/README.md @@ -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/_/` (this needs Bun and takes a +`packages/go/driver/internal/typescript/embedded/bin/_/` (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`. @@ -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`) @@ -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`)