Skip to content

Publish the React and Vue wrappers as their own packages (0.8.0) - #11

Draft
cmm-cmm wants to merge 11 commits into
mainfrom
claude/forgedialog-library-mhkpak
Draft

cmm-cmm wants to merge 11 commits into
mainfrom
claude/forgedialog-library-mhkpak

Conversation

@cmm-cmm

@cmm-cmm cmm-cmm commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

forgedialog advertised having no runtime dependencies while declaring react and vue as peer dependencies and shipping a build of each. They were marked optional, so nothing broke — but two frameworks sat in the dependency tree of a library that has nothing to do with either, and every tool that reads peer dependencies had to be told to ignore them.

The adapters move to packages/react and packages/vue, published as forgedialog-react and forgedialog-vue, matching how the sibling repository already ships forge-select-react and forge-select-vue. Each declares its own framework as a peer dependency and the core as a real one, and each builds with the framework and the core external, so a wrapper never ships a second copy of either.

forgedialog/svelte and forgedialog/web-component stay in the core. Neither needs a framework runtime, so neither costs a peer dependency.

Breaking, hence 0.8.0

forgedialog/react and forgedialog/vue are gone, and the core no longer declares peerDependencies at all. The hook itself is unchanged, so the migration is one import line:

- import { useForgeDialog } from 'forgedialog/react';
+ import { useForgeDialog } from 'forgedialog-react';
npm install forgedialog-react   # or forgedialog-vue

The core's tarball drops to 63 files / 118.2 KiB, from 71 / 120.3.

Why the dependency range is not a caret

The wrappers depend on forgedialog: ">=0.7.0 <1.0.0". On a 0.x core a caret pins the minor, so ^0.7.0 would exclude 0.8.0 — the very release these ship beside. check-release.mjs now refuses a caret range on the core rather than guessing at what it was meant to mean.

Release plumbing

check-release.mjs previously checked the one package it was pointed at. It now covers every package a release publishes and verifies each wrapper's range against the release — proved by setting three wrong ranges and watching each one fail, not by watching it pass.

  • npm publish --workspaces runs after the core: a consumer installing a wrapper the moment it appears has to find the core already on the registry.
  • Each package gets prepublishOnly, so a clean checkout cannot publish an empty dist.
  • validate, lint, and check:package now cover the workspaces, so CI gates them.
  • docs/releasing.md gains a step: npm scopes trusted publishing per package, so all three need their own publisher entry. Miss one and the release half-succeeds — core published, wrappers not.

Verification

  • npm run validate — now including the workspaces' tests, typecheck, and package checks
  • npm run format:check
  • npm run test:e2e — not run locally (the pinned Playwright wants a browser build this container does not have). It drives demo/ and the library, neither of which changed. CI runs it.
  • Added or updated tests covering the change — each package carries the test for its own hook; the core's adapter suite keeps only Svelte and Web Components

Verified end to end rather than by inspection. All three packages were packed and installed into an empty project, where:

  • forgedialog-react and forgedialog-vue export the hook
  • forgedialog/react and forgedialog/vue no longer resolve; forgedialog/svelte still does
  • the core reports no peerDependencies
  • a real React render through the packed dist opens a dialog with the right title and role, clears active on close, and destroys a still-open dialog on unmount
  • the install reports found 0 vulnerabilities

publint and attw pass on both packages across node10, node16 (CJS and ESM), and bundler.

Checklist

  • Added a changeset — not applicable: this repository writes CHANGELOG.md by hand, and 0.8.0 is written there with the migration.
  • Refreshed API reports — not applicable: the reports cover the core's entry points, and the removed adapters never had one.
  • Raised a gzip budget — not needed; the core got smaller.
  • Refreshed the visual baseline — not needed; demo/ is unchanged.

One note for the record

This branch's lockfile gains tsup/node_modules/esbuild@0.27.7, which npm audit flags low. It is not a regression: main's lockfile simply omitted the entry, while tsup@8.5.1 has always required esbuild ^0.27.0, which the hoisted 0.28.1 cannot satisfy. No patched 0.27.x exists and tsup@latest still pins that range, so forcing 0.28 on it would be an unsupported combination adopted to silence a dev-server advisory this repository never runs. Nothing gates on npm audit, and nothing reaches the published packages.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4


