The conversion process involves several key components working together:
An .sb3 file is simply a ZIP archive containing:
project.json — The project definition (blocks, sprites, variables, etc.).svg, .png) and sounds (.wav, .mp3) named by their content hashmodule.exports = function (input, isSprite, callback) {
unpack(input, isSprite)
.then(function (unpackedProject) {
return parse(unpackedProject[0])
.then(validate.bind(null, isSprite))
.then(function (validatedProject) {
return [validatedProject, unpackedProject[1]];
});
})
.then(callback.bind(null, null), callback);
};
Three steps:
unpack.js — Detects file format by byte signature (80 75 = ZIP) and extracts using JSZipparse.js — Parses the JSON string using turbowarp/json (handles control characters)validate.js — Detects Scratch version (SB2 vs SB3) by checking for targets or objName keysThe Scaffolding class (src/scaffolding/scaffolding.js) is a minimal Scratch project player.
import VM from 'scratch-vm';
import Renderer from 'scratch-render';
import ScratchStorage from '@turbowarp/scratch-storage';
import AudioEngine from 'scratch-audio';
import {BitmapAdapter} from '@turbowarp/scratch-svg-renderer';
scratch-vm — The Scratch Virtual Machine that compiles blocks to JavaScript and executes themscratch-render — WebGL renderer for sprites and penscratch-storage — Asset loading systemscratch-audio — Sound playback enginesetup () {
this.vm = new VM();
this.vm.setCompatibilityMode(true);
this.vm.setLocale(navigator.language);
this.vm.on('MONITORS_UPDATE', this._onmonitorsupdate.bind(this));
this.vm.runtime.on('QUESTION', this._onquestion.bind(this));
// ... renderer, storage, audio engine initialization
}
loadProject (data) {
return this.vm.loadProject(data)
.then(() => {
this.vm.setCloudProvider(this.cloudManager);
this.cloudManager.projectReady();
this.renderer.draw();
// ...
});
}
The vm.loadProject(data) call is where the magic happens — scratch-vm parses the project JSON, loads all sprite costumes and sounds, and compiles the block scripts to JavaScript.
The scratch-vm library handles the conversion from Scratch blocks to executable JavaScript in three stages:
project.json into internal data structuresThe VM exposes compiler configuration:
packager.js#1596-1599if (vm.setCompilerOptions) vm.setCompilerOptions({
enabled: ${this.options.compiler.enabled},
warpTimer: ${this.options.compiler.warpTimer}
});
async loadResources () {
const texts = [COPYRIGHT_HEADER];
if (this.project.analysis.usesMusic) {
texts.push(await this.fetchLargeAsset('scaffolding', 'text'));
} else {
texts.push(await this.fetchLargeAsset('scaffolding-min', 'text'));
}
// ...
this.script = texts.join('\n');
}
For HTML output, the entire .sb3 is base85-encoded and embedded in chunks:
for (let i = 0; i < projectData.length; i += CHUNK_SIZE) {
const projectChunk = projectData.subarray(i, i + CHUNK_SIZE);
const base85 = encode(projectChunk);
result.push(`<script data="${base85}">decodeChunk(${projectChunk.length})</script>\n`);
}
The generated HTML includes inline JavaScript that bootstraps the player:
packager.js#1443-1450const scaffolding = new Scaffolding.Scaffolding();
scaffolding.width = ${this.options.stageWidth};
scaffolding.height = ${this.options.stageHeight};
scaffolding.setup();
scaffolding.appendTo(appElement);
const run = async () => {
const projectData = await getProjectData();
await scaffolding.loadProject(projectData);
setProgress(1);
loadingScreen.hidden = true;
if (${this.options.autoplay}) {
scaffolding.start();
}
};
Key Insight: The JavaScript "Conversion"
Scratch blocks are not pre-converted to static JavaScript files. Instead:
This allows the packaged HTML to support all Scratch features including dynamic block creation (clones), runtime introspection (variable monitors), extension loading, and proper event handling (broadcasts, key presses, etc.).