Repository navigation
1.7.0
A release about what lands in your bundle. Nothing about how a component looks or behaves has changed; what changed is how much of the library you have to take to get one of them.
Measured with a real bundler rather than reported from an unpackedSize — rollup and terser, react external, judged on gzip:
| What you import | 1.6.0 | 1.7.0 |
|---|---|---|
Chip |
14.5 kB | 3.0 kB |
LineChart |
22.8 kB | 11.0 kB |
| 12 components — a typical app | 79.8 kB | 67.0 kB |
| 25 components — a large one | 124.4 kB | 110.8 kB |
| all 117 exports | 218.7 kB | 206.8 kB |
The last row is the least interesting one, and that is the point: taking the whole library is barely cheaper, because nothing was removed. What changed is the price of taking a part — and the reason Chip used to cost fourteen kilobytes was never Chip.
Changed
-
Languages are registered rather than shipped. Neba speaks English out of the box and the other eighteen languages are now modules you turn on:
import { registerMessages, ko } from 'neba/locales'; registerMessages('ko', ko);
Call it once at module scope, before your first render, for each language you support; then a
localeprop translates exactly as it did.This is a breaking change in a minor release, which is deliberate given how few projects are on Neba today, and it is the only one here. A project that passes
localeand registers nothing will see English where it used to see a translation — the strings still resolve, they just resolve to the fallback. If you pass alocaleanywhere, add the two lines above and you are done. Everything else is unchanged: tags are still matched by script, then region, then language, so registeringkoanswersko-KRandzhHansregistered aszh-hansanswerszh-CNand a barezh; a language still fills in only what it has and English answers for the rest; and every string a component invents still has a prop that overrides it.The reason is arithmetic. Eighteen languages of sixteen namespaces was one object literal, and a bundler cannot drop a key out of an object literal — so a
Chipthat wanted the word "Remove" carried the ColorPicker's colour names and the Table's column labels in eighteen languages. Registered, a language costs about 1.7 kB gzipped and you pay for the ones you name. -
severityIconsis nowseverityIcon(color)internally, so a component that draws one severity mark carries one, and no React elements are built at import time for a page that may draw none.
Added
- Every component is its own entry point, named after its folder:
import { Button } from 'neba/button',import { TextField } from 'neba/text-field'. The bundle is the same as the barrel's —import { Button } from 'neba'already tree-shook correctly — but the barrel makes a bundler parse two hundred modules to keep five, and the subpath makes it parse five. It is also the escape hatch if your bundler ignoressideEffects. neba/localesfor the barrel andneba/locales/kofor one language, plus theNebaLocaletype for a translation of your own.neba/package.jsonis exported, which some tooling asks for.npm run size— a bundle-size budget. Seven scenarios, from one component to all of them, bundled againstdist/and checked against the numbers committed inscripts/bundle-budget.json. It runs as its own CI job, so a change that quietly stops something being tree-shakeable fails a pull request instead of shipping.npm run size:updaterecords a new budget when the growth is real and wanted.
Fixed
- The package can be imported by Node, and by TypeScript on
moduleResolution: node16. Emitted ESM used extensionless relative imports (export * from './types'), which Node's resolver rejects outright and which TypeScript onnode16ornodenextreported as "Module 'neba' has no exported member" — for every component at once. Anything using a bundler was unaffected, which is why it went unnoticed. Every relative specifier now carries its.js. - The published files now carry
@__PURE__annotations, so a bundler can drop the parts of a multi-component module you did not import. They are written intodist/during the build and terser is told to keep them, which it does not do by default — until now the annotations reached the consumer's bundler stripped, and it keptTabandTabPanelfor anyone who imported onlyTabs.Tabson its own is 23% smaller.
Documentation
- The README gains a Languages section and the subpath import form;
CLAUDE.mdgains a Packaging, bundle size and tree-shaking section with the measurements, the five invariants that hold them in place, and the list of things measured and rejected so they are not tried again — minifier tuning, per-component stylesheets, and dropping Tailwind's@propertyfallback. test/package/resolution.test.tsandtest/locales/register.test.tsxare new. Neither renders a component: the first checks the wiring betweensrc/,dist/andpackage.jsonthat no render test ortsc --noEmitcan see, and the second checks the registration contract.
Every release is in the changelog on the docs site and in CHANGELOG.md at this tag.