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):
getLibraries(target) returns target.libraries — which here is [group], the rollup.
- 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
Summary
LibraryGroup({ members })works correctly when placed directly inCreateApplication({ libraries: [...] }).bootstrap(), but crashes withTypeError: i[WIRE_PROJECT] is not a functionwhen the same app is handed toTestRunner({ target: app }).Affected version
@digital-alchemy/core26.6.21 (peer@digital-alchemy/symbols26.6.20)Runtime: node v24.17.0
Symptom
Boots fine via a direct
CreateApplication(...).bootstrap(). Broken only when the same app definition is fed toTestRunner.Why TestRunner triggers it
TestRunner.buildAppdoes not keep the target app'slibrariesas top-level libraries. It re-expresses them as thedependsof a synthesized carrier library (dist/testing/test-module.mjslines 43–48, 53, 66–76):getLibraries(target)returnstarget.libraries— which here is[group], the rollup.CreateLibrary({ depends: [group], … }).So at bootstrap, the rollup is sitting on a
dependsedge rather than a top-levellibrariesposition.Root cause
In
src/helpers/wiring.mts(dist:dist/helpers/wiring.mjs), insideflattenLibraries→ the hoistedaddLibrarywalker, thedepends/closure-as-membership walk (roughly lines 227–230 of the dist) lacks theisRollupguard:addLibrary(line 204) treats its argument as a plain library — it recordslib.name, thenseen.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 callsisRollup(entry)before deciding whether to expand or wire:declared.forEach(... visit(entry, …))(~lines 256–263)impliesbundles —implied.forEach(member => visit(member, …))(~line 237)Only the
dependswalk callsaddLibrarydirectly, bypassing the guard. The guard already exists in a sibling function:addCarrierDepends(dist ~lines 140–151) doesif (isRollup(member))before recursing — the check simply went missing inflattenLibraries's depends walk.Proposed minimal fix
Route a rollup dep through the rollup-safe
visitpath, keeping closure semantics (fromClosure=true,puller) for plain-library deps. BothisRollupandvisitare already in scope in the same module.Before (
src/helpers/wiring.mts, deps walk insideflattenLibraries → addLibrary):After:
Alternative (smaller diff): add the guard at the top of
addLibrary—if (isRollup(lib)) { visit(lib, path); return; }immediately beforenote(lib.name, path). This catches the rollup-in-dependscase (the only unguarded entry intoaddLibrary).Suggested test
A unit test in
testing/wiring.spec.mts(or equivalent): a library withdepends: [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 withlibraries: [LibraryGroup({ members })]passed toTestRunner({ target: app })should boot all members without throwing.Workaround (until fixed)
Carry the
LibraryGroupon a library'simpliesinstead of an app's top-levellibraries. Theimpliespath routes throughvisitand is rollup-safe: