crsdevtool
v1.1.0
Published
CLI tool to scaffold a Playwright/Vite project template.
Downloads
205
Maintainers
Readme
crsdevtool
A CLI tool for rapid A/B testing and experimentation on real websites (for development and internal use only, not for production deployment). It scaffolds a TypeScript/Vite project, lets you write and organize code in src/, and builds a single JavaScript file ready for injection into any site. Live preview happens in a Playwright-driven browser that injects your bundle into the real page — no CORS or CSP issues.
Features
- Project scaffolding: create a new experiment project with a single command.
- Native npm / pnpm / yarn support: the CLI detects which package manager invoked it and uses the same one everywhere.
- Vite 8 + TypeScript: modern build with live rebuild, SCSS support, and the native oxc minifier.
- Preview on real sites: Playwright opens your test URL and injects the built script on every page load.
- Reusable utilities: analytics (
pushData), DOM manipulation (jQuery-like$el), visibility tracking, and more — imported from'crsdevtool'and tree-shaken into the bundle. - Build-time injection: developer/project info (
startLog) and the Clarity heatmap call are injected during build and can't be accidentally deleted. - Git initialization: every scaffolded project starts with a repo and an initial commit.
Requirements
- Node.js
>=22.12.0(Vite 8 + native TypeScript type stripping; no ts-runner needed) - npm, pnpm, or yarn
Quick Start
# 1. Scaffold a project (npm)
npx crsdevtool my-project
# …or with pnpm
pnpm dlx crsdevtool my-project
# 2. Set your test URL in crs.config.ts, then develop with live preview
npm run dev # pnpm dev
# 3. Ship the optimized bundle
npm run build # pnpm buildThe CLI detects the invoking package manager (npx → npm, pnpm dlx → pnpm) and uses it to install dependencies in the new project. Override with --pm:
npx crsdevtool my-project --pm=pnpmScaffolding
Interactive (default)
npx crsdevtool my-project # or: npx crsdevtool . (current directory)Prompts for developer name, project name, test URL, language (TypeScript or JavaScript), and heatmap tool (Clarity or none; arrow-key selection). Everything is saved to crs.config.ts.
One-line (all fields via CLI args)
npx crsdevtool my-app "Project Name" "Dev Name" "https://test.url" clarity event_nameArguments: [target] [project name] [dev name] [test url] [tool: clarity|none] [event name]. Missing arguments are prompted interactively.
Empty install (no prompts)
npx crsdevtool my-app -y # or --yesCreates a project with empty fields and no heatmap tool.
Flags
| Flag | Effect |
|------|--------|
| -y, --yes | Skip all prompts, use empty defaults |
| --js | Scaffold a plain JavaScript project: src/index.js, no tsconfig.json, no TypeScript devDependencies |
| --pm=<npm\|pnpm\|yarn> | Force a package manager for dependency installation |
| --playwright-cli | Additionally install @playwright/cli and its agent skills |
Project Structure
my-project/
crs.config.ts # Project config (dev, name, testUrl, heatmapTool, heatmapEventName)
package.json # Dependencies and scripts
tsconfig.json # TypeScript config
.gitignore # Node, build output, Playwright artifacts, etc.
src/
index.ts # Your main entry file (index.js when scaffolded with --js)
globals.css # Your CSS styles
bin/ # Build machinery (you normally don't touch this)
app.ts # Playwright runner: opens browser, injects build/index.js
vite.config.mjs # Vite build configuration
global.d.ts # Global type declarations (dataLayer, clarity, …)Commands
Examples use npm; with pnpm the same commands are pnpm dev, pnpm start --browser=edge, pnpm build.
npm run dev / npm start — development with live preview
Both commands are identical:
npm run dev
npm start -- --browser=edge --device="iPhone 12"- Watches
src/and rebuilds on every change. - Opens a Playwright browser with DevTools, navigates to
testUrlfromcrs.config.ts, and injectsbuild/index.jsinto the page. - Re-injects on every page load and reloads the page after each rebuild.
- The dev bundle is unminified with sourcemaps: errors and
console.log()point tosrc/index.ts:42, not to minified code. - Exit with
Ctrl+C.
Browser and device options
npm run dev -- --browser=webkit
npm start -- --browser=edge --device="iPad Pro"- Browsers:
chromium(default),webkit,edge(uses the system Edge) - Devices: any name from Playwright's device registry (
"iPhone 12","Pixel 5", …)
npm run build — production bundle
npm run build
ls -lh build/index.js # check the size before shippingOne-time optimized build of build/index.js, ready for injection on live sites.
| | dev / start | build |
|---|---|---|
| Browser preview (Playwright) | ✅ | ❌ |
| Sourcemaps | ✅ | ❌ |
| Minification (oxc) | ❌ readable output | ✅ |
| Your console.* calls | ✅ kept | ❌ stripped |
| Core APIs (pushData, startLog, …) | ✅ | ✅ always kept |
Typical production sizes: a simple test is roughly 8–20KB raw (2–8KB gzipped), depending on how much code and CSS you write. If the size jumps unexpectedly, check for new imports — every external dependency is bundled in.
Creating Markup
A/B tests constantly create new elements. Besides plain template literals, the tool supports two approaches with proper syntax highlighting in any editor — no plugins required.
Static blocks: .html files
Import any HTML file as a string with Vite's ?raw suffix — the file gets full native HTML highlighting because it is an HTML file, and it's inlined into the bundle at build time:
// src/components/promo.html — regular HTML file
import promoHtml from './components/promo.html?raw';
document.querySelector('.target').insertAdjacentHTML('beforeend', promoHtml);Dynamic markup: JSX without a framework
.tsx / .jsx files compile straight to real DOM elements via crsdevtool/jsx-runtime (~150 bytes in the bundle, tree-shaken away when unused — no React, no virtual DOM). JSX highlighting is built into every editor, and in .tsx TypeScript also checks your markup:
// src/components/banner.tsx — no imports needed, the runtime is wired automatically
export const Banner = (price: string) => (
<div class="crs-banner">
<span class="crs-banner__title">Special offer</span>
<b class="crs-banner__price">{price}</b>
<button class="crs-banner__cta" onClick={() => trackClick()}>Buy now</button>
</div>
);
// src/index.ts
import { Banner } from './components/banner.tsx';
document.querySelector('.target').append(Banner('$9.99'));- Attributes are written as in HTML (
class, notclassName);onClick/onInput/… attach real event listeners. - Expressions interpolate with
{value}, fragments (<>…</>) and function components work as expected. - The result is an
HTMLElement— append it withappend()/insertAdjacentElement(), query into it, attach more listeners. - Works in both project flavors:
.tsxin TypeScript projects,.jsxin--jsprojects.
How the Build Works
- Vite 8 (rolldown) bundles
src/index.tsinto a single IIFE file — no chunks, safe to inject as one<script>. - Production builds are minified by the native oxc minifier with
console.*stripping; dev builds skip minification entirely and keep sourcemaps. - Tree-shaking removes unused
crsdevtoolexports: if you only usepushData(), nothing else is bundled. - CSS (including SCSS) is minified with LightningCSS and inlined via Vite's
?rawimports. startLog(...)(andclarityInterval(...)when Clarity is selected) are prepended to your entry at build time fromcrs.config.ts.
Bundle-size tips: prefer native browser APIs over utility libraries ([...new Set(arr)] instead of lodash), keep styles small, and re-check ls -lh build/index.js after adding imports.
Typical Workflow
npx crsdevtool my-test # scaffold (git repo + initial commit included)
cd my-test
# set testUrl in crs.config.ts
npm run dev # write code, test in the Playwright browser
git add . && git commit -m "Add experiment logic"
npm run build # produce the final build/index.js for injectionTroubleshooting
| Problem | Solution |
|---------|----------|
| Source files not shown in console | Use npm run dev / npm start, not npm run build; dev builds inline the sourcemap into build/index.js |
| Browser won't open | Check testUrl in crs.config.ts is set and reachable; verify Playwright: npm ls playwright |
| Browsers not installed | Run npx playwright install chromium webkit (normally done by postinstall) |
| Changes don't reload the page | Watch mode runs only in dev / start, not in build |
| Bundle too large | Compare after npm run build (dev bundles are intentionally bigger); look for heavy imports |
| console.log missing on the live site | Expected: production builds strip console.*; pushData/analytics still work |
API Reference
Core Functions
startLog({ name, dev })
Logs experiment/project info in the browser console with styled output.
pushData(name, desc, type, loc?)
Pushes analytics events to window.dataLayer for Google Analytics 4.
type: 'click' | 'view' | 'submit' | 'input' | 'change' | 'error' | 'success' | 'other'
clarityInterval(name)
Sets Microsoft Clarity custom event (only works when Clarity is enabled).
log(text, style?)
Console logging with colored styles.
style: 'info' | 'warn' | 'error' | 'success'
DOM Manipulation
$el(selector) - NativeQuery Class
jQuery-like DOM manipulation library without external dependencies.
// Select elements and chain methods
$el('.button')
.addClass('active')
.removeClass('inactive')
.toggleClass('highlighted')
.style('color', 'blue')
.attr('data-test', 'value')
.text('New Text')
.html('<span>HTML Content</span>')
.on('click', (event) => console.log('Clicked!'))
.each((element, index) => {
// Process each element
})
.find('.child-element')
.addClass('found');waitEl<T>(selector): Promise<T>
Wait for an element to appear in the DOM using MutationObserver.
Utility Functions
loadScriptsOrStyles(urls: string[])
Dynamically load external scripts (.js) or stylesheets (.css). Prevents duplicate loading.
visibilityOfTime(selector, eventName, visiblePlace, description?, time?, threshold?)
Track element visibility using IntersectionObserver. Fires analytics event after element is visible for specified time.
scrollToElement(selector, offset?)
Smooth scroll to an element with optional offset.
checkScrollSpeed(selector, callback)
Monitor scroll speed and direction. Works with window or any scrollable element.
getCookies(name)
Get cookie value by name.
slideup(element, duration?) / slidedown(element, duration?)
CSS transition-based slide animations.
Usage Example
import {
startLog,
pushData,
clarityInterval,
$el,
waitEl,
log,
visibilityOfTime,
scrollToElement
} from 'crsdevtool';
// Basic logging and analytics
pushData('button_click', 'User clicked CTA', 'click', 'header');
// DOM manipulation with the built-in jQuery-like library
$el('.button')
.addClass('active')
.style('color', 'red')
.on('click', () => {
log('Button clicked!', 'success');
});
// Wait for elements and track visibility
await waitEl('.dynamic-content');
visibilityOfTime('.product-card', 'product_view', 'homepage', 'Product card viewed', 2000);
// Smooth scrolling
scrollToElement('.target-section', 100);Note:
startLogand the Clarity call are injected automatically at build time — you don't need to call them yourself.
Legacy Functions (Removed)
$$el— use$el().elementsinsteadwaitForElement— usewaitElinsteadblockVisibility— usevisibilityOfTimeinstead
Internal Details
- The CLI copies only user-facing files from the template; build machinery lives in
bin/of the scaffolded project. - Dev/start spawn two linked processes:
vite build --watchand the Playwright runner (bin/app.ts, executed by Node's native TypeScript type stripping). If either exits, the other is killed. - No dependency ships native build scripts, so pnpm installs work without any
allowBuildsapprovals. - The Playwright context runs with
bypassCSP: true, which is what makes injection work on arbitrary sites. - Browser and device options are passed to the Playwright runner via environment variables.
crs.config.tsis the single source of truth for project metadata; both the Vite plugin and the Playwright runner read it.
Repository
- GitHub: conversionrate-store/crsdevtool
- NPM: crsdevtool
License
MIT
