From ca7e81e964d927d2420fcd8c902c9451271ee509 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 2 Oct 2026 21:44:44 +0200 Subject: [PATCH] A TypeScript bundle carries what it runs with: the toolchain's runtime directory is copied into it (hq to-be 38 WP3, ADR 0188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bundle that compiled was not yet a bundle that ran. The compiler resolved `import "nats"` from the toolchain image's own node_modules and the pack took only what the compiler wrote, so what a machine unpacked could not find a single dependency — and Node would have read the bare `.js` as CommonJS besides. No TypeScript bundle had run live to show it; the runtime's own is the first that must. A toolchain now names a Dependencies directory in its image, copied whole into the output's root after the compile by a second run in the same image: for TypeScript /app/runtime, which the runtime's image puts a `"type": "module"` package.json and its pruned node_modules at. An older image without it fails the build by name rather than packing a bundle that starts nowhere. The SDK's and the runtime's dependencies, nothing module-specific yet: a skeleton, by ADR 0188 §5. --- internal/builder/builder.go | 20 ++++++++++++++++ internal/builder/bundle_test.go | 41 ++++++++++++++++++++++++++++++++- internal/builder/toolchain.go | 24 ++++++++++++++++--- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 43ed91f..fdcbb64 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -968,6 +968,26 @@ func compile(ctx context.Context, run Runner, tree string, chain Toolchain, if _, err := run(ctx, tree, "docker", invocation...); err != nil { return "", err } + if chain.Dependencies != "" { + // **What the bundle runs with, from the image it was compiled in** (Toolchain.Dependencies). + // A second run in the same image rather than a shell wrapped around the compiler: the + // compile line stays a plain command a reader can run by hand, and the copy is one more + // plain command beside it. Refused by name when the image carries no such directory — an + // older toolchain image — because a bundle packed without its dependencies starts nowhere + // and says so three layers away from here. + copying := []string{ + "run", "--rm", + "--volume", tree + ":" + within, + "--workdir", within, + base, + "sh", "-c", + `test -d "$1" || { echo "the toolchain image carries no $1: it predates the mesh shipping a bundle's dependencies, rebuild $2 first" >&2; exit 1; }; cp -a "$1/." "$3/"`, + "dependencies", chain.Dependencies, chain.Base, out, + } + if _, err := run(ctx, tree, "docker", copying...); err != nil { + return "", fmt.Errorf("copying the %s dependencies a bundle runs with: %w", chain.Language, err) + } + } return filepath.Join(tree, out), nil } diff --git a/internal/builder/bundle_test.go b/internal/builder/bundle_test.go index da718fd..7872da2 100644 --- a/internal/builder/bundle_test.go +++ b/internal/builder/bundle_test.go @@ -82,6 +82,41 @@ func TestABundleIsCompiledAndPackedWithNoDockerfile(t *testing.T) { if !strings.HasPrefix(digest, "sha256:") { t.Fatalf("the bundle was not pinned: %v", got.Manifest.Resources[0]) } + + // **And what it runs with, from the image it was compiled in** (novox/hq to-be 38 WP3). A + // second run in the same toolchain image copies the toolchain's runtime directory — the + // `"type": "module"` package.json and the pruned node_modules — into the output's root, and + // refuses by name when the image carries none rather than packing a bundle that starts nowhere. + var copied string + for _, line := range r.ran { + if strings.HasPrefix(line, "docker run") && strings.Contains(line, "/app/runtime") { + copied = line + } + } + if copied == "" { + t.Fatalf("the bundle's dependencies were not copied in after the compile:\n%s", strings.Join(r.ran, "\n")) + } + if !strings.Contains(copied, "mesh-tools/build@sha256:") || !strings.Contains(copied, "predates") || + !strings.Contains(copied, Out("code")) { + t.Fatalf("the copy does not run in the same toolchain, refuse an older image by name, or land in the artifact's output: %s", copied) + } + if strings.Index(strings.Join(r.ran, "\n"), "--outDir") > strings.Index(strings.Join(r.ran, "\n"), "/app/runtime") { + t.Fatal("the dependencies were copied before the compile wrote its output") + } +} + +// A language whose bundle carries its own dependencies copies nothing in: a Go binary is static. +func TestOnlyALanguageWithARuntimeDirectoryCopiesDependenciesIn(t *testing.T) { + ts, _ := ToolchainFor("typescript") + if ts.Dependencies != "/app/runtime" { + t.Fatalf("typescript bundles run with %q", ts.Dependencies) + } + for _, language := range []string{"go", "python"} { + chain, _ := ToolchainFor(language) + if chain.Dependencies != "" { + t.Fatalf("%s copies %q into every bundle, and its bundles carry their own", language, chain.Dependencies) + } + } } // **Refused before anything is built, naming what to build first.** A base the mesh has not built @@ -145,9 +180,13 @@ func TestTwoBundlesInOneModuleArePackedSeparately(t *testing.T) { t.Fatalf("a module with two bundles did not build: %v", err) } - // Compiled into two different places. + // Compiled into two different places. Only the compile lines: the copy of each bundle's + // dependencies names the same directory again, deliberately. var outputs []string for _, line := range r.ran { + if !strings.Contains(line, "--outDir") { + continue + } for _, part := range strings.Fields(line) { if strings.HasPrefix(part, ".mesh-build/") { outputs = append(outputs, part) diff --git a/internal/builder/toolchain.go b/internal/builder/toolchain.go index 0e2d50a..5df2d3f 100644 --- a/internal/builder/toolchain.go +++ b/internal/builder/toolchain.go @@ -57,6 +57,23 @@ type Toolchain struct { // carrying its debug info. The mistake was believing a comment rather than reading the file it // produced (novox/hq 04-ISSUES/161). LinkerFlags []string + // Dependencies is a directory inside the toolchain image whose contents a bundle in this + // language runs with, copied whole into the compiled output's root after the compile. + // + // **A bundle that compiles is not yet a bundle that runs.** The compiler resolves `import + // "nats"` from the toolchain image's own node_modules and the pack takes only what the compiler + // wrote, so what a machine unpacked could not find a single dependency — and no TypeScript bundle + // had ever run live to show it (novox/hq to-be 38 WP3). For TypeScript the directory holds a + // `package.json` saying `"type": "module"` — Node reads a bare `.js` as CommonJS otherwise, so a + // bundle with its dependencies and without that line still fails to start — and the pruned, + // production-only node_modules the runtime itself ships with: the SDK's and the runtime's + // dependencies, and nothing module-specific yet (novox/hq ADR 0188 §5: a skeleton; a module's + // own npm dependencies are a later step). Empty for a language whose bundle carries its own — + // a Go binary is static, a Python bundle is installed with its dependencies. + // + // A toolchain image without the directory fails the build by name rather than packing a bundle + // that starts nowhere: the image predates this and must be rebuilt first. + Dependencies string // SystemStamp is the variable this language's linker fills with the artifact's declared system, // for a language whose binaries are pinned to one at link time (novox/hq ADR 0005). // @@ -118,9 +135,10 @@ var toolchains = []Toolchain{ "--module", "NodeNext", "--moduleResolution", "NodeNext", "--target", "ES2022", "--rootDir", ".", }, - OutputFlag: "--outDir", - Unit: UnitSources, - SourceExt: ".ts", + OutputFlag: "--outDir", + Unit: UnitSources, + SourceExt: ".ts", + Dependencies: "/app/runtime", }, { Language: "go",