Generated by Claude Code

ForgeDialog had a README and a manual test page but nothing to point people
at. site/ is a landing page with an interactive appearance playground,
written in plain HTML, CSS, and browser JavaScript so the site carries no
dependencies of its own, matching the library it advertises.

The page runs the build from this checkout rather than a CDN:
scripts/build-site.mjs copies dist/index.global.js and dist/index.css into
site/vendor/, which is git-ignored. It uses only Node built-ins and rewrites
no source files. The version badge reads the library's own VERSION export at
runtime, so nothing has to be kept in sync by hand.

The playground calls instance.update() on every control change, so the dialog
restyles while it is open, and it prints the matching source alongside, so the
demo doubles as documentation for the appearance API.

site/_headers ships a strict CSP for Cloudflare Pages limited to same-origin
scripts and styles, which the library already satisfies: test:csp enforces
that dist contains no eval or new Function. docs/website.md covers local use
and the Cloudflare build settings.

Verified in Chromium: no console errors or failed requests, the alert traps
focus and returns it to the trigger on Escape, the playground applies a 30px
radius and casts its shadow right at 90 degrees, live updates reach an open
dialog, and the theme toggle drives the library and the page together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
…ise for search and answer engines

The site had a landing page and an emoji favicon; it now has a full API
reference, a real mark, and the metadata that search engines and answer
engines read.

Naming: the product is Forge Dialog, two words, and every page and document
now says so. Identifiers keep the one-word spelling because that is what they
are: the npm package (forgedialog), the browser global (ForgeDialog), the
custom element (forge-dialog), and the repository. Nothing under src/ changed,
so the published API and its runtime strings are untouched.

Docs: site/docs.html is a 23-section reference written against the reviewed
API report in etc/, covering every option, method, and entry point. The shared
chrome (version badge, theme toggle, copy buttons) moved out of site.js into
common.js so both pages use it without the landing page's demo wiring.

Icon: site/icon.svg is the source of truth and the favicon browsers load.
scripts/build-icons.mjs renders the committed apple-touch-icon.png and
og-image.png from it through Chromium, which is already a dev dependency. The
touch variant squares off the mark's ground because iOS applies its own mask.
The first draft of the OG image used a gradient and cost 296 KB; a flat ground
looks the same at preview size and costs 37 KB.

SEO: canonical, og:url, and og:image are absolute. The canonical tag in
index.html is the single source of truth for the domain — build-site.mjs reads
it back to generate sitemap.xml, robots.txt, and llms.txt, so moving the site
is an edit to the canonical tags and nothing else. Both pages carry JSON-LD;
the FAQ entries are generated from the visible questions so the markup and the
page cannot disagree.

Answer engines: llms.txt follows the llmstxt.org convention and states the
facts models most often get wrong, with the version read from package.json so
it cannot drift. robots.txt names the AI crawlers explicitly rather than
leaving them to the default, and deliberately does not disallow /vendor/ —
blocking a site's own CSS and JS is what breaks rendering for crawlers.

Two layout bugs found while measuring, both pre-existing:

- The header never wrapped, so at 380px the page scrolled sideways by 102px.
- The playground's grid track had an automatic minimum of min-content, which
  the long code lines inside .snippet pushed open — 812px of horizontal
  overflow at 320px. Capping the track at minmax(0, 1fr) and giving .snippet
  min-width: 0 fixes it.

Both pages now fit at 320, 380, 480, and 768px with zero horizontal overflow.

Verified in Chromium against the production CSP: no console errors and no
failed requests on either page, the inline JSON-LD survives script-src 'self'
(it is a data block, not an executable script), all 23 sidebar links resolve
to real sections, scroll-spy tracks the section being read, the hero alert
still gets role="alertdialog" with focus returning to its trigger, and
robots.txt, sitemap.xml, llms.txt, and all three icons serve with the right
content types. npm run validate and format:check pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
The Cloudflare Worker was built with `npm run build` and deployed with
`wrangler deploy --assets ./dist`, so it uploaded 97 files of compiled
library — .mjs, .cjs, .d.ts, .css — with no index.html among them. Workers
Assets had nothing to answer `/` with, which is why the domain served
nothing.

