|
| 1 | +# `implies` type propagation — demo + regression guard |
| 2 | + |
| 3 | +A tiny two-package workspace that proves a non-obvious property of |
| 4 | +`@digital-alchemy/core`: when a library lists another library in `implies`, a |
| 5 | +downstream consumer that imports **only** the first library gets the second one |
| 6 | +both **wired at runtime** and **fully typed at compile time** — with no |
| 7 | +`LoadedRollups` registration and no manual re-export. |
| 8 | + |
| 9 | +It is also a durable regression guard: it compiles and runs against the real |
| 10 | +local core, so if a future TypeScript release changes how declaration-merge |
| 11 | +augmentations travel across package boundaries, `run.sh` fails loudly. |
| 12 | + |
| 13 | +## Run it |
| 14 | + |
| 15 | +```bash |
| 16 | +bash run.sh |
| 17 | +``` |
| 18 | + |
| 19 | +That builds core, builds `@demo/home-libraries`, type-checks `@demo/home-app` |
| 20 | +(the proof), then runs it (the demo). Expected output ends with the lights being |
| 21 | +driven by a library the app never imported: |
| 22 | + |
| 23 | +``` |
| 24 | +[INFO][living_room:Scenes]: ☀️ good morning |
| 25 | +[INFO][lighting:Lights]: 💡 lights → 80% |
| 26 | +[INFO][home:Routine]: lighting reports 80% — typed, no registration |
| 27 | +``` |
| 28 | + |
| 29 | +## Layout |
| 30 | + |
| 31 | +| Package | Role | |
| 32 | +|---|---| |
| 33 | +| `home-libraries/src/lighting.mts` | A plain library. Its service is a **named `function` declaration** — the load-bearing detail. | |
| 34 | +| `home-libraries/src/living-room.mts` | `implies: [LIGHTING]` (membership + types) and `depends: [LIGHTING]` (wiring order). Registers only `living_room`. | |
| 35 | +| `home-app/src/main.mts` | Imports **only** `LIVING_ROOM`, lists **only** `LIVING_ROOM`. Uses `params.lighting` — typed and live. | |
| 36 | +| `home-app/src/proof.mts` | Type-only guard (`@ts-expect-error` probes) — never executed, only type-checked. | |
| 37 | + |
| 38 | +## Why it works (and why named functions matter) |
| 39 | + |
| 40 | +Capturing `implies` as a literal tuple makes the implier's emitted `.d.mts` |
| 41 | +reference each member. With **named-function** services the reference is a real |
| 42 | +module edge: |
| 43 | + |
| 44 | +```ts |
| 45 | +// living-room.d.mts — NAMED fn: a genuine import edge to lighting.mjs |
| 46 | +readonly [LibraryDefinition<{ Lights: typeof import("./lighting.mjs").Lights }, …>] |
| 47 | +``` |
| 48 | + |
| 49 | +That `import("./lighting.mjs")` pulls `lighting.d.mts` into the consumer's |
| 50 | +program, so its `declare module { interface LoadedModules { lighting } }` |
| 51 | +augmentation activates and `TServiceParams` gains `lighting` automatically. |
| 52 | + |
| 53 | +An **arrow** service would be inlined anonymously with **no** edge: |
| 54 | + |
| 55 | +```ts |
| 56 | +// hypothetical arrow version — anonymous, no edge, augmentation never travels |
| 57 | +readonly [LibraryDefinition<{ read: (p: TServiceParams) => { … } }, …>] |
| 58 | +``` |
| 59 | + |
| 60 | +so the member would wire at runtime but `params.<member>` would be untyped. Named |
| 61 | +function declarations are not a style preference here — they are what makes the |
| 62 | +cross-package type edge exist. |
| 63 | + |
| 64 | +## Membership vs. ordering |
| 65 | + |
| 66 | +The two fields on `living_room` are orthogonal and both intentional: |
| 67 | + |
| 68 | +- `implies: [LIGHTING]` — **membership + types.** The app can omit `lighting` |
| 69 | + from `libraries`; it is pulled into the resolved set and its types travel. |
| 70 | +- `depends: [LIGHTING]` — **ordering only.** `params` is a wire-time snapshot, so |
| 71 | + `lighting` must wire *before* `living_room` for `Scenes` to call it. |
| 72 | + |
| 73 | +Drop `implies` and the app must list `lighting` itself (or boot throws |
| 74 | +`MISSING_DEPENDENCY`). Drop `depends` and `lighting` may wire after `living_room`, |
| 75 | +leaving `params.lighting` undefined at call time. |
| 76 | + |
| 77 | +## The core change this depends on |
| 78 | + |
| 79 | +One thing: `CreateLibrary` captures `implies` as a `const` tuple, carried as a |
| 80 | +third type parameter on `LibraryDefinition`. No `TServiceParams` machinery — |
| 81 | +deriving member keys for `TServiceParams` directly is provably circular; letting |
| 82 | +the member's own augmentation ride the import edge is what avoids that. |
0 commit comments