Skip to content

feat(core): keep stencil's hydrated class on every pine component through a dom morph - #877

Merged
QuintonJason merged 9 commits into
mainfrom
fix/keep-hydrated-class
Oct 16, 2026
Merged

QuintonJason merged 9 commits into
mainfrom
fix/keep-hydrated-class

Conversation

@QuintonJason

@QuintonJason QuintonJason commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Description

Stencil hides every Pine component until it has the hydrated class. A DOM morph (Turbo morphing, Idiomorph) that makes a host's class match the server's HTML removes it, and nothing puts it back, so the component goes invisible. #875 and #876 restore it for the 13 components with their own host restore. Every other component, such as pds-button, pds-input, pds-alert and pds-accordion, still vanishes after a morph.

This adds keepPineHydrated(): one MutationObserver on the document that puts hydrated, and a scoped component's own sc-* classes, back on any pds-* element that loses them.

  • It only restores classes listed in that element's previous class value, so it never runs ahead of Stencil.
  • It doesn't touch non-Pine elements or a consumer's own sc- classes (styled-components ids).
  • It only adds classes, so it settles after one round, alongside the per-component observers.

Where it runs

  • CDN and dist builds: it runs as Pine's Stencil globalScript (new src/global/pine-global.ts, wired in stencil.config.ts), so it starts as soon as the loader loads.
  • Custom-elements build (@pine-ds/core/components, @pine-ds/react): that build doesn't run global scripts, so keepPineHydrated is exported from @pine-ds/core. The getting-started guide tells those consumers to call it once.
  • Repeat calls: calling it again does nothing, and the observer is shared across copies of Pine on one page.
  • Server rendering: without MutationObserver, the first call does nothing either.

Size

  • What the global script costs: Stencil puts a global script in its own chunk, about 0.45 KB fetched once, and every component chunk gets a ~20-byte import of it. Summed over the entry files, that's about 1 KB.
  • Limits: the "All CDN component chunks" limit goes from 348 KB to 349.25 KB. The loader stays at 5.73 KB, under its 6 KB limit.
  • Why it's in a module of its own: isStencilClass and classNamesOf moved into utils/keep-hydrated.ts. If the global script had imported attributes.ts, the loader would have fetched that whole module.

Known limit: a page script that removes hydrated every time it comes back will loop against the observer. That's the same tug-of-war the planned restore cap (limitRestores) handles for the per-component observers. It can apply here once it lands.

Fixes DSS-362

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How Has This Been Tested?

  • unit tests: utils/test/keep-hydrated.spec.ts covers:
    • no observer without MutationObserver;
    • one observer however often it's called;
    • only Stencil classes from the previous value come back;
    • nothing ahead of Stencil;
    • non-Pine elements are left alone.
  • e2e tests: global/test/pine-global.e2e.ts strips the classes of pds-button, pds-input, pds-alert, pds-accordion and the scoped pds-radio-group, and checks that their Stencil classes come back and they stay visible. With the global script switched off, all five fail.
  • The host-restore suites still pass with both observers running: pds-box, pds-row, pds-radio, pds-tabs and pds-select.
  • Full core suite: everything passes except the 4 pds-chip "subtle and solid sentiments" e2e tests, which fail on main too.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing tests pass locally with my changes

Note

Medium Risk
Adds a global MutationObserver on every page load that mutates host class on all Pine components; low conflict risk but could fight scripts that intentionally strip hydrated.

Overview
Adds keepPineHydrated(), a document-level MutationObserver that re-applies Stencil’s hydrated and host sc-* classes on any pds-* element when a DOM morph (e.g. Turbo) strips them—fixing invisible components that lack per-component host restore.

CDN/dist: wired via new Stencil globalScript (pine-global.ts). React / custom-elements: export from @pine-ds/core and document a one-time startup call in getting-started. isStencilClass / classNamesOf move to keep-hydrated.ts and are reused by existing per-host restore in attributes.ts. Includes unit + e2e coverage; CDN chunk size limit raised 348 KB → 350 KB.

Reviewed by Cursor Bugbot for commit c1e5b71. Bugbot is set up for automated code reviews on this repo. Configure here.

@QuintonJason QuintonJason added the ran-gauntlet Multi-agent review gauntlet has been run on this branch label Oct 16, 2026
@QuintonJason QuintonJason self-assigned this Oct 16, 2026
@QuintonJason
QuintonJason requested a review from a team as a code owner October 16, 2026 20:36
@netlify

netlify Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for pine-design-system ready!

Name Link
🔨 Latest commit c1e5b71
🔍 Latest deploy log https://app.netlify.com/projects/pine-design-system/deploys/6ad29555f94375b60a41a6c1
😎 Deploy Preview https://deploy-preview-877--pine-design-system.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added bug Something isn't working package: core Changes have been made to the Core package labels Oct 16, 2026

@pixelflips pixelflips left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observer logic looks solid. A few small notes on the docs and the new e2e.

Comment thread libs/core/src/stories/introduction/getting-started.docs.mdx Outdated
Comment thread libs/core/src/stories/introduction/getting-started.docs.mdx Outdated
Comment thread libs/core/src/global/test/pine-global.e2e.ts
Comment thread libs/core/src/global/test/pine-global.e2e.ts Outdated
@QuintonJason
QuintonJason merged commit 0317af7 into main Oct 16, 2026
27 checks passed
@QuintonJason
QuintonJason deleted the fix/keep-hydrated-class branch October 16, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working package: core Changes have been made to the Core package ran-gauntlet Multi-agent review gauntlet has been run on this branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants