Skip to content

LibraryGroup (rollup) wiring crashes under TestRunner — addLibrary missing isRollup guard on depends path #341

Description

@zoe-codez

Summary

LibraryGroup({ members }) works correctly when placed directly in CreateApplication({ libraries: [...] }).bootstrap(), but crashes with TypeError: i[WIRE_PROJECT] is not a function when the same app is handed to TestRunner({ target: app }).

Affected version

@digital-alchemy/core 26.6.21 (peer @digital-alchemy/symbols 26.6.20)
Runtime: node v24.17.0

Symptom

[INFO] (resolveLibraryMembership): tr_group auto-pulled into membership by tr_app
>>>MARKER<<< name=tr_lib_a stage=construct   ← members start wiring...
bootstrap failed
TypeError: i[WIRE_PROJECT] is not a function
    at .../src/services/wiring.service.mts:824:28   (sourcemap)
    at eachSeries (.../src/helpers/async.mts:57:11)
    at async bootstrap (.../src/services/wiring.service.mts:821:5)

Boots fine via a direct CreateApplication(...).bootstrap(). Broken only when the same app definition is fed to TestRunner.

Why TestRunner triggers it

TestRunner.buildApp does not keep the target app's libraries as top-level libraries. It re-expresses them as the depends of a synthesized carrier library (dist/testing/test-module.mjs lines 43–48, 53, 66–76):

  1. getLibraries(target) returns target.libraries — which here is [group], the rollup.
  2. The target is wrapped as CreateLibrary({ depends: [group], … }).

So at bootstrap, the rollup is sitting on a depends edge rather than a top-level libraries position.

Root cause

In src/helpers/wiring.mts (dist: dist/helpers/wiring.mjs), inside flattenLibraries → the hoisted addLibrary walker, the depends/closure-as-membership walk (roughly lines 227–230 of the dist) lacks the isRollup guard:

// dist/helpers/wiring.mjs ~227-230 — NO isRollup guard
const deps = lib.depends ?? [];
if (!is.empty(deps)) {
    deps.forEach(dep => addLibrary(dep, `${path} -> depends(${lib.name})`, true, lib.name));
}

addLibrary (line 204) treats its argument as a plain library — it records lib.name, then seen.add(lib); out.push(lib) — so the rollup carrier is pushed into the wired membership set. Bootstrap then iterates membership and calls [WIRE_PROJECT] on every entry. A rollup carrier has [IS_ROLLUP] but no [WIRE_PROJECT], producing the TypeError.

The asymmetry

Both other expansion paths guard against this correctly by routing through visit, which calls isRollup(entry) before deciding whether to expand or wire:

  • Top-level declared libraries — declared.forEach(... visit(entry, …)) (~lines 256–263)
  • implies bundles — implied.forEach(member => visit(member, …)) (~line 237)

Only the depends walk calls addLibrary directly, bypassing the guard. The guard already exists in a sibling function: addCarrierDepends (dist ~lines 140–151) does if (isRollup(member)) before recursing — the check simply went missing in flattenLibraries's depends walk.

Proposed minimal fix

Route a rollup dep through the rollup-safe visit path, keeping closure semantics (fromClosure=true, puller) for plain-library deps. Both isRollup and visit are already in scope in the same module.

Before (src/helpers/wiring.mts, deps walk inside flattenLibraries → addLibrary):

const deps = lib.depends ?? [];
if (!is.empty(deps)) {
    deps.forEach(dep => addLibrary(dep, `${path} -> depends(${lib.name})`, true, lib.name));
}

After:

const deps = lib.depends ?? [];
if (!is.empty(deps)) {
    deps.forEach(dep => {
        // A rollup reached via a `depends` edge must be expanded into its members
        // via the rollup-safe `visit` path — a rollup carrier has no [WIRE_PROJECT].
        // Mirrors the top-level and `implies` expansion paths.
        if (isRollup(dep)) {
            visit(dep, `${path} -> depends(${lib.name})`);
            return;
        }
        addLibrary(dep, `${path} -> depends(${lib.name})`, true, lib.name);
    });
}

Alternative (smaller diff): add the guard at the top of addLibraryif (isRollup(lib)) { visit(lib, path); return; } immediately before note(lib.name, path). This catches the rollup-in-depends case (the only unguarded entry into addLibrary).

Suggested test

A unit test in testing/wiring.spec.mts (or equivalent): a library with depends: [LibraryGroup({ members: [a, b, c] })] must flatten to the individual members [a, b, c], never to the rollup carrier.

Also verify via TestRunner: an app with libraries: [LibraryGroup({ members })] passed to TestRunner({ target: app }) should boot all members without throwing.

Workaround (until fixed)

Carry the LibraryGroup on a library's implies instead of an app's top-level libraries. The implies path routes through visit and is rollup-safe:

// Safe under TestRunner
const carrier = CreateLibrary({ name: "carrier", implies: [LibraryGroup({ name: "group", members: [a, b, c] })] });
const app = CreateApplication({ libraries: [carrier] });
TestRunner({ target: app }); // works

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions