CDN Installation - Design
Date: 2026-07-21
Status: Draft (proposal, to be refined)
Affected repos: twd (bundle build + export)
Context
TWD is agnostic to any framework, and we already ship a bundled version. We
currently cover most of the popular frameworks. Building on the existing bundle,
it should be possible to publish a CDN-installable version of the library.
Goals
- Publish a CDN-consumable build of TWD from the existing bundle.
- Let vanilla JavaScript projects, HTMX projects, or any project use TWD without
a bundler in their toolchain.
- Support a "migrate to a framework" story: tests written against a vanilla app
via the CDN should survive a migration to React (or another framework) largely
unchanged, because TWD is framework-agnostic.
- Produce a showcase (example app + docs) demonstrating that migration path.
Why
This unlocks TWD for projects that do not use a framework build step at all:
plain vanilla sites and HTMX projects would get the benefits of TWD with a
simple script/import from a CDN.
Beyond raw reach, the more valuable angle is a funnel and a marketing narrative.
Someone can start testing a plain vanilla app with TWD via a single CDN script
tag, and then keep those same tests when they later migrate to React or another
framework. That reframes CDN from "reach a small vanilla/HTMX audience" into a
top-of-funnel acquisition play: "the best way to migrate to React" (or any
framework) with tests that survive the rewrite. Framework-agnostic tests that
outlive a migration are a genuinely differentiated story.
Proposed Approach
The bundle is straightforward to export and add to CDNs such as:
Publishing the bundle to these is largely a packaging exercise on top of what we
already build.
Recommended first step (derisk spike)
The CDN publish itself is easy: the bundle already exists and unpkg/jsdelivr/
esm.sh auto-mirror published npm packages. The real risk sits entirely on the
test-loading story (see Open Questions), and that risk is on the critical path of
the marketing narrative: a vanilla showcase needs users to actually write and
load tests smoothly, or the "easy win" is not easy.
So before committing to the showcase push, run a small spike: prove one vanilla
app loading and running TWD tests via the CDN with no bundler. If that flow is
smooth, the rest of this is genuinely a quick win.
Spike results (2026-07-21)
The spike was built and run (see examples/cdn-vanilla/). Findings:
- esm.sh works out of the box. A minimal import map (three twd-js entries, no
React mapping) loads twd-js/bundled, twd-js, and twd-js/runner in a plain
vanilla page, and a real test (selection + userEvent click + assertions) runs
and passes with no bundler. A docs-only path is viable today via esm.sh.
- Raw jsdelivr and unpkg currently fail at load with "Uncaught
ReferenceError: process is not defined" (dist/bundled.es.js). Root cause:
vite.bundle.config.ts has no define for process.env.NODE_ENV, so the bundle
ships unguarded process.env.NODE_ENV checks (the Preact/React dev-vs-prod
branch). esm.sh masks this by injecting a process polyfill; raw CDNs serve
the file verbatim, so process is undefined and it throws. Chai's
process.versions check is already guarded with typeof process, so only the
NODE_ENV branch is the blocker.
- The main
twd-js entry has a bare import ... from "react", so on raw CDNs
React must also be mapped in the import map. esm.sh resolves it automatically.
- Request mocking works (validated with
index-mock.html + mock.twd.js).
The library loads from the CDN, but the service worker (mock-sw.js) must be
served same-origin, because browsers only allow same-origin SW registration.
That is not a TWD limitation and not a blocker: the documented step is to get
mock-sw.js onto the user's own domain (npx twd-js init public or a direct
download from the CDN) and point serviceWorkerUrl at it. mock-sw.js has no
process references, so it is unaffected by the bundle issue above.
Two small integration nuances the docs must call out (found while running the
mock spike):
- Service worker path is relative to where the page is served. For a real app
with the SW at the site root and the page at the origin root, /mock-sw.js
(absolute) is correct and gives the SW root scope. When the page is served from
a subpath (as the spike is), use a relative ./mock-sw.js so it resolves within
that folder. It still intercepts root requests like /api/user, because a page
controlled by the SW has all its fetches intercepted regardless of request path.
- screenDom wants an app root. In a plain vanilla page with no framework root,
TWD logs "screenDom could not find a known app root (#root / #app / app-root)"
and falls back to a best-guess container. The test still passes, but the docs
should tell vanilla users to either wrap their app in a known root
(<div id="app">) or pass rootSelector to initTWD.
HTMX POC
Built and validated an HTMX variant (examples/cdn-vanilla/index-htmx.html +
htmx.twd.js): HTMX from a CDN plus TWD from esm.sh in the same no-build page, a
button driven purely by hx-get/hx-target, mocked with twd.mockRequest.
- The integration works. The service worker intercepts HTMX's XHR (SWs
intercept XHR and fetch alike), so mocking needs no extra setup, and loading
HTMX + TWD together from CDNs is fine.
- New finding: mock responses are JSON-only.
src/cli/utils/mockResponse.js
always JSON.stringify(response). That fits JSON APIs, but HTMX swaps the raw
body as HTML, so a mocked '<span>...</span>' comes back JSON-quoted
("<span>...</span>"): the element renders and the test passes, but stray
quote characters appear around it.
- Implication: first-class HTMX / hypermedia support wants a mock option to
return a raw, non-stringified body (detect string responses, or a raw: true /
responseType option). Small, contained change. Independent of the JSON/CDN
story already validated, so it is a separate optional enhancement, not a
blocker. Candidate for the HTMX slice of this backlog item.
Decision: docs-only via esm.sh
Chosen direction: docs only, standardize on esm.sh. The esm.sh path works
today with a three-line import map and no code changes, and it is enough for the
vanilla and migration-showcase story. We are not adding raw jsdelivr/unpkg
support for now.
Trade-off accepted: this leans on esm.sh as a single CDN vendor. That is low risk
here because TWD loads only at development time (it is not shipped to end users),
and esm.sh is widely used and reliable. Documenting a pinned version
(twd-js@x.y.z) keeps it deterministic.
Deferred (not needed for the chosen path; revisit only if we later want to drop
the esm.sh dependency or offer raw-CDN usage):
- Fix the
process leak in vite.bundle.config.ts
(define: { 'process.env.NODE_ENV': JSON.stringify('production') }). Would
make the bundled entry load on raw jsdelivr/unpkg too, and would shrink the
bundle by dropping the dev-only branch. Low effort, low risk, but not required
while we standardize on esm.sh.
- Re-export the test API from
twd-js/bundled (twd, expect,
userEvent, screenDom, describe, it) for a single-import, zero
import-map experience. A DX nicety, optional.
Open Questions (to refine)
- Test loading without a bundler is resolved by the spike: build the test-module
map by hand ({ './x.twd.js': () => import('./x.twd.js') }) since
import.meta.glob is bundler-only. Remaining question is whether we document
that pattern as-is or provide a small helper.
- We likely need to provide a mechanism for this (a documented convention or a
small loader helper) so CDN users have a supported way to register their tests.
Apart from solving test loading, the rest of this is achievable.
- Shape of the migration showcase: which vanilla starter, how the "same tests
after migrating to React" claim is demonstrated concretely, and what stays
identical vs what has to change across the migration.
Value
Extends TWD's reach to the whole "no build step" segment (vanilla, HTMX, and
similar), consistent with TWD being framework-agnostic. More importantly, it
opens a top-of-funnel acquisition and marketing narrative: start testing a
vanilla app via CDN, then migrate to a framework and keep the same tests. That
"tests that survive the migration" story is the main value, beyond raw reach.
CDN Installation - Design
Date: 2026-07-21
Status: Draft (proposal, to be refined)
Affected repos:
twd(bundle build + export)Context
TWD is agnostic to any framework, and we already ship a bundled version. We
currently cover most of the popular frameworks. Building on the existing bundle,
it should be possible to publish a CDN-installable version of the library.
Goals
a bundler in their toolchain.
via the CDN should survive a migration to React (or another framework) largely
unchanged, because TWD is framework-agnostic.
Why
This unlocks TWD for projects that do not use a framework build step at all:
plain vanilla sites and HTMX projects would get the benefits of TWD with a
simple script/import from a CDN.
Beyond raw reach, the more valuable angle is a funnel and a marketing narrative.
Someone can start testing a plain vanilla app with TWD via a single CDN script
tag, and then keep those same tests when they later migrate to React or another
framework. That reframes CDN from "reach a small vanilla/HTMX audience" into a
top-of-funnel acquisition play: "the best way to migrate to React" (or any
framework) with tests that survive the rewrite. Framework-agnostic tests that
outlive a migration are a genuinely differentiated story.
Proposed Approach
The bundle is straightforward to export and add to CDNs such as:
Publishing the bundle to these is largely a packaging exercise on top of what we
already build.
Recommended first step (derisk spike)
The CDN publish itself is easy: the bundle already exists and unpkg/jsdelivr/
esm.sh auto-mirror published npm packages. The real risk sits entirely on the
test-loading story (see Open Questions), and that risk is on the critical path of
the marketing narrative: a vanilla showcase needs users to actually write and
load tests smoothly, or the "easy win" is not easy.
So before committing to the showcase push, run a small spike: prove one vanilla
app loading and running TWD tests via the CDN with no bundler. If that flow is
smooth, the rest of this is genuinely a quick win.
Spike results (2026-07-21)
The spike was built and run (see
examples/cdn-vanilla/). Findings:React mapping) loads
twd-js/bundled,twd-js, andtwd-js/runnerin a plainvanilla page, and a real test (selection +
userEventclick + assertions) runsand passes with no bundler. A docs-only path is viable today via esm.sh.
ReferenceError: process is not defined" (
dist/bundled.es.js). Root cause:vite.bundle.config.tshas no define forprocess.env.NODE_ENV, so the bundleships unguarded
process.env.NODE_ENVchecks (the Preact/React dev-vs-prodbranch). esm.sh masks this by injecting a
processpolyfill; raw CDNs servethe file verbatim, so
processis undefined and it throws. Chai'sprocess.versionscheck is already guarded withtypeof process, so only theNODE_ENV branch is the blocker.
twd-jsentry has a bareimport ... from "react", so on raw CDNsReact must also be mapped in the import map. esm.sh resolves it automatically.
index-mock.html+mock.twd.js).The library loads from the CDN, but the service worker (
mock-sw.js) must beserved same-origin, because browsers only allow same-origin SW registration.
That is not a TWD limitation and not a blocker: the documented step is to get
mock-sw.jsonto the user's own domain (npx twd-js init publicor a directdownload from the CDN) and point
serviceWorkerUrlat it.mock-sw.jshas noprocessreferences, so it is unaffected by the bundle issue above.Two small integration nuances the docs must call out (found while running the
mock spike):
with the SW at the site root and the page at the origin root,
/mock-sw.js(absolute) is correct and gives the SW root scope. When the page is served from
a subpath (as the spike is), use a relative
./mock-sw.jsso it resolves withinthat folder. It still intercepts root requests like
/api/user, because a pagecontrolled by the SW has all its fetches intercepted regardless of request path.
TWD logs "screenDom could not find a known app root (#root / #app / app-root)"
and falls back to a best-guess container. The test still passes, but the docs
should tell vanilla users to either wrap their app in a known root
(
<div id="app">) or passrootSelectortoinitTWD.HTMX POC
Built and validated an HTMX variant (
examples/cdn-vanilla/index-htmx.html+htmx.twd.js): HTMX from a CDN plus TWD from esm.sh in the same no-build page, abutton driven purely by
hx-get/hx-target, mocked withtwd.mockRequest.intercept XHR and fetch alike), so mocking needs no extra setup, and loading
HTMX + TWD together from CDNs is fine.
src/cli/utils/mockResponse.jsalways
JSON.stringify(response). That fits JSON APIs, but HTMX swaps the rawbody as HTML, so a mocked
'<span>...</span>'comes back JSON-quoted(
"<span>...</span>"): the element renders and the test passes, but strayquote characters appear around it.
return a raw, non-stringified body (detect string responses, or a
raw: true/responseType option). Small, contained change. Independent of the JSON/CDN
story already validated, so it is a separate optional enhancement, not a
blocker. Candidate for the HTMX slice of this backlog item.
Decision: docs-only via esm.sh
Chosen direction: docs only, standardize on esm.sh. The esm.sh path works
today with a three-line import map and no code changes, and it is enough for the
vanilla and migration-showcase story. We are not adding raw jsdelivr/unpkg
support for now.
Trade-off accepted: this leans on esm.sh as a single CDN vendor. That is low risk
here because TWD loads only at development time (it is not shipped to end users),
and esm.sh is widely used and reliable. Documenting a pinned version
(
twd-js@x.y.z) keeps it deterministic.Deferred (not needed for the chosen path; revisit only if we later want to drop
the esm.sh dependency or offer raw-CDN usage):
processleak invite.bundle.config.ts(
define: { 'process.env.NODE_ENV': JSON.stringify('production') }). Wouldmake the bundled entry load on raw jsdelivr/unpkg too, and would shrink the
bundle by dropping the dev-only branch. Low effort, low risk, but not required
while we standardize on esm.sh.
twd-js/bundled(twd,expect,userEvent,screenDom,describe,it) for a single-import, zeroimport-map experience. A DX nicety, optional.
Open Questions (to refine)
map by hand (
{ './x.twd.js': () => import('./x.twd.js') }) sinceimport.meta.globis bundler-only. Remaining question is whether we documentthat pattern as-is or provide a small helper.
small loader helper) so CDN users have a supported way to register their tests.
Apart from solving test loading, the rest of this is achievable.
after migrating to React" claim is demonstrated concretely, and what stays
identical vs what has to change across the migration.
Value
Extends TWD's reach to the whole "no build step" segment (vanilla, HTMX, and
similar), consistent with TWD being framework-agnostic. More importantly, it
opens a top-of-funnel acquisition and marketing narrative: start testing a
vanilla app via CDN, then migrate to a framework and keep the same tests. That
"tests that survive the migration" story is the main value, beyond raw reach.