Review round 1 for the Node build module: registry for other @novox packages and refused bundles (1489), toolchain.reported stream (1483, 1488), re-pin to mesh-sdk 36d6f774; delta part 4 of 4

This commit is contained in:
2026-10-20 21:12:10 +00:00
parent 4a1804cda3
commit 00fa5e74d0
9 changed files with 210 additions and 31 deletions
@@ -45,6 +45,15 @@ type FetchResult struct {
// Modules are the TypeScript modules' directories found, and Installed those whose own packages were installed.
Modules []string `json:"modules"`
Installed []string `json:"installed"`
// Refused are the bundles whose packages were not installed, each with why (a module naming packages beyond the SDK
// with no package-lock.json). The fetch passes; a build of such a bundle fails naming it (issue 1459).
Refused []Refusal `json:"refused"`
}
// Refusal is a module a fetch did not install the packages of.
type Refusal struct {
Module string `json:"module"`
Reason string `json:"reason"`
}
func (m *Module) fetchTool() StepTool {
@@ -93,13 +102,16 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (any, error) {
if err != nil {
return nil, err
}
res := FetchResult{Workspace: id, Commit: commit, Modules: []string{}, Installed: []string{}}
res := FetchResult{Workspace: id, Commit: commit, Modules: []string{}, Installed: []string{}, Refused: []Refusal{}}
for _, d := range dirs {
res.Modules = append(res.Modules, d)
did, err := m.installOwn(ctx, root, filepath.Join(tree, filepath.FromSlash(d)), d)
did, refused, err := m.installOwn(ctx, root, filepath.Join(tree, filepath.FromSlash(d)), d)
if err != nil {
return nil, err
}
if refused != "" {
res.Refused = append(res.Refused, Refusal{Module: d, Reason: refused})
}
if did {
res.Installed = append(res.Installed, d)
}
@@ -165,27 +177,45 @@ func ownDependencies(dir string) (names []string, pkg map[string]any, err error)
return names, pkg, nil
}
// installOwn installs a module's own packages into its directory, or does nothing for a module that has none.
func (m *Module) installOwn(ctx context.Context, root, dir, rel string) (bool, error) {
// lockProblem says why a module's own packages cannot be installed: it names packages beyond the SDK and has no
// package-lock.json. Empty when it names none or has its lock.
func lockProblem(dir, rel string) (string, error) {
names, _, err := ownDependencies(dir)
if err != nil || len(names) == 0 {
return "", err
}
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
}
return "", nil
}
// installOwn installs a module's own packages into its directory, or does nothing for a module that has none. A module
// that names packages and has no lock is not installed and not an error: the reason is answered, the fetch passes and
// the build of that bundle fails naming it.
func (m *Module) installOwn(ctx context.Context, root, dir, rel string) (installed bool, refused string, err error) {
names, pkg, err := ownDependencies(dir)
if err != nil || len(names) == 0 {
return false, err
return false, "", err
}
for _, n := range names {
if strings.HasPrefix(n, "@novox/") {
return false, fmt.Errorf("%s depends on %s, and a fetch holds no credential for the mesh's package registry "+
"(ADR 0466 §4), so it cannot be installed", rel, n)
}
if why, err := lockProblem(dir, rel); why != "" || err != nil {
return false, why, err
}
lock, err := os.ReadFile(filepath.Join(dir, "package-lock.json"))
if err != nil {
return false, fmt.Errorf("%s names packages beyond the SDK (%s) and has no package-lock.json: commit one, and "+
"the step installs exactly what it names", rel, strings.Join(names, ", "))
return false, "", err
}
if m.Toolchain == nil || m.Toolchain.Registry == nil {
return false, "", fmt.Errorf("the module has no package registry to install %s from", rel)
}
reg, err := m.Toolchain.Registry()
if err != nil {
return false, "", err
}
// A scratch copy, so the module's package.json and lock are never rewritten (dependencies.go installSteps).
scratch := filepath.Join(root, "npm-"+strings.NewReplacer("/", "-", ".", "root").Replace(rel))
if err := os.MkdirAll(scratch, 0o700); err != nil {
return false, err
return false, "", err
}
for _, k := range []string{"dependencies", "peerDependencies", "optionalDependencies"} {
if d, ok := pkg[k].(map[string]any); ok {
@@ -196,28 +226,34 @@ func (m *Module) installOwn(ctx context.Context, root, dir, rel string) (bool, e
delete(pkg, "scripts")
raw, _ := json.Marshal(pkg)
if err := os.WriteFile(filepath.Join(scratch, "package.json"), raw, 0o600); err != nil {
return false, err
return false, "", err
}
if err := os.WriteFile(filepath.Join(scratch, "package-lock.json"), lock, 0o600); err != nil {
return false, err
return false, "", err
}
// The registry is given through a config file of its own, never on the command line, and removed after.
npmrc := filepath.Join(scratch, ".npmrc-install")
if err := os.WriteFile(npmrc, []byte(reg.Npmrc()), 0o600); err != nil {
return false, "", err
}
env := append(m.baseEnv(), "npm_config_cache="+filepath.Join(m.Toolchain.Paths.Cache, "npm"))
r := m.Run(ctx, env, "npm", "ci", "--omit=dev", "--omit=peer", "--ignore-scripts", "--no-bin-links",
"--no-audit", "--no-fund", "--prefix", scratch)
"--no-audit", "--no-fund", "--prefix", scratch, "--userconfig", npmrc)
os.Remove(npmrc)
if why := ranFailed(r); why != "" {
return false, fmt.Errorf("installing the packages of %s (%s): %s", rel, strings.Join(names, ", "), why)
return false, "", fmt.Errorf("installing the packages of %s (%s): %s", rel, strings.Join(names, ", "), why)
}
nm := filepath.Join(scratch, "node_modules")
if err := removeSDKCopies(nm); err != nil {
return false, err
return false, "", err
}
if err := os.RemoveAll(filepath.Join(dir, "node_modules")); err != nil {
return false, err
return false, "", err
}
if err := os.Rename(nm, filepath.Join(dir, "node_modules")); err != nil {
return false, err
return false, "", err
}
return true, os.RemoveAll(scratch)
return true, "", os.RemoveAll(scratch)
}
// removeSDKCopies removes every copy of the SDK the install pulled in: the toolchain's is the pin.