A relative replace of a go.mod still resolves in the temporary modfile, so a module with a sibling package builds and verifies through the pins path
mesh/delivery merged: source.main moved to novox/mesh-catalog@ee75d4327ce5
mesh/check being carried to 45084af849fc on ee75d4327ce5 | from c52afdf88b84

ensureModfile copied go.mod word for word into pins/<dir>/, where `go mod verify` runs, so a replace to ./x or ../x pointed
nowhere (route-proxy's failure of 2026-11-02 13:53, met a second time with the release's sibling packages). Every relative
replace target is now made absolute under the tree the go.mod came from. mesh-build-node installs a module's own npm
packages in a scratch copy of package.json, so a file:, link: or portal: dependency on a relative path would fail the same
way; it is refused naming the package and its path, as a module without a lock is.

Quick fix: rewrite the one replace route-proxy had, in ensureModfile (minutes). Thorough fix: the kind, a go.mod or
package.json copied away from its tree, for every replace form (single, block, with a version, ./ and ../) in the one place
all copies go through (ensureModfile, which versions.go and the pins share), and the same kind in the other build module
that copies a manifest. Chosen: thorough, since it was met twice and hides a failure until the delivery (quick-or-thorough,
fix-a-bug-met-twice). Test: TestARelativeReplaceStillResolvesInTheTemporaryModfile runs real go mod verify in the pins directory.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 19:36:15 +01:00
parent c52afdf88b
commit 45084af849
4 changed files with 108 additions and 1 deletions
@@ -282,6 +282,18 @@ func lockProblem(dir, rel string) (string, error) {
if _, err := os.Stat(filepath.Join(dir, "package-lock.json")); err != nil {
return fmt.Sprintf("%s names packages beyond the SDK and has no package-lock.json (%s): commit one", rel, strings.Join(names, ", ")), nil
}
_, pkg, _ := ownDependencies(dir)
for _, k := range []string{"dependencies", "peerDependencies", "optionalDependencies"} {
deps, _ := pkg[k].(map[string]any)
for _, name := range names {
spec, _ := deps[name].(string)
for _, scheme := range []string{"file:", "link:", "portal:"} {
if path, ok := strings.CutPrefix(spec, scheme); ok && (path == "." || path == ".." || strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")) {
return fmt.Sprintf("%s depends on %s by the relative path %s, which the install in a scratch copy cannot resolve: name the package from the registry", rel, name, spec), nil
}
}
}
}
return "", nil
}
@@ -168,6 +168,23 @@ func TestAModuleWithoutALockIsRefusedInTheResultNotTheFetch(t *testing.T) {
}
}
// A dependency on a directory by a relative path (file: or link:, as a go.mod replace is in Go) would resolve to nothing in
// the scratch copy `npm ci` runs in, so the module is refused naming the package and its path, as one without a lock is,
// never installed wrongly (the kind of the 2026-11-02 13:53 route-proxy failure in mesh-build-go).
func TestAModuleDependingOnARelativeDirectoryIsRefusedNamingIt(t *testing.T) {
files := typeScriptModule(`{"dependencies":{"@novox/mesh-sdk":"^0.1.6","sibling":"file:../sibling"}}`)
files["modules/m/package-lock.json"] = `{"lockfileVersion":3}`
m, f := fetchMachine(t, files, nil)
out, err := m.fetch(context.Background(), fetchArgs)
if err != nil {
t.Fatalf("the fetch failed: %v", err)
}
r := out.(FetchResult)
if len(npmCalls(f)) != 0 || len(r.Refused) != 1 || !strings.Contains(r.Refused[0].Reason, "sibling") || !strings.Contains(r.Refused[0].Reason, "file:../sibling") {
t.Errorf("refused = %+v after %d npm calls, want the dependency named and nothing installed", r.Refused, len(npmCalls(f)))
}
}
// 1489 (review 4): a bundle depending on another @novox package is installed, the mesh's registry given to npm ci
// through --userconfig, as the toolchain's install gives it.
func TestAnotherNovoxPackageIsInstalledFromTheRegistryThroughAUserconfig(t *testing.T) {