diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1fce02..7c880f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,12 +48,19 @@ jobs: - name: Type-check run: npx tsc --noEmit - - name: Test - run: npm test -- --run - + # BUILD BEFORE TEST, deliberately. `tests/cdn-artifacts.test.ts` asserts + # properties of the published artifacts that source cannot show — that + # SibuJS is absent from the CDN bundle's bytes, that diagnostics are + # compiled out of the production build, that the stylesheet carries the + # utilities the components use. With no `dist/` those tests have nothing + # to read, and they are written to FAIL rather than skip on CI, because a + # silent skip is how the gap they cover would come back. - name: Build run: npm run build + - name: Test + run: npm test -- --run + - name: Packaging check run: npm pack --dry-run @@ -101,12 +108,15 @@ jobs: - name: Type-check against SibuJS ${{ matrix.sibujs }} run: npx tsc --noEmit - - name: Test against SibuJS ${{ matrix.sibujs }} - run: npm test -- --run - + # Built first for the same reason as above: the artifact tests read + # `dist/`, and here they also run the CDN bundle against THIS matrix + # entry's runtime, which is the point of the job. - name: Build against SibuJS ${{ matrix.sibujs }} run: npm run build + - name: Test against SibuJS ${{ matrix.sibujs }} + run: npm test -- --run + audit: name: Audit (production deps) runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index a83e9da..58ea3b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,76 @@ This project follows [Semantic Versioning](https://semver.org/). --- +## [1.6.0] — 2026-09-07 + +### Added — a CDN build + +`sibujs-ui` had no no-build story: the package shipped ESM and CJS only, so a +page without a bundler could use the SibuJS runtime from a ` + + +``` + +New export paths: `sibujs-ui/cdn`, `sibujs-ui/cdn-dev` and `sibujs-ui/cdn-css`. +`npm run build` produces all three; `npm run build:cdn` and `npm run build:css` +build them separately. + +**A compiled stylesheet ships with it, and it is not optional.** Every component +carries Tailwind utility classes — 29 on a single `Button` — and the theme files +are custom properties only, so a page that loaded just the script tag got +correct behaviour and raw browser defaults: `display: inline-block`, +`background: rgb(240,240,240)`, `padding: 6px`, no radius. Nothing threw, which +makes that harder to diagnose, not easier. A no-build consumer has no build step +by definition, so `dist/sibujs-ui.css` is now compiled from the components' +actual class usage plus the base and default themes — 113.7 KB, 17.7 KB gzip. +With it linked, the same `Button` renders `inline-flex`, `oklch(0.205 0 0)`, +16px padding, 8px radius, 36px tall, and dark mode inverts correctly. + +It includes Tailwind's Preflight, matching what bundler consumers get from +`@import "tailwindcss"` — the components are designed against that reset, and +without it buttons keep their native chrome. It therefore restyles the host +page, which the README states plainly. + +**SibuJS is not bundled in.** It stays a peer dependency in this artifact too: +the build resolves `sibujs` to the `window.Sibu` that the runtime tag installs, +so a page that already loaded the framework does not download it again. Loading +`sibujs-ui`'s tag without the runtime throws a message naming the problem and +the fix, rather than surfacing later as an undefined property inside whichever +component ran first. + +| bundle | size | +| --- | --- | +| `cdn.global.js` | 573.5 KB raw / 132.2 KB gzip | +| `cdn.dev.global.js` | 574.1 KB raw / 132.4 KB gzip | + +The CDN build carries the whole package — every component and the full icon +set — because a ` + + +``` + +That is the whole setup — no build step, and no separate theme import. + +This is what makes the components usable from a SibuJS **island**: build them +inside a `registerIsland` setup and append them to the server-rendered markup. +Components create their signals through the same runtime the island uses, so a +click handler on a `Button` drives the island's own state with no wiring. + +SibuJS is **not** bundled into `sibujs-ui`'s CDN file. It stays a peer +dependency there too: the build resolves `sibujs` to the `window.Sibu` that the +runtime tag installs, so a page never downloads the framework twice. That is +also why the order matters — loading `sibujs-ui` alone throws an error saying +exactly that, rather than failing later inside a component. + +Use `cdn.dev.global.js` while developing to get the package's warnings; the +production file has them compiled out, not merely disabled. Both are also +reachable as `sibujs-ui/cdn` and `sibujs-ui/cdn-dev`. + +The CDN build carries the whole package — every component **and** the full icon +set — because a ` +// +// +// +// SibuJS IS NOT BUNDLED IN HERE. `sibujs` is a peer dependency, and the build +// resolves it to the `window.Sibu` the runtime tag installed (see +// `tsup.cdn.config.ts`). Bundling it would ship a second copy of the framework +// to every page that already loaded one — the components would still work, +// because the runtime shares its reactive API through a global registry, but +// the page would pay for the bytes twice. +// +// That is also why the tags are ordered: this file reads `window.Sibu` while it +// evaluates, so the runtime has to be there already. Loading it alone throws +// with a message that says so, rather than failing later as an undefined +// property somewhere inside a component. +// --------------------------------------------------------------------------- + +import * as components from "./index"; + +// `globalThis` rather than `window`, so the bundle also self-registers in a +// worker. In a browser they are the same object. +// +// Written by hand instead of via esbuild's `globalName`: that option emits +// `var SibuUI = (() => { … })()`, which lands AFTER the module body and would +// overwrite whatever the body installed with the module's own export namespace. +// Harmless while the two are identical, and a silent bug the moment they are +// not. See `tsup.cdn.config.ts`. +if (typeof globalThis !== "undefined") { + (globalThis as unknown as Record).SibuUI = { ...components }; +} diff --git a/src/components/types.ts b/src/components/types.ts index 5a1aabd..36e0084 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -1,11 +1,26 @@ import type { NodeChild, NodeChildren } from "sibujs"; +declare const __SIBU_DEV__: boolean | undefined; + // Dev-mode check, mirroring sibujs core's tree-shakeable `__SIBU_DEV__` // convention. Off in production browsers, on in test/dev Node. +// +// THE BARE IDENTIFIER MUST COME FIRST. Only a bare `__SIBU_DEV__` is a `define` +// target — `globalThis.__SIBU_DEV__` is a member expression, which a bundler +// cannot replace, so leading with it left every branch live and carried the +// warning text into builds that could never print it. Leading with the bare +// name lets `define: { __SIBU_DEV__: "false" }` fold this to `false` and drop +// the diagnostics entirely, which is what the CDN build relies on. When no +// define is applied the identifier resolves to the global of that name, so the +// first branch reads what the second would — hence the `!!` coercion. const _isDev: boolean = - typeof (globalThis as { __SIBU_DEV__?: boolean }).__SIBU_DEV__ !== "undefined" - ? !!(globalThis as { __SIBU_DEV__?: boolean }).__SIBU_DEV__ - : typeof process !== "undefined" && process.env?.NODE_ENV !== "production"; + typeof __SIBU_DEV__ !== "undefined" + ? !!__SIBU_DEV__ + : typeof (globalThis as { __SIBU_DEV__?: boolean }).__SIBU_DEV__ !== + "undefined" + ? !!(globalThis as { __SIBU_DEV__?: boolean }).__SIBU_DEV__ + : typeof process !== "undefined" && + process.env?.NODE_ENV !== "production"; // Heuristic mirror of sibujs core's tagFactory check: does a lone string look // like a CSS class list rather than text? Used ONLY to warn — behavior is diff --git a/styles/cdn.css b/styles/cdn.css new file mode 100644 index 0000000..d387eff --- /dev/null +++ b/styles/cdn.css @@ -0,0 +1,28 @@ +/** + * The stylesheet published alongside the CDN bundle. + * + * Compiled to `dist/sibujs-ui.css` by `npm run build:css`, and served as + * `sibujs-ui/cdn-css`. A page that loads the CDN