From 22992da15ded2cb5e0d5cc12573c522cedd0554b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 18:23:25 +0200 Subject: [PATCH] mesh-build-node: emit a package that builds before its tests, so tests importing its own output find it (issue 1869, part of issue 1309's run step) A package with a build or prepare script and a tsconfig.json is built with tsc -p . in the shut-off run step, at both levels, instead of the --noEmit type-check; the fetch's --ignore-scripts never runs those scripts. mesh-sdk's tests import ../dist and failed ERR_MODULE_NOT_FOUND. The result names the step build. --- .../mesh-build-node/build_integration_test.go | 25 +++++++++ .../cmd/mesh-build-node/check.go | 25 +++++++-- .../cmd/mesh-build-node/emit_build_test.go | 52 +++++++++++++++++++ 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 modules/mesh-build-node/cmd/mesh-build-node/emit_build_test.go diff --git a/modules/mesh-build-node/cmd/mesh-build-node/build_integration_test.go b/modules/mesh-build-node/cmd/mesh-build-node/build_integration_test.go index a7faa3d6..afe9d122 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/build_integration_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/build_integration_test.go @@ -89,3 +89,28 @@ func TestTheFixturePassesTheUnitLevel(t *testing.T) { t.Errorf("result = %+v", r) } } + +// Issue 1869: a package whose test imports its own build output (as mesh-sdk's do) passes the unit level: the test step +// emits it with the real tsc, shut off, before the test script runs. +func TestAPackageWhoseTestsImportItsBuildPassesTheUnitLevel(t *testing.T) { + if _, err := exec.LookPath("bwrap"); err != nil { + t.Skip("NOT RUN: no bubblewrap installed where this check runs; every build node has it (the module declares it)") + } + m, work := realMachine(t) + for p, body := range map[string]string{ + "modules/p/package.json": `{"name":"p","type":"module","scripts":{"build":"tsc","test":"node --test test/a.test.mjs","prepare":"tsc"}}`, + "modules/p/tsconfig.json": `{"compilerOptions":{"module":"NodeNext","moduleResolution":"NodeNext","target":"ES2022","outDir":"dist","rootDir":"src"},"include":["src"]}`, + "modules/p/src/index.ts": "export const answer: number = 42;\n", + "modules/p/test/a.test.mjs": "import { test } from 'node:test';\nimport assert from 'node:assert';\nimport { answer } from '../dist/index.js';\n" + + "test('the build is there', () => assert.equal(answer, 42));\n", + } { + write(t, filepath.Join(work, "w1", "tree", filepath.FromSlash(p)), body, 0o644) + } + out, err := m.test(context.Background(), map[string]any{"workspace": "w1", "module": "modules/p", "level": "unit"}) + if err != nil { + t.Fatal(err) + } + if r := out.(*TestResult); !r.Passed || !slices.Equal(r.Ran, []string{"build", "test"}) { + t.Errorf("result = %+v", r) + } +} diff --git a/modules/mesh-build-node/cmd/mesh-build-node/check.go b/modules/mesh-build-node/cmd/mesh-build-node/check.go index 02463848..1bf8c379 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/check.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/check.go @@ -138,13 +138,20 @@ func (m *Module) test(ctx context.Context, args map[string]any) (any, error) { return nil } if len(sources) > 0 { + // A package that builds (a build or prepare script, which the fetch's --ignore-scripts never runs) is emitted + // by tsc into the workspace, shut off like every run step, so its tests find what they import of its own output + // (issue 1869: mesh-sdk's tests import ../dist); any other is type-checked without emitting. + name, line := "type-check", m.typeCheck(dir, sources) + if emit := m.emitBuild(dir, scripts); emit != nil { + name, line = "build", emit + } start := m.now() - r := m.runIn(ctx, root, false, dir, nil, "node", m.typeCheck(dir, sources)...) + r := m.runIn(ctx, root, false, dir, nil, "node", line...) check = m.now().Sub(start) - if err := ran("type-check", r); err != nil { + if err := ran(name, r); err != nil { return nil, err } - res.Ran = append(res.Ran, "type-check") + res.Ran = append(res.Ran, name) } if hasScript && len(failed) == 0 { start := m.now() @@ -220,6 +227,18 @@ func (m *Module) typeCheck(dir string, sources []string) []string { "--target", "ES2022", "--rootDir", "."}, sources...) } +// emitBuild is tsc's line that builds a package as its tsconfig.json says (`tsc -p .`, emitting where it says), when the +// package builds (a build or prepare script) and has a tsconfig.json; nil otherwise. +func (m *Module) emitBuild(dir string, scripts map[string]string) []string { + if scripts["build"] == "" && scripts["prepare"] == "" { + return nil + } + if _, err := os.Stat(filepath.Join(dir, "tsconfig.json")); err != nil { + return nil + } + return []string{m.tsc(), "-p", "."} +} + // packageScripts are the scripts of the package.json in dir (none without one). func packageScripts(dir string) (map[string]string, error) { raw, err := os.ReadFile(filepath.Join(dir, "package.json")) diff --git a/modules/mesh-build-node/cmd/mesh-build-node/emit_build_test.go b/modules/mesh-build-node/cmd/mesh-build-node/emit_build_test.go new file mode 100644 index 00000000..b13a4daf --- /dev/null +++ b/modules/mesh-build-node/cmd/mesh-build-node/emit_build_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "context" + "slices" + "strings" + "testing" +) + +// The package.json of mesh-sdk's TypeScript package (feat/r4e-sdk 9cb4d59), whose tests import its own build output +// (test/retire.test.ts imports ../dist/...): built by `tsc` (build and prepare), which a fetch with --ignore-scripts +// never runs. +const sdkPackageJSON = `{"name":"@novox/mesh-sdk","version":"0.1.15","type":"module","exports":{".":"./dist/index.js"},"files":["dist"],` + + `"scripts":{"build":"tsc","test":"node --test --experimental-strip-types 'test/*.test.ts'","prepare":"tsc"}}` + +// Issue 1869: a package that builds (a build or prepare script) is emitted by tsc into the workspace, inside the shut-off +// run step, at both levels, before its tests, which import what it emits; the result names it "build". A package that +// does not build is still type-checked without emitting. +func TestAPackageThatBuildsIsEmittedBeforeItsTests(t *testing.T) { + for _, level := range []string{"unit", "integration"} { + m, f, work := newMachine(t) + pkg := sdkPackageJSON + if level == "integration" { + pkg = strings.Replace(pkg, `"prepare":"tsc"`, `"prepare":"tsc","test:integration":"node --test test/it.mjs"`, 1) + } + workspaceWith(t, work, map[string]string{"package.json": pkg, "tsconfig.json": `{"compilerOptions":{"outDir":"dist"}}`, + "src/index.ts": "export const a = 1;\n", "test/retire.test.ts": "import '../dist/index.js';\n"}) + out, err := m.test(context.Background(), testArgs(level)) + if err != nil { + t.Fatal(err) + } + r := out.(*TestResult) + if !slices.Contains(r.Ran, "build") || slices.Contains(r.Ran, "type-check") { + t.Errorf("%s: ran %v, want the build and no separate type-check", level, r.Ran) + } + first := joined(f.calls[0]) + if strings.Contains(first, "--noEmit") || !strings.HasSuffix(first, "typescript/bin/tsc -p .") { + t.Errorf("%s: the build ran %s, want tsc -p . emitting", level, first) + } + if len(f.shut) == 0 || !strings.Contains(f.shut[0].command, "tsc -p .") { + t.Errorf("%s: the build did not run shut off", level) + } + } + m, f, work := newMachine(t) + workspaceWith(t, work, map[string]string{"package.json": `{"scripts":{"test":"node --test"}}`, "tsconfig.json": `{}`, "index.ts": "export const a = 1;\n"}) + if _, err := m.test(context.Background(), testArgs("unit")); err != nil { + t.Fatal(err) + } + if first := joined(f.calls[0]); !strings.Contains(first, "--noEmit") { + t.Errorf("a package that does not build ran %s, want the type-check", first) + } +}