tw-plugin-webpack
tw-plugin-webpack
is a webpack and Rspack 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.
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 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:
Add the plugin to your bundler config (this example uses Rspack; webpack is identical):
The plugin reaches the bundler only through compiler.webpack, which resolves
on both webpack 5 and Rspack — so the same config shape works for either.
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
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 configures the bundler to
inline it as 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; }. - If you build with Rsbuild/Rslib (which already handle SVG imports), set
inlineAssets: falseto avoid a duplicate asset rule.
Notes & caveats
- One file only. TurboWarp loads a single
<script>, so don't use dynamicimport()/ code splitting — keep it one synchronous bundle. 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
devtool: false. A trailing//# sourceMappingURL=comment would otherwise sit in the middle of the wrapped file.