Skip to content

feat(plugin): twd() Vite plugin for plug-and-play dev setup#238

Merged
kevinccbsg merged 19 commits into
mainfrom
feat/vite-plugin-poc
May 8, 2026
Merged

feat(plugin): twd() Vite plugin for plug-and-play dev setup#238
kevinccbsg merged 19 commits into
mainfrom
feat/vite-plugin-poc

Conversation

@kevinccbsg
Copy link
Copy Markdown
Member

@kevinccbsg kevinccbsg commented May 6, 2026

Summary

  • New twd() Vite plugin (twd-js/vite-plugin) that auto-injects initTWD(...) at dev time via a virtual module + transformIndexHtml. Replaces the boilerplate if (import.meta.env.DEV) { ... initTWD(...) } block every consumer had to copy-paste into their main.{ts,tsx}.
  • Plug-and-play for real consumers: plugins: [react(), twd()] in vite.config.ts is the entire integration. No entry-file changes, no twdHmr() required.
  • Non-breaking: existing initTWD / initTests / TWDSidebar APIs are untouched. The plugin is opt-in; current setups keep working unchanged.

How it works

  • Plugin owns the virtual module virtual:twd/init. In apply: 'serve', transformIndexHtml injects <script type="module" src="/@id/virtual:twd/init">. The virtual module's source contains import { initTWD } from 'twd-js/bundled' + import.meta.glob(<pattern>) + initTWD(tests, <inlined options>).
  • apply: 'serve' means the plugin is a hard no-op in production builds — zero TWD code reaches consumer prod bundles.
  • Test-file HMR full-reload comes for free: the virtual module has no import.meta.hot.accept boundary, so Vite falls back to full reload on test edits — same effect as twdHmr(). The twdHmr plugin stays exported for users who haven't adopted twd().

What's in the diff

  • src/plugin/twd.ts — the plugin (~120 LOC including JSDoc)
  • src/tests/plugin/twd.spec.ts — 10 unit tests covering all hooks + options round-trip
  • src/vite-plugin.ts — re-exports twd and TwdPluginOptions alongside existing plugins
  • Examples migrated: twd-test-app/, tutorial-example/, vue-twd-example/ (Angular intentionally out of scope)
  • Drive-by tsconfig fix: moduleResolution: \"Node\"\"Bundler\" so Vite 8's conditional exports types resolve and stop emitting TS2307 on import type { Plugin } from 'vite'. Likely de-flakes future TS upgrades that previously broke the dts CI guard.
  • Dependency bumps: lint-staged 16→17, typescript-eslint patch (TypeScript itself stays at ~5.9.3)
  • Design spec: specs/2026-05-07-twd-vite-plugin-design.md

