Skip to content

bug: signals stop working when a linked library has its own copy of @preact/signals-core #6889

Description

@jgroth

Prerequisites

Stencil Version

5.0.0-beta.7

Current Behavior

We have a component library that uses signals and has @stencil/core as a peer dependency. When we link it into an app to work on the two together, components in the app render once with the right data and then never update again. Nothing throws and nothing is logged.

Linking is what breaks it. Installing the same library from npm works, so this passes CI and works in production, and only the people developing the library see it.

The cause is that linking leaves two copies of @preact/signals-core in the project. @stencil/core/signals does not implement the signal functions, it re-exports them:

import { batch, computed, effect, signal, untracked } from "@preact/signals-core";

The linked library resolves @stencil/core from its own node_modules, and that copy uses its own @preact/signals-core. The app uses the copy in its own tree. A computed made by one copy cannot see a signal made by the other.

npm run test:issue4 shows it with no components involved:

✓ the two copies are separate
✓ a computed sees a signal from its own copy
× a computed does not see a signal from the other copy
✓ the signal still notifies its own subscribers

The failing test sets the source to 5 and the computed still reads 2.

That last passing test is why this took us so long to find. The signal keeps working. It notifies its subscribers, and every value we inspected in the running app was correct. Only the screen was stale, so we spent a long time looking for a bug in our own code.

Expected Behavior

Two copies should either share one set of signals, or there should be a way to stop the duplication happening.

dist/signals/index.js exports a symbol that nothing reads:

const STENCIL_SIGNALS_SYMBOL = Symbol.for("stencil.signals");

Symbol.for is the usual way to let two copies of a library share one instance, so this looks meant for the problem. Using it would fix this without any app having to change.

Failing that, the Stencil config could expose rolldown's resolve.symlinks. Rolldown's own documentation describes the case:

Whether to resolve symlinks to their symlinked location. When enabled, symlinked resources are resolved to their real path, not their symlinked location. Note that this may cause module resolution to fail when using tools that symlink packages (like npm link). Default true

System Info

System: node 24.18.1
    Platform: darwin (25.6.0)
   CPU Model: Apple M4 (10 cpus)
    Compiler: node_modules/@stencil/core/dist/compiler/index.mjs
       Build: 1788951557
     Stencil: 5.0.0-beta.7
  TypeScript: 5.9.3
    Rolldown: 1.2.8
      Terser: 5.51.2

Steps to Reproduce

  1. npm install
  2. npm run test:issue4 — 1 of 4 tests fails

The second copy is installed under an alias so that one npm install is enough:

"@preact/signals-core-copy": "npm:@preact/signals-core@1.14.4"

It is imported by file path, because the alias keeps its original name in package.json and importing it by name lets the bundler merge the two back into one.

Installing a second @stencil/core does not reproduce this. npm then installs only one @preact/signals-core and both copies share it.

Code Reproduction URL

https://github.com/jgroth/stencil-5-signals/tree/main/src/issue4-duplicate-copies

Additional Information

No response

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions