feat(plugin): twd() Vite plugin for plug-and-play dev setup#238
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
twd()Vite plugin (twd-js/vite-plugin) that auto-injectsinitTWD(...)at dev time via a virtual module +transformIndexHtml. Replaces the boilerplateif (import.meta.env.DEV) { ... initTWD(...) }block every consumer had to copy-paste into theirmain.{ts,tsx}.plugins: [react(), twd()]invite.config.tsis the entire integration. No entry-file changes, notwdHmr()required.initTWD/initTests/TWDSidebarAPIs are untouched. The plugin is opt-in; current setups keep working unchanged.How it works
virtual:twd/init. Inapply: 'serve',transformIndexHtmlinjects<script type="module" src="/@id/virtual:twd/init">. The virtual module's source containsimport { 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.import.meta.hot.acceptboundary, so Vite falls back to full reload on test edits — same effect astwdHmr(). ThetwdHmrplugin stays exported for users who haven't adoptedtwd().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-tripsrc/vite-plugin.ts— re-exportstwdandTwdPluginOptionsalongside existing pluginstwd-test-app/,tutorial-example/,vue-twd-example/(Angular intentionally out of scope)moduleResolution: \"Node\"→\"Bundler\"so Vite 8's conditionalexportstypes resolve and stop emitting TS2307 onimport type { Plugin } from 'vite'. Likely de-flakes future TS upgrades that previously broke the dts CI guard.lint-staged16→17,typescript-eslintpatch (TypeScript itself stays at~5.9.3)specs/2026-05-07-twd-vite-plugin-design.mdTest plan
npm run test:ci— expect 402/402npm run build— green, alldist/*.d.tsnon-empty (CI dts guard runs in workflow)npm run lint— 0 errorsexamples/twd-test-app/—npm run dev, sidebar appears, tests list and run, mock SW worksexamples/tutorial-example/—npm run dev, same checksexamples/vue-twd-example/—npm run dev, same checks, dark theme applied via plugin optionsdist/vite-plugin.d.tsexports bothtwdandTwdPluginOptionsFollow-ups (not in this PR)
🤖 Generated with Claude Code