@stackone/hub is a React component library that ships an embeddable integration picker. It's bundled with Rollup and consumed in three forms: ESM, CJS, and a self-contained web component IIFE.
| Task | Command |
|---|---|
| Build all bundles | npm run build |
| Vite dev sandbox (port 3001) | npm run dev |
| Next.js SSR sandbox (port 3002) | npm run dev:nextjs |
| First-time Next sandbox setup | npm run dev:nextjs:setup |
| Lint | npm run lint (Biome) |
| Auto-fix lint/format | npm run lint:fix and npm run code:format:fix |
| Regenerate Relay artifacts | npm run relay |
Always run npm run lint before committing. Biome is the only linter — don't add ESLint/Prettier configs.
src/index.ts— public entry, exportsStackOneHubsrc/StackOneHub.tsx— top-level component (carries the'use client'directive)src/Hub.tsx— inner component, switches onmodesrc/modules/integration-picker/— main featuresrc/shared/— error boundary, http client, feature flags, queriessrc/WebComponentWrapper.tsx— separate entry built intodist/webcomponent.jsdev/vite/— Vite-based dev sandbox (ownpackage.json, hub linked viafile:../..)dev/nextjs/— Next.js 15 + React 19 App Router SSR sandbox (ownpackage.json, hub linked viafile:../..)rollup.config.mjs— three bundle outputs (ESM main, CJS main, IIFE webcomponent) plus a.d.tsrollupdist/— build output, gitignored, the only thing published
The package is consumed by SSR frameworks (Next.js App Router). When changing src/, keep these invariants intact:
'use client'directive on the bundle. It's injected byoutput.bannerinrollup.config.mjsand survives minification because terser is configured withcompress: { directives: false }. If you change the rollup config, verifyhead -c 30 dist/index.esm.jsstill starts with"use client";.- No
window/document/localStorageaccess during render. Anything that touches the DOM must live insideuseEffect, an event handler, or be guarded withtypeof window !== 'undefined'. Render-time access (includinguseMemoand module-scope code) breaks SSR. customElements.defineis module-scoped inWebComponentWrapper.tsxand is guarded withtypeof window !== 'undefined' && typeof customElements !== 'undefined'plus acustomElements.getcheck. Keep the guards if you edit that file.- Theme application mutates
<html>.applyTheme/applyLightTheme/applyDarkThemefrom@stackone/malachiteset CSS custom properties ondocument.documentElement. This requires consumers to addsuppressHydrationWarningto their<html>tag (documented in README). Don't move these calls out ofuseEffect. package.jsonsideEffectsfield marks only./dist/webcomponent.jsas side-effecting so bundlers can tree-shake the React entry. Keep it that way.- Single React instance.
react/react-dom/react-hook-formare peer deps and the bundle imports them at runtime — two copies in the consumer's tree breaks hooks (Invalid hook call). Standardnpm installhoists React and is fine. Monorepos, pnpm without hoist, andfile:/link:deps may require explicit deduping by the consumer. The README's "Invalid hook call — duplicate React" section documents fixes. - Every JS bundle must apply
replaceValuesfromrollup.config.mjs. It stamps__HUB_VERSION__with the package version, whichsrc/shared/version.tssends as thex-hub-versionheader on every request. A new bundle target that omits it still builds clean and silently reportsx-hub-version: unknown.npm run verify:build(wired into both CI workflows) is the guard — add any new bundle to the list inscripts/verify-build.mjs.
| File | Format | Notes |
|---|---|---|
dist/index.esm.js |
ESM | Has 'use client' banner |
dist/index.js |
CJS | Has 'use client' banner |
dist/index.d.ts |
TS declarations | Generated by rollup-plugin-dts |
dist/webcomponent.js |
IIFE | React + ReactDOM bundled in, registers <stackone-hub> |
Pre-existing build warnings about crypto / vm Node built-ins (from @stackone/utils and jsonpath-plus) are noisy but harmless for the React bundles — only the webcomponent IIFE actually needs them and they're never reached at runtime in a browser.
Both sandboxes are now their own npm packages and consume the hub via "@stackone/hub": "file:../..", so they exercise the actual built dist/ (including the 'use client' banner). Edit hub source, run npm run build from the repo root, and both sandboxes pick up the new bundle automatically — no reinstall needed.
- Vite sandbox (
dev/vite/): port 3001. Run vianpm run devfrom the repo root. First-time setup isnpm run dev:setup(builds the hub, thennpm installinsidedev/vite/). - Next.js sandbox (
dev/nextjs/): port 3002. Run vianpm run dev:nextjs. First-time setup isnpm run dev:nextjs:setup. Use this one to validate SSR behaviour.
Vite sandbox needs resolve.dedupe for react, react-dom, react-hook-form because the symlinked hub at node_modules/@stackone/hub resolves to a directory with its own node_modules — two React copies otherwise. The dev/vite/vite.config.ts is already set up correctly. If you add a new peer dep to the hub, add it to the dedupe list.
If you add a new public prop to StackOneHub, update both dev/vite/main.tsx and dev/nextjs/app/HubWrapper.tsx so the props stay testable in both sandboxes.
Releases use release-please-config.json. Don't bump version in package.json manually — release-please handles it.
- No comments in code unless the why is genuinely non-obvious. Names should carry the intent.
- TypeScript strict mode is on; don't reach for
any. Internal types live insrc/types/, feature-specific types live in their feature folder (src/modules/integration-picker/types.ts). - 4-space indent (Biome enforces).
- Imports are sorted by Biome — let the formatter do it.