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 addLibrary — if (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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions