package builder import ( "fmt" "sort" "strings" ) // What a language implies, so a module does not have to say it. // // **A module says what it is written in; this says what that means.** The alternative is what the // mesh had: every module carrying a Dockerfile that repeated the same incantation, and most of the // catalogue never converted because the incantation is easy to get wrong in ways that fail // somewhere else (novox/hq 03-DESIGN/01-to-be/18-building-a-module.md). // // A toolchain is deliberately not configurable by the module. Anything a module could override // here it would be writing a Dockerfile to override, and then this bought nothing. // Toolchain is how one language is compiled into a bundle. type Toolchain struct { // Language is what a module declares to select this. Language string // Base is the module whose artifact provides the compiler, named rather than pinned: the mesh // answers with the copy it holds, so a recipe never names one particular build of it // (novox/hq 04-ISSUES/044). Base string // Artifact is which of that module's artifacts is the compiling one. Artifact string // Compile is what runs inside it, relative to the module's own directory. The output goes to // Output, which is what gets packed. Compile []string // Output is the directory the compiled result lands in, relative to the module's directory. Output string } // toolchains is every language the mesh can build. // // **A closed list, and adding to it is a decision rather than a configuration.** Every language is // permanent: it needs an SDK carrying the broker client, sealed-credential reading, the event // envelope and tool serving, and the contracts every module shares change rarely and cascade when // they do (novox/hq ADR 0039). A mesh whose languages disagree about the envelope fails by ignoring // messages rather than by failing to compile, so a new entry here is a commitment to keeping N // implementations of one contract in step. var toolchains = []Toolchain{ { Language: "typescript", Base: "mesh-tools", Artifact: "build", // Invoked by its real path rather than through node_modules/.bin, whose entries are // symlinks to a launcher that requires its library relatively — and the base image's own // assembly resolves them away, leaving a launcher whose relative require points nowhere. // Every module's hand-written Dockerfile had to know this. Now none of them does. Compile: []string{ "node", "/app/node_modules/typescript/bin/tsc", "--module", "NodeNext", "--moduleResolution", "NodeNext", "--target", "ES2022", "--outDir", "dist", }, Output: "dist", }, } // ToolchainFor is what builds this language, or says what it can build. func ToolchainFor(language string) (Toolchain, error) { want := strings.ToLower(strings.TrimSpace(language)) if want == "" { return Toolchain{}, fmt.Errorf( "a bundle must say what language it is written in: the mesh chooses the compiler, and "+ "it cannot choose one for a module that has not said. It can build %s", spoken()) } for _, t := range toolchains { if t.Language == want { return t, nil } } return Toolchain{}, fmt.Errorf( "%q is not a language this mesh builds. It can build %s — and adding one is a decision "+ "rather than a setting, because every language is another implementation of the "+ "contracts every module shares", language, spoken()) } // spoken lists the languages, so a refusal says what would have worked. func spoken() string { names := make([]string, 0, len(toolchains)) for _, t := range toolchains { names = append(names, t.Language) } sort.Strings(names) return strings.Join(names, ", ") } // Languages is every language the mesh can build, for anything that wants to say so. func Languages() []string { names := make([]string, 0, len(toolchains)) for _, t := range toolchains { names = append(names, t.Language) } sort.Strings(names) return names }