tw-plugin-rollup
tw-plugin-rollup
is a Rollup, Rolldown, and Vite plugin that lets you write a
TurboWarp / Scratch extension
across as many files as you like, then bundles it into the single
self-contained file TurboWarp expects.
It is the Rollup-ecosystem twin of
tw-plugin-webpack — same output, same options, for the
other half of the bundler world. Because it relies only on the standard Rollup
plugin API, the exact same plugin object works in Rollup 3/4, Rolldown,
and Vite (including Vite 8, which builds on Rolldown).
A normal TurboWarp extension is one file wrapped in an IIFE that registers itself:
That works, but it forces every block, helper, and constant into one file. This
plugin lets you split the extension into real ES modules and produces exactly
that IIFE for you, with Scratch.extensions.register(...) wired up
automatically.
How it works
Point your bundler at an entry module that export defaults the extension class
(or instance). The plugin:
- Configures the build to emit a single self-executing file
(
format: 'iife') whose entry export is captured in a local variable. - Wraps the whole bundle in the standard
(function (Scratch) { … })(Scratch)template. - Appends
Scratch.extensions.register(new YourExtension()).
Because the entire bundle lives inside the IIFE, every bare reference to the
Scratch global in your code binds to the local parameter — the "personal copy
of the Scratch API" the TurboWarp docs recommend. No import of Scratch is
needed (or possible); it's a host global, just like in a hand-written extension.
Install
Usage
Split your extension however you like:
Rollup / Rolldown
The plugin forces output.format = 'iife' and captures the entry export itself,
so you don't set format/name — just give it an input and an output file.
For Rolldown, import from rolldown instead of rollup; nothing else
changes.
Vite (incl. Vite 8 / Rolldown)
Build the extension as a single-file library. The plugin carries
enforce: 'pre', so its asset inlining runs ahead of Vite's own:
Build it, then load dist/my-extension.js in TurboWarp via add extension →
Custom Extension, or serve it and use ?extension=<url>.
Options
register accepts either a class (instantiated with new) or an
already-constructed instance (registered as-is).
Submitting to the gallery
The TurboWarp extensions gallery
requires a metadata header — a block of // Key: Value comments at the very top
of the file. Pass metadata and the plugin emits it above the IIFE:
produces, at the top of the bundle:
Fields are emitted in the conventional order Name → ID → Description → By →
Original → License → Context; by/original accept an array for multiple
lines, and any extra keys you add are appended verbatim. The header is injected
in renderChunk (after minification), so it's never stripped.
Icons
menuIconURI / blockIconURI expect a data: URI. With inlineAssets on (the
default), just import the image — the plugin's load hook reads the file
and resolves the import to a base64 data: URI string:
Details:
svg,png,jpg,gif,webp, andavifimports are inlined by default. Pass aRegExpto match your own set (inlineAssets: /\.(svg|png)$/i), orinlineAssets: falseto leave asset handling to your own config.- Inlining (rather than emitting a separate file) is required: a TurboWarp extension is a single file and can't reference external assets.
- TypeScript: add an asset module declaration so the import is typed, e.g.
declare module '*.svg' { const url: string; export default url; }. - Vite already inlines assets of its own accord. Set
inlineAssets: falseand let Vite handle it (raisingbuild.assetsInlineLimit), to avoid two layers fighting over the same import.
Notes & caveats
- One file only. TurboWarp loads a single
<script>, so don't use dynamicimport()/ code splitting — keep it one synchronous bundle. The plugin setsoutput.inlineDynamicImportsto enforce a single chunk. Scratchis a global, not an import. Your editor/linter may flag it; declare it as a readonly global (e.g. in ESLintlanguageOptions.globals). For TypeScript,@turbowarp/typesprovides theScratchtypings for Scratch extensions. If you're targeting TurboWarp or one of its forks, usetypes-twinstead, which adds the TurboWarp-specific APIs. It's no longer published to npm, so install it from git — it keeps the@turbowarp/typespackage name as a drop-in replacement:npm install @turbowarp/types@git+https://github.com/TurboWarp/types-tw.git#tw.- Source maps: prefer
sourcemap: false. A trailing//# sourceMappingURL=comment would otherwise sit in the middle of the wrapped file. (When sourcemaps are on, the plugin usesmagic-stringto keep the mapping correct across the wrapper.)