Test plan

  • npm run test:ci — expect 402/402
  • npm run build — green, all dist/*.d.ts non-empty (CI dts guard runs in workflow)
  • npm run lint — 0 errors
  • examples/twd-test-app/npm run dev, sidebar appears, tests list and run, mock SW works
  • examples/tutorial-example/npm run dev, same checks
  • examples/vue-twd-example/npm run dev, same checks, dark theme applied via plugin options
  • Confirm dist/vite-plugin.d.ts exports both twd and TwdPluginOptions

Follow-ups (not in this PR)

  • Real-project validation in production-shaped consumer apps before docs / AI skill updates.
  • Angular adoption path (Angular doesn't use Vite the same way; needs a separate design).
  • Out-of-scope deferrals captured in the spec: bundling `twdHmr` / `removeMockServiceWorker` into `twd()`, auto-installing the service worker, multi-pattern `testFilePattern`, function-valued options.

🤖 Generated with Claude Code

kevinccbsg and others added 19 commits May 7, 2026 00:06
Captures the design for moving the dev-only initTWD(...) block out of
user entry files into a single Vite plugin call. POC scope, validation
criteria, backward-compat plan, and deferred follow-ups documented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial plugin shell with name 'twd' and apply: 'serve' so it only
runs at dev time. Hooks added in subsequent tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
resolveId / load hooks emit a virtual:twd/init module containing
import { initTWD } + import.meta.glob + initTWD call with default
options inlined via JSON.stringify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks the contract that custom init options are JSON-serialized into
the virtual module and that testFilePattern is consumed by the plugin
without leaking into initTWD's options object.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a <script type="module" src="/@id/virtual:twd/init"> tag to the
served HTML so the virtual module executes in the browser at dev time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds twd() and TwdPluginOptions to the public vite-plugin entry
alongside the existing twdHmr and removeMockServiceWorker exports.
Includes JSDoc + @example matching the twdHmr documentation style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the entry-file initTests/initRequestMocking block; the new
twd() Vite plugin handles both via the injected virtual init module.
The twd-relay browser-client init stays in main.tsx (separate concern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous alias pointed to src/bundled.tsx, exposing the source
file to the test app's React JSX transform. The resulting React
vnodes are frozen by React.createElement in dev, so Preact's
render() crashed trying to attach __ properties.

Switching the alias to dist/bundled.es.js uses the artifact built
with @preact/preset-vite (Preact JSX baked in), eliminating the
mismatch.

Documents the same caveat in the design spec: in-repo / local-dist
consumers must alias to the built file, not source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tutorial-example: drop the dev-only initTWD block from main.tsx and
register twd() in vite.config.ts; alias twd-js/bundled to the local
src/dist copy since the tutorial does not install twd-js as a package.

vue-twd-example: same migration, plus relocate the dark theme literal
into vite.config.ts so initTWD options live in one place. main.ts is
now a 5-line Vue bootstrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vite 8 ships its types only through conditional package.json exports,
which the legacy 'Node' moduleResolution cannot read. The result was
TS2307 errors on every 'import type { Plugin } from "vite"' across
the existing twdHmr / removeMockServiceWorker plugins (and the new
twd plugin inherited the same issue).

'Bundler' is the recommended setting for Vite-based projects and is
already consistent with the existing module: ESNext / target: ESNext.
402/402 tests pass and npm run build succeeds after the change.

The remaining tsc --noEmit complaints (fs/path in
removeMockServiceWorker.ts, two unrelated url.spec.ts findings) are
pre-existing and out of scope for this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The typescript-eslint 8.59.2 patch tightens no-floating-promises and
flagged two pre-existing call sites in twdHmr.spec.ts where the
handleHotUpdate hook (typed as void | Promise<void>) was invoked
without awaiting. Prefix the calls with the void operator so the
fire-and-forget intent is explicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates the docs to recommend the new twd() Vite plugin as the
primary setup for Vite-based projects (React, Vue, Solid.js, Astro).
The manual initTWD bundled approach is now positioned as the
fallback for non-Vite projects (Angular, Webpack/CRA) and as a
secondary option for Vite users who need full control.

Pages updated:
- README.md — installation snippet
- docs/.vitepress/theme/components/HomePage.vue — quick start
- docs/getting-started.md — primary setup + manual non-Vite section
- docs/tutorial/installation.md — tutorial first install step
- docs/frameworks.md — React/Vue/Solid lead with plugin; Angular and CRA stay manual; Astro updated to use plugin via vite block
- docs/theming.md — theme passed via plugin in vite.config.ts
- docs/api/index.md — twd() plugin documented first; initTWD as manual API
- docs/testing-library.md — rootSelector example shows both plugin and manual
- docs/api-mocking.md — service worker registration via plugin (default)
- docs/tutorial/api-mocking.md — same pattern in tutorial flavor
- docs/coverage.md + tutorial/coverage.md — twd() plus istanbul example
- docs/tutorial/production-builds.md — removeMockServiceWorker import path corrected
- docs/claude-plugin.md — auto-setup uses plugin
- docs/ai-remote-testing.md — twd() and twdRemote() shown together

npx twd-js init public is still mentioned in getting-started and
api-mocking — auto-install hasn't shipped yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the request-mocking toggles (serviceWorker, serviceWorkerUrl)
in the function-level JSDoc so they're discoverable via IntelliSense
without expanding the options interface. Caught during real-project
migration where a reviewer concluded the options had been dropped
because they weren't shown in the function's example block.

No runtime change — JSDoc only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plugin was emitting a hardcoded "/@id/virtual:twd/init" script
tag in transformIndexHtml(), which 404s on dev servers configured
with a non-root base (e.g. base: "/platform-admin/") because Vite
serves all dev resources under that prefix.

Capture the resolved base via configResolved() and prefix:
  - script src in transformIndexHtml — always
  - default serviceWorkerUrl — only when the user did NOT explicitly
    set it (so user-supplied paths like "/platform-admin/mock-sw.js"
    are not double-prefixed)

Default base "/" preserves existing behavior. Tests cover all four
combinations (base x serviceWorkerUrl override).

Reported by a real-project consumer with:
  base: "/platform-admin/"
  → GET http://localhost:5173/@id/virtual:twd/init 404

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kevinccbsg kevinccbsg merged commit 6039884 into main May 8, 2026
10 checks passed
@kevinccbsg kevinccbsg deleted the feat/vite-plugin-poc branch May 8, 2026 14:42
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.

1 participant