Skip to content

CDN Installation #305

Description

@kevinccbsg

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):

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

Type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions