TypeScript in the browser. No build step. No npm. No terminal.
<script src="https://unpkg.com/rawscript"></script>
<script type="module" src="./main.ts"></script>That is the entire setup. main.ts can use TypeScript syntax, import from npm, and use JSX. It just works.
rawscript registers a Service Worker that intercepts every .ts and .tsx network request before it reaches the browser's module loader. The SW fetches the raw source, compiles it in-browser using esbuild-wasm, rewrites bare specifiers to esm.sh CDN URLs, and returns the result as application/javascript.
From the browser's perspective, it made a request for a .js file. It has no idea TypeScript was involved.
Browser Service Worker Network
────── ────────────── ───────
import './main.ts' → fetch('/main.ts') → your server
← transpile(source)
← rewriteImports(js)
import './main.js' ← Response(js, { 'Content-Type': 'application/javascript' })
The esbuild WASM binary (~8MB) is pre-cached on the SW's install event. After the first load, compilation is instant.
Bare specifiers are automatically rewritten to esm.sh URLs. No importmap required.
// This works. No npm install. No importmap.
import React, { useState } from 'react'
import { createRoot } from 'react-dom/client'
const App = () => {
const [n, setN] = useState(0)
return <button onClick={() => setN(n => n + 1)}>count: {n}</button>
}
createRoot(document.getElementById('app')!).render(<App />)To pin a version: import React from 'react@18.3.1'
To use a subpath: import { signal } from '@preact/signals-core'
If you have an importmap in the HTML, rawscript respects it. Any specifier already mapped will not be rewritten.
rawscript detects the JSX transform from your importmap.
| Framework | importmap entry | JSX source |
|---|---|---|
| React (default) | — | react |
| Preact | "react": "https://esm.sh/preact/compat" |
preact/compat |
| Solid | "solid-js": "https://esm.sh/solid-js" |
solid-js/h |
| Vue | — | No JSX; use h() or defineComponent |
Vue .vue SFC files are not supported. Use the Composition API with .ts files and h().
See examples/ for working demos of each.
rawscript is a development tool. For production, use the CLI to produce a minified, CDN-free bundle:
npx rawscript buildReads index.html, finds all <script type="module" src="*.ts"> entries, bundles them with esbuild (Node API, not WASM), and writes to dist/. The output has no CDN dependencies and no reference to rawscript itself.
npx rawscript build --entry index.html --out dist --no-minify
npx rawscript serve # static file server on :3000, no configThe CLI uses esbuild and commander as dependencies. These are never part of the browser runtime.
rawscript/
├── packages/
│ ├── runtime/ # Zero-dependency browser library
│ │ └── src/
│ │ ├── boot.ts # Main thread: SW registration, reload logic
│ │ ├── sw.ts # Service Worker: fetch interception, orchestration
│ │ ├── transpiler.ts # esbuild-wasm wrapper, lazy-initialized
│ │ ├── resolver.ts # Bare import → esm.sh rewriter (pure regex)
│ │ ├── loader.ts # First-load progress indicator
│ │ ├── hmr.ts # BroadcastChannel-based change notification
│ │ ├── watcher.ts # ETag polling, dev mode only
│ │ ├── fallback.ts # Blob URL fallback for file:// and sandboxed iframes
│ │ ├── env.ts # Environment detection (SW available, isDev, etc.)
│ │ ├── errors.ts # Error overlay, sourcemap-aware
│ │ └── debugpanel.ts # Ctrl+Shift+R dev panel
│ └── cli/
│ └── src/
│ ├── index.ts # commander entry, build + serve commands
│ ├── bundler.ts # esbuild Node API wrapper
│ └── html.ts # HTML parser: finds .ts entries, rewrites output
└── examples/
├── vanilla/
├── react/
├── preact/
├── vue/
├── solid/
└── three/
The invariant that must never break: packages/runtime/package.json → "dependencies": {}. The runtime is static files served from a CDN. It has no install step because it imposes no install step.
| File | Format | Entry | Size |
|---|---|---|---|
dist/rawscript.js |
IIFE | boot.ts |
~2kb min+gz |
dist/rawscript-sw.js |
ESM | sw.ts |
~300kb (esbuild-wasm ref, not inlined) |
rawscript.js is the script tag users include. It registers the SW and handles the initial reload. rawscript-sw.js is registered at /rawscript-sw.js by default and does all the actual work.
The SW must be served from your origin. When loading rawscript from unpkg, add data-sw-inline:
<script src="https://unpkg.com/rawscript" data-sw-inline></script>This fetches rawscript-sw.js and registers it as a Blob URL, bypassing the cross-origin restriction. Slightly slower to register on first load, but functionally identical.
To host it yourself (recommended for production-like setups):
dist/rawscript-sw.js → /rawscript-sw.js (serve from root)
Or configure a custom path:
<script src="..." data-sw-path="/static/rawscript-sw.js"></script>In dev mode (localhost or 127.0.0.1), rawscript polls your .ts files using HEAD requests and compares ETag / Last-Modified headers. On change, the SW busts its module cache for that file and the page reloads.
No dev server required. No WebSocket. No Node process. Polling interval defaults to 1000ms and is configurable:
<script src="..." data-hmr-interval="500"></script>Hot reload is disabled in production (any non-localhost origin).
Service Workers are unavailable on file:// protocol, in cross-origin iframes, and in some browser configurations. In these contexts, rawscript automatically switches to a Blob URL fallback:
- Finds all
<script type="module" src="*.ts">tags in the document - Fetches each source file, transpiles it in the main thread (esbuild-wasm, same WASM binary)
- Rewrites imports, resolves relative imports recursively
- Replaces each script's
srcwith aBlobURL
The fallback is slower (no cross-load caching, main thread transpilation) and logs a warning. It exists so rawscript works everywhere, not as a preferred path.
| Browser | Minimum version | Notes |
|---|---|---|
| Chrome / Edge | 89 | Full support |
| Firefox | 84 | Full support |
| Safari | 15.4 | Full support |
| Firefox ESR | 91 | Full support |
| IE | — | Not supported |
Requirements: Service Workers, ES modules, BroadcastChannel, fetch. All present in any browser released after 2021.
rawscript is the right tool in specific contexts. It is not a replacement for a proper build setup.
Use rawscript when:
- Prototyping or building demos where setup friction matters
- Teaching TypeScript without requiring a dev environment
- Embedding interactive TypeScript examples in docs or articles
- Building single-page tools that you want to share as a raw HTML file
Do not use rawscript when:
- You need reproducible, auditable production builds
- Your app imports large dependency graphs (each is fetched individually from esm.sh)
- You need tree-shaking or code splitting on dependencies
- Offline support is a requirement (esm.sh dependencies require network)
Performance characteristics:
- First load: ~2–4s while esbuild WASM initializes and caches (shown as a loading indicator)
- Subsequent loads: <100ms (WASM served from SW cache, transpiled output cached per-file)
- Transpilation: ~5–50ms per file depending on size (esbuild is fast even in WASM mode)
- Production build via CLI: uses esbuild Node API, not WASM — full speed
git clone https://github.com/rawscript/rawscript
cd rawscript
pnpm install
pnpm build # builds packages/runtime/dist/To run an example:
pnpm serve # serves project root on :3000
# open http://localhost:3000/examples/react/There is no watch mode for the examples. Edit a source file in packages/runtime/src/, run pnpm build, reload. The SW's HMR handles .ts file changes in examples during development — it does not handle changes to the runtime itself.
pnpm test # Playwright end-to-end tests
pnpm typecheck # tsc --noEmit across all packagesTests launch a real browser via Playwright. There are no unit tests for the SW itself — integration tests against a real browser are more reliable for fetch interception behavior.
- Create
packages/runtime/src/yourmodule.ts - Import it from
sw.tsorboot.ts(wherever it belongs) - Run
pnpm build— esbuild picks it up automatically - Never add it to
"dependencies"inpackage.json
The version is pinned in two places and must match:
packages/runtime/src/transpiler.ts → wasmURL: 'https://unpkg.com/esbuild-wasm@X.X.X/esbuild.wasm'
packages/runtime/src/sw.ts → import ... from 'https://unpkg.com/esbuild-wasm@X.X.X/esm/browser.js'
Also bump the cache name in sw.ts (rawscript-wasm-vN) to force re-fetch on existing installs.
Releases are automated via GitHub Actions on version tags.
npm version patch # or minor / major
git push --follow-tagsThe publish.yml workflow runs npm ci, npm test, and npm publish from packages/runtime/. The CLI (packages/cli/) is published separately under the same rawscript package name with a bin entry.
Issues and PRs are welcome. A few things to know before contributing:
- The zero-dependency constraint is absolute. If your change requires adding a runtime dependency to
packages/runtime/package.json, it will not be merged regardless of how useful the feature is. - The SW is the critical path. Changes to
sw.tsortranspiler.tsrequire Playwright tests that cover the actual fetch interception. Untested SW changes have caused subtle bugs that only manifest in specific browser versions. - resolver.ts is intentionally regex-based. We know a proper AST parser would be more correct. We have chosen not to add one. The regex handles all real-world cases we've encountered. If you've found one it doesn't handle, open an issue with a reproduction.
See CONTRIBUTING.md for setup details and the list of good first issues.
- esm.sh — CDN that makes this possible
- esbuild — the compiler rawscript runs in your browser
- ts-blank-space — alternative approach: strips types without transpiling
- TypeScript playground — sandboxed, no import support
- StackBlitz — full dev environment, requires their infrastructure
rawscript's niche is the space between "paste into a playground" and "set up a real project." It works with your own files, on your own server, with no account.
MIT © rawscript contributors