wrangler.jsonc now pins the assets directory to `site/`, the directory
`npm run site:build` actually fills, so the configuration lives in the
repository rather than in a dashboard field. A deploy command that passes
`--assets` overrides it, so docs/website.md says to drop the flag.

While verifying the routing, the docs page turned out to be reachable only
by redirect. Workers Assets serves `foo.html` at `/foo` and 307s the
extension form, and `serve` (which `npm run site` uses) does the same, so
every URL naming `/docs.html` — the canonical tag, og:url, the TechArticle
and BreadcrumbList nodes, the sitemap entry, the llms.txt link, and the
in-page links — pointed at a URL that redirects. A canonical or sitemap
entry that redirects is a conflicting signal, so `/docs` is now the URL
everywhere, and the docs page links back to `./` rather than
`./index.html`.

Verified against `wrangler dev` with the production `_headers` CSP: both
pages and every asset return 200, every relative link resolves without a
redirect, the pages load with no console errors or failed requests, the
JSON-LD parses, and the hero alert still opens as `role="alertdialog"` and
closes on Escape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
A repo-wide pass before the worker is recreated from scratch.

An unmatched path got the platform's bare 404, so site/404.html now answers
for them through not_found_handling. It links its assets root-relative
rather than with "./" because it also answers for nested paths, where a
relative href would resolve against the missing directory, and it carries
noindex with no canonical tag since it has no URL of its own.

The sitemap stamped every entry with the build date, which told crawlers
both pages change daily. lastmod now comes from the last commit that
touched each page's HTML. A build with no reachable git history writes the
entry without a lastmod rather than with a plausible-looking date.

npm run check:release reported what the docs had not caught up with:
forgedialog 0.7.0 was published on 1 September. docs/releasing.md still
said the package was unpublished and framed the manual first publish and
the E404 troubleshooting entry as the likely path; both are now scoped to a
fork starting from an unpublished name. package.json homepage moves from
the README anchor to the site, listed under Unreleased because 0.7.0 is
already out and cannot carry it.

README no longer calls the deployment Cloudflare Pages.

Verified against wrangler dev with the production _headers: /, /docs and
every asset return 200; /nope and /deep/nested/nope return 404 with the
site's own page, its assets loading and the CSP applied; both pages and the
404 render with no console errors, no failed requests, and zero horizontal
overflow at 320, 380, 480 and 768px. npm run validate passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
main added a wrangler.jsonc pointing `assets.directory` at `./dist`, which
is the compiled library — .mjs, .cjs, .d.ts and .css with no index.html —
so a deploy from it serves nothing at /. This branch's version, pointing at
`./site`, is kept.

The conflict is also why CI stopped running here: workflows triggered by
`pull_request` build the merge ref, and GitHub cannot compute one for a
conflicting PR, so no run was created for the last two pushes. Merging the
base back in restores it.

"$schema" is left out rather than carried over: it resolves into
node_modules, and wrangler is deliberately not a dependency of this
package — deploys invoke it through npx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
The site advertised the library but barely demonstrated it. The landing page
had four hero buttons and the appearance playground; the "examples" section
was static code with a Copy button and nothing that ran. The full demo — the
one covering drawers, sheets, toasts, the palette, forms and wizards — was
demo/index.html at the repository root, which loads ../dist/ directly and is
never deployed.

site/demo.html runs all of it at /demo, against the same vendored build the
rest of the site uses: alert, confirm, prompt, validated prompt, open(),
stacking, animation options, drawers on both sides, bottom sheets, three
toast tones, the lightbox, loading states, the command palette, the
notification centre, form(), wizard(), and formWizard(). Each card pairs a
control that really runs with the snippet of the call being made, so the page
cannot drift from what it documents — a wrong snippet would show up as a
misbehaving button beside it. A plugin registered on the page logs afterOpen
and afterClose, and every demo prints its return value, so the result log is
also evidence the hooks fire.

demo.js uses a single delegated listener keyed by data-demo. The site's CSP
is script-src 'self', so inline handlers would be blocked; a demo that throws
prints the failure into the log rather than only the console.

