Conversation
Types & correctness: - pass an initial value to every useRef (fixes 4 React 19 type errors) - give useIntersectionObserver a real lazy conditional return type, a stable callback ref, and number[] threshold support - fix useLazyState previousValue (was always undefined) - cancel pending timers on unmount in useDebouncedCallback and useResizeObserver Packaging: - collapse packages/react into src/ (removes the duplicate package name) - dual ESM + CJS build via tsdown unbundle with per-file "use client" so useObjectFit stays usable in Server Components - inline the resize emitter and drop nanoevents -> zero runtime dependencies - drop the unused react-dom peer; set peer react >=18; add engines.node >=18 Tooling & tests: - Biome-only (remove the dead ESLint config); add typecheck/lint/test scripts - add a bun:test suite: render smoke, SSR safety, debounce leak regression - rewrite publish CI for bun; add a CI workflow with a React 18/19 matrix
…e object
Folds three setState calls in the resize effect into a single atomic update,
and returns a referentially stable object (it only changes on resize instead of
on every render). Public return shape { width, height, dpr } is unchanged.
Pins GITHUB_TOKEN to `contents: read` for both jobs, resolving the CodeQL "workflow does not contain permissions" findings (publish.yml already does this).
…lback The private helper shadowed React's reserved useEffectEvent API name, which misleads readers and tooling into assuming that hook's call-site restrictions apply. It is just a ref-backed stable-identity callback with none of them. Behavior-preserving: private function, internal call sites only.
…RCE) happy-dom <20 has a critical advisory (GHSA-37j7-fg3j-429f) plus two highs. Test-only dependency; all 46 tests pass on 20.10.2.
Upgrade the (non-published) playground stack to patched majors and override a transitive: - astro ^4.16.1 -> ^6.4.4 (high reflected-XSS GHSA-wrwg-2hg8-v723 + several moderate/low advisories; pulls vite/esbuild to patched) - @astrojs/react ^3.6.2 -> ^5.0.7, @astrojs/check ^0.9.3 -> ^0.9.9 - override yaml ^2.9.0 (transitive <2.8.3 stack-overflow GHSA-48c2-rrv3-qjmp) bun audit now reports no vulnerabilities. Shipped library unaffected: 46 tests pass, typecheck + build clean.
The playground build (astro check && astro build, not in CI) had been broken independently of any published code: - hamo was pulled from the registry (typeless 0.3.2) instead of the local build, because the repo root isn't a member of its own workspaces. Link it with file:.. so it resolves to the local dist (1.0.0, with types). - Align playground on React 19 to match hamo's build types (fixes ReactNode version-skew errors from @astrojs/react 5 pulling @types/react 19). - Remove www/pages/core.astro: dead page copied from another project that imported a non-existent ~/core/ dir and a different library (tempus). astro check: 0 errors; astro build: 2 pages. Shipped library untouched.
Completes the green-build fix (prior commit carried only the core.astro removal). Link hamo via file:.. so it resolves to the local dist instead of the typeless published 0.3.2, and bump react/react-dom/@types to ^19 to match hamo's build types (clears the @astrojs/react 5 ReactNode version skew).
Nuclear-review of playground deps: - Remove lorem-ipsum: unused since core.astro was deleted (no remaining references anywhere). - typescript ^5.5.4 -> ^6.0.3: was a major behind; @astrojs/check 0.9.9 declares typescript ^5 || ^6, so astro check is compatible. - @types/react refreshed to latest 19.x patch via lockfile. astro/@astrojs/react/@astrojs/check/react/react-dom already at latest. astro check: 0 errors; build: 2 pages. Shipped library manifest unchanged.
Move the publishable hamo library off the repo root into packages/hamo so it is a real workspace member. The root is now a private workspace root whose build/typecheck/lint/test/dev scripts delegate via `bun --filter hamo`. - playground links the library with workspace:* (the file:.. hack is gone; it had been silently resolving the published hamo@0.3.2 from npm, without types). - CI: test runs via `bun run test` so the package's bunfig.toml preload loads; React-18 matrix pin targets the package with --cwd; publish runs from packages/hamo (cd packages/hamo && npm publish). - biome vcs.useIgnoreFile off (gitignore stays at root); tsconfig/biome excludes trimmed to package scope. Verified: shipped tarball byte-identical to pre-move (npm pack --dry-run vs baseline), 46 tests pass, typecheck/lint/build clean, playground builds.
Legacy sandbox whose App.jsx imported ../src/hooks/* (a path that never existed); already excluded from lint + typecheck, no live references.
This was referenced Jun 8, 2026
#13) Co-authored-by: Clément Roche <rchclement@gmail.com>
Behavior-preserving maintainability pass; typecheck/lint/test/build all green.
- useScrollTrigger: delete the always-true isNumber guard and collapse the four
repeated position-parsing blocks into one resolveAnchor helper; drop the stale
redundant debugRef; fix debug-effect deps (biome hooks-lint 9 warnings -> 0);
un-export the internal modulo.
- useTransform: remove structuredClone from getTransform (ran every scroll tick)
in favour of direct accumulation; createTransform factory for inits; stabilise
the parent-inherit subscription.
- debounce-config: extract the triplicated module-level debounce default + setter
(useRect/useResizeObserver/useWindowSize) into one factory; per-hook semantics
preserved.
- useEffectEvent: canonical useInsertionEffect + useCallback ponyfill; route the
hand-rolled callbackRef pattern in four hooks through it; tighten the generic
off any.
- useResizeObserver: null the shared observer after disconnect so a later observe
re-creates it.
- types: deps: any[] -> DependencyList; drop {} as Rect casts; useDebouncedCallback
preserves arg tuples; export DebouncedFunction from the barrel.
- move yaml/esbuild overrides to the workspace root so bun honours them (the yaml override sat in packages/hamo where it was ignored); bump playground astro ^6.4.4 -> ^6.4.8. bun audit: 0 vulnerabilities (was 1 high / 2 moderate / 1 low, all in the non-published playground). - drop the now-dead overrides block from the published package manifest. - CHANGELOG: document the new useScrollTrigger/useTransform/useEffectEvent hooks and the optional lenis peer; fix stale "single src/ tree" wording. Published artifact unchanged except CHANGELOG. hamo gates green (typecheck/test/lint/build), playground builds 3 pages, publint clean.
- tsdown sourcemap: false. The output is modern, unminified, tree-shaken es2022 and tiny, so maps only bloated the tarball. Published package: 65.6 kB -> 31.1 kB packed (-53%), 268 kB -> 119 kB unpacked, 95 -> 63 files. - typescript ^5.4.5 -> ^6.0.3, matching the playground and removing the cross-workspace version skew. Gates green (typecheck/test/lint/build), publint clean.
Drop NODE_AUTH_TOKEN/NPM_TOKEN; authenticate via the id-token OIDC token and the trusted publisher configured on the hamo npm package. Upgrade npm to latest on the runner (trusted publishing needs npm >= 11.5.1; node 20 ships npm 10). Provenance still attached via --provenance.
0.3.2 shipped ESM-only; the dual ESM+CJS build was added in this branch for hypothetical CJS consumers that cannot exist (hamo never published CJS). The target stack — Next.js, React Router, TanStack, Vite — is ESM-native, and the in-repo consumers (bun tests, astro playground) are ESM. Remove the speculative surface rather than maintain it. - tsdown format: ['esm'] only; single .mjs/.d.ts output. - package.json: drop the `require` export conditions, .d.cts, and the legacy `main` CJS field; exports resolve through `module` + `exports`. - drop now-dead scripts: publish:main (superseded by the CI release workflow) and postversion's redundant rebuild. Keep prepublishOnly + version:* helpers. Package: 31.1 kB -> 16.8 kB packed (-46%; -74% from the pre-cleanup 65.6 kB), 63 -> 33 files. publint clean; attw is the canonical ESM-only profile (bundler/node16-ESM green, node16-CJS dynamic-import, node10 n/a). Gates green.
5 tasks
A cross-model (Codex) review caught that the main `hamo` entry statically re-exports useScrollTrigger, which statically imports `lenis/react`. With lenis declared optional, a consumer importing only e.g. useWindowSize without lenis installed could hit an unresolved `lenis/react` import in Vite dev / SSR / non-tree-shaking paths — so "optional" was not a robust guarantee. Make lenis a required peer (drop peerDependenciesMeta). The native-scroll fallback inside useScrollTrigger is unchanged: it keys on whether a Lenis *instance* is mounted at runtime, not on whether the package is installed. Docs (README, CHANGELOG, scroll-trigger README) updated to match. No hook logic changed. Gates green: typecheck 0, 46 tests, build + publint clean.
4 tasks
…asing) - useScrollTrigger/resolveAnchor: guard the numeric branch with Number.isFinite so a NaN/Infinity position can't propagate into progress math (falls back to 0). - useTransform/getTransform: deep-clone the parent's userData (guarded to the non-empty case, so the scroll hot path skips the clone) and shallow-merge self's — restoring the pre-cleanup semantics a callback could otherwise use to mutate a parent provider's nested userData. Both flagged by the Codex cross-model review. Gates green: typecheck 0, 46 tests, build + publint clean.
This was referenced Jun 22, 2026
Closed
Major bumps: astro ^6.4.8 -> ^7.0.0, @astrojs/react ^5 -> ^6 (pulls vite 8 + esbuild 0.28, already covered by the root esbuild override). @astrojs/check stays 0.9.9 (latest, astro-7 compatible). astro check: 0 errors; build: 3 pages (index, react, scroll-trigger). bun audit: 0. hamo gates unaffected (typecheck/test/build green). Playground is dev-only and not in CI; astro 7 requires Node >=22.12 (local 22.22).
Reverts the astro ^7 / @astrojs/react ^6 bump (916457b). astro 7.0.0 + @astrojs/react 6 + vite 8 produce a dev-mode React-island hydration failure ("react_jsx-runtime.js does not provide an export named 't'"): the production build is fine, but `bun run dev` renders the islands blank — which is exactly the playground's purpose (sanity-checking the demos). 6.4.8 is the latest stable that fully works (build + dev + live scroll-trigger hydration, verified in-browser). Revisit astro 7 once 7.0.x / @astrojs/react 6.0.x patch the jsx-runtime interop. Tracked in #19. bun audit: 0. astro check: 0 errors, 3 pages.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Ships
hamoas a real1.0.0. The branch hardens the existing hooks, ports three new scroll hooks, restructures into a monorepo, and clears the package for a clean stable publish.No existing public hook changed shape; current imports keep working.
Summary
Types & correctness
useRef— fixes 4@types/react19 type errors (tsc --noEmitclean).useIntersectionObserver: reallazyconditional return type (removed theascast), stable callback ref,number[]threshold support.useLazyState:previousValuenow reports the real prior value (was alwaysundefined).useDebouncedCallback/useResizeObservercancel pending timers on unmount (no callbacks after teardown).Maintainability pass (whole-codebase review)
useScrollTrigger: removed a broken always-trueisNumberguard; the four repeated position-parsing blocks collapse into oneresolveAnchorhelper; corrected debug-effect dependency arrays (Biome hooks-lint 0 warnings, down from 9); un-exported the internalmodulo.useTransform: droppedstructuredClonefromgetTransform()(it ran on every scroll tick) in favour of building the accumulated transform directly; acreateTransform()factory replaces the cloned inits; the parent-inherit subscription is a stable identity so it isn't recreated every render.useRect,useResizeObserver, anduseWindowSizeeach carried a verbatim copy of the module-level debounce default +setDebounce. Extracted to onedebounce-config.tsfactory; each hook keeps its own independent config instance, souseX.setDebouncesemantics are unchanged.useEffectEventrewritten to the canonicaluseInsertionEffect+useCallbackponyfill; the hand-rolledcallbackRefpattern that four hooks reimplemented now routes through it.useResizeObserver: the shared observer is reset tonullafterdisconnect(), so a laterobserve()re-creates it.deps: any[]→DependencyList; removed{} as Rectcasts;useDebouncedCallbackpreserves argument tuples;DebouncedFunctionexported from the barrel.New hooks — ported from #11 (@clementroche)
useScrollTrigger— scroll-progress tracking with GSAP-style position syntax, optional Lenis integration with graceful native-scroll fallback. Debug overlay behind thehamo/scroll-trigger/debuggersubpath.useTransform/TransformProvider— context-based transform accumulation for parallax compensation.useEffectEvent— promoted to a public hook;use-debounceconsumes it (v1's unmount-cleanup implementation was kept).Packaging / structure
packages/hamo/;playground/is a sibling workspace that links it viaworkspace:*(the oldfile:..hack that silently pulled the published0.3.2is gone). Root is a private workspace root whose scripts delegate viabun --filter hamo.unbundle, per-file"use client"souseObjectFitstays usable in Server Components. (0.3.2was already ESM-only; the target stack — Next.js, React Router, TanStack, Vite — is ESM-native, and CJS consumers can load it via dynamicimport().)nanoevents). Peers:react >=18andlenis >=1.3(used byuseScrollTrigger; the hook falls back to native scroll when no Lenis instance is mounted).-engines.node >=18; removed the dead legacydocs/directory.Security
happy-dom→^20— fixes a critical VM context-escape RCE (GHSA-37j7-fg3j-429f) plus 2 highs in the test environment.bun auditreports 0 vulnerabilities. The playground is not published, so none of this reaches consumers.Tooling & tests
bun:testsuite (46 tests): render smoke tests, SSRrenderToStringsafety, and a debounce-cancel-on-unmount leak regression.ci.yml(React 18 + 19 matrix) andpublish.ymlretargeted topackages/hamo; least-privilege permissions; lockfile committed.NPM_TOKENsecret. Triggered by creating a GitHub Release.v1-prep pass (final cleanup on this branch)
A round of release hardening after review:
0.3.2never shipped CJS, so there are no CJS consumers to preserve). Removed therequireexport conditions,.d.cts, and the legacymainfield.hamoto TypeScript 6; moved theyaml/esbuildoverrides to the workspace root (where bun honours them) and bumped playgroundastro, restoringbun auditto 0.lenis/reactviauseScrollTrigger, so an optionallenispeer wasn't a robust guarantee (could fail to resolve in Vite dev / SSR for consumers who skipped it). Madelenisa required peer; the runtime native-scroll fallback is unchanged.Combined effect on the published package: 65.6 kB → 16.8 kB packed (−74%), 95 → 33 files.
Test Plan
bun run typecheck— 0 errorsbun run test— 46 pass / 0 fail (React 18 + 19 in CI)bun run lint(biome) — clean, 0 warningsbun run build— single ESM bundle (+ the new hooks,debounce-config, debugger subpath)bun audit— 0 vulnerabilitiesastro check0 errors, 3 pages incl./scroll-triggerpublint— cleanattw— canonical ESM-only profile (bundler + node16-ESM 🟢; node16-CJS resolves via dynamic import; node10 n/a, predates ESM)bun run dev), including the/scroll-triggerdemoRelease
hamopackage settings, creating a GitHub Releasev1.0.0runs the full gate and publishes with provenance, no token.Known follow-ups (post-v1 — none blocking)
All deferred items are consolidated in a single tracker: #19 (lenis decoupling,
useRectcoordinate space,useScrollTriggerresize-staleness).Supersedes #11 (its three hooks are ported here, credited via Co-Authored-By). Stale Dependabot PRs #6/#7/#8 closed (the bun migration replaces that lockfile).