The mark was only ever a favicon and an apple-touch icon: no page displayed
it. It now sits in the header of every page beside the wordmark, with alt=""
because the adjacent text already names the product and a screen reader
should not announce it twice.

The API calls were taken from the reviewed report in etc/ and the manual demo
page rather than written from memory, and the global surface was read out of
a real browser first, so nothing here calls an export that does not exist.

Verified in Chromium against wrangler dev with the production _headers CSP:
all 22 demo controls surface what they claim with no console errors and no
failed requests, the header mark renders on all four pages, the version badge
reads from the library's own export, and every page fits at 320, 380, 480,
768 and 1280px with zero horizontal overflow. npm run validate passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
PR #9 was squashed onto main as 631f43c, so this branch was restarted from
main and the demo work sits on top of it. The remote branch still points at
the pre-squash commits, whose content main already carries; recording them as
a parent lets this push fast-forward without discarding the branch history a
force push would have rewritten.

The tree is unchanged by this merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
Read against autumn.konexforge.com and forgeselect.konexforge.com — their
sources, since this environment's egress proxy refuses both hostnames — and
closed the gaps that were real rather than cosmetic.

The checker is the substantial one. ForgeSelect verifies its built site for
broken local links; scripts/check-site.mjs does that and the four other
things this site has actually got wrong: a fragment naming an id that does
not exist on the page it lands on, a link written in the .html form the
server answers with a redirect, JSON-LD that does not parse or an FAQPage
whose questions have drifted from the visible ones, and a page missing a
canonical, title, description or og:image — plus a 404 that declares a
canonical or forgets noindex, and any disagreement between pages about the
canonical origin. Each rule was verified by breaking the site and watching
it fail, not by watching it pass. It runs at the end of site:build, so a
broken site fails the Cloudflare deploy instead of reaching the domain.

Entity graph: an Organization node for KonexForge, referenced as the site's
publisher and the software's author. Both sibling sites are under the same
domain, and a shared @id is what lets a search engine treat them as one
publisher's products rather than three unrelated pages.

Positioning: the FAQ answered how the library differs from window.alert but
named no library. It now answers the comparison people actually search for —
SweetAlert2 and Bootstrap modals — factually: the native dialog element does
the focus and layering work, there are no runtime dependencies where a
Bootstrap modal needs Bootstrap, and an alert-only build is about 5.5 KB.

Metadata parity with the siblings: robots, author, explicit twitter:title,
twitter:description and twitter:image rather than relying on the og: tags
being inherited, og:image:type, rel=sitemap, and og:locale corrected from
"en" to the en_US form the property actually takes.

favicon.ico, rendered from the same icon.svg as every other icon: a 32×32
PNG in an ICO container, which needs no bitmap encoder and costs 1.2 KB.
Clients request that path whatever the markup says. build-icons.mjs writes
it alongside the other two and reproduces all three byte-for-byte.

Deliberately not adopted: meta keywords. Both siblings carry one and Google
has ignored it for years; the competitor terms belong in the visible FAQ
answer, where they are also true, and that is where they went.

Verified in Chromium against wrangler dev with the production _headers CSP:
all four pages carry the new tags in the live DOM, /favicon.ico serves as
image/vnd.microsoft.icon, the four schema types on the landing page parse
with Organization among them, and every page still fits at 320, 380 and
768px with zero horizontal overflow and no console errors. npm run validate
passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
Autumn Note ships its site in English and Vietnamese under /vi/; this one
did not. All three pages now do: /vi/, /vi/demo, and /vi/docs, translations
of the English pages rather than different content, mirroring them section
for section and keeping the same element ids so the scripts drive both
languages.

Nothing user-visible stayed hard-coded in those scripts. site.js, demo.js
and common.js now read their strings from a JSON data block in the page,
falling back to English — a data block rather than an inline script,
because the site's CSP is script-src 'self' and real inline code would be
blocked. The block's id is prefixed: "i18n" alone is already a section id
on the docs page, and getElementById would have found that section instead.
Nothing broke on that today, since the docs page loads neither script, but
it was a trap set for whoever added one.

common.js also passes the page's `labels` to the library's own setLabels(),
so a dialog opened from a Vietnamese page says Huỷ and Gửi rather than
Cancel and Submit. The theme toggle and copy buttons read their four
strings from data- attributes for the same reason. The code shown beside
each demo is translated too: the snippet has to be the call actually made,
or the page stops documenting itself.

hreflang: every page declares en, vi and x-default, and the sitemap repeats
the set as xhtml:link alternates on all six entries, as Autumn's does.
Assets on a /vi/ page are linked root-relative, because the page sits a
directory down and "./" would resolve inside /vi/.

check-site.mjs gained the hreflang rules, and they earned their place
immediately: writing this, both /vi/demo and /vi/docs shipped a language
switcher still marking EN as current, because the replacement that flips it
silently matched nothing. The checker now fails on a one-way annotation, a
missing x-default, and a page whose hreflang set omits its own canonical —
each verified by breaking the site and watching it fail. None of the three
is visible in a browser.

Verified in Chromium against wrangler dev with the production CSP: all six
pages return 200 with the right lang attribute, the switcher marking the
right side, the localised theme label, the mark rendered, and the version
badge read from the library's own export — with zero console errors, zero
failed requests, and zero horizontal overflow at 320 and 768px. The
Vietnamese demo opens a Vietnamese dialog with Vietnamese buttons while the
English one is unchanged. npm run validate passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
forgedialog advertised having no runtime dependencies while declaring react
and vue as peer dependencies and shipping a build of each. They were marked
optional, so nothing broke, but two frameworks sat in the dependency tree of
a library that has nothing to do with either, and every tool that reads peer
dependencies had to be told to ignore them.

The adapters move to packages/react and packages/vue, published as
forgedialog-react and forgedialog-vue, matching how the sibling repository
already ships forge-select-react and forge-select-vue. Each declares its own
framework as a peer dependency and the core as a real one, and each is built
with the framework and the core external so a wrapper never ships a second
copy of either.

Breaking, hence 0.8.0: forgedialog/react and forgedialog/vue are gone, and
the core no longer declares peerDependencies at all. The hook is unchanged,
so the migration is one import line, spelled out in the changelog.
forgedialog/svelte and /web-component stay in the core — neither needs a
framework runtime, so neither costs a peer dependency.

The wrappers depend on `forgedialog: >=0.7.0 <1.0.0`, not a caret: on a 0.x
core a caret pins the minor and would exclude the very release these ship
beside. check-release.mjs now covers every package a release publishes and
verifies each wrapper's range is one the release satisfies — proved by
setting three wrong ranges and watching each fail. It also refuses a caret
range rather than guessing at it.

Release plumbing: `npm publish --workspaces` runs after the core, since a
consumer installing a wrapper the moment it appears must find the core
already there; each package gets prepublishOnly so a clean checkout cannot
publish an empty dist; validate, lint, and check:package now cover the
workspaces.

The core's tarball is 63 files and 118.2 KiB, down from 71 and 120.3.

Verified end to end rather than by inspection: all three packages were
packed and installed into an empty project, where forgedialog-react and
forgedialog-vue export the hook, forgedialog/react and /vue no longer
resolve, /svelte still does, the core reports no peerDependencies, and a
real React render through the packed dist opens a dialog, clears `active` on
close, and destroys a still-open dialog on unmount. publint and attw pass on
both packages across node10, node16 CJS and ESM, and bundler.

One note for the record: this branch's lockfile gains
tsup/node_modules/esbuild@0.27.7, which npm audit flags low. It is not a
regression — main's lockfile simply omitted the entry while tsup@8.5.1 has
always required esbuild ^0.27.0, which the hoisted 0.28.1 cannot satisfy.
No patched 0.27.x exists and tsup@latest still pins that range, so forcing
0.28 on it would be an unsupported combination to silence a dev-server
advisory this repository never runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtJ3nDatGiv7dHjvZL1uZ4
PR #10 was squashed onto main as 19a9303, so this branch was restarted from
main and the package split sits on top of it. The remote branch still points
at the pre-squash commits, whose content main already carries; recording them
as a parent lets this push fast-forward without rewriting history.

The tree is unchanged by this merge.
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
forge-dialog 75f3c72 Commit Preview URL

Branch Preview URL
Sep 02 2026, 12:29 PM

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants