← Back

How TurboWarp Packager Converts Scratch Projects to HTML

Overview Architecture

The conversion process involves several key components working together:

.sb3 file → scratch-parser (unzip & parse JSON) → scratch-vm (runtime) → scaffolding (player) → packager (HTML output)

1. Parsing .sb3 Files

File Structure

An .sb3 file is simply a ZIP archive containing:

Parser Pipeline

src/scaffolding/scratch-parser/index.js#20-30
module.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:

  1. unpack.js — Detects file format by byte signature (80 75 = ZIP) and extracts using JSZip
  2. parse.js — Parses the JSON string using turbowarp/json (handles control characters)
  3. validate.js — Detects Scratch version (SB2 vs SB3) by checking for targets or objName keys

2. Scaffolding — The Scratch Player

The Scaffolding class (src/scaffolding/scaffolding.js) is a minimal Scratch project player.

Core Dependencies

scratch-libraries.js#3-7
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';

Setup Process

scaffolding.js#313-371
setup () {
    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
}

Project Loading

scaffolding.js#447-461
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.

3. Block Compilation (scratch-vm)

The scratch-vm library handles the conversion from Scratch blocks to executable JavaScript in three stages:

  1. Deserialize — Parses project.json into internal data structures
  2. Compile — TurboWarp's VM includes a JIT compiler that converts block stacks to optimized JavaScript functions
  3. Runtime — Executes the compiled code in a game loop, handling events, broadcasts, clones, etc.

The VM exposes compiler configuration:

packager.js#1596-1599
if (vm.setCompilerOptions) vm.setCompilerOptions({
    enabled: ${this.options.compiler.enabled},
    warpTimer: ${this.options.compiler.warpTimer}
});

4. HTML Generation (Packager)

Resource Loading

packager.js#276-287
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');
}

Project Data Embedding

For HTML output, the entire .sb3 is base85-encoded and embedded in chunks:

packager.js#1056-1061
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`);
}

Runtime Initialization

The generated HTML includes inline JavaScript that bootstraps the player:

packager.js#1443-1450
const scaffolding = new Scaffolding.Scaffolding();
scaffolding.width = ${this.options.stageWidth};
scaffolding.height = ${this.options.stageHeight};
scaffolding.setup();
scaffolding.appendTo(appElement);

Project Loading at Runtime

packager.js#1654-1661
const run = async () => {
    const projectData = await getProjectData();
    await scaffolding.loadProject(projectData);
    setProgress(1);
    loadingScreen.hidden = true;
    if (${this.options.autoplay}) {
        scaffolding.start();
    }
};

5. Data Flow Summary

┌─────────────────────────────────────────────────────────────────────┐ │ BUILD TIME (Packager) │ ├─────────────────────────────────────────────────────────────────────┤ │ .sb3 file │ │ │ │ │ ▼ │ │ JSZip.loadAsync() → Extract project.json + assets │ │ │ │ │ ▼ │ │ Analyze project (extensions, cloud vars, music usage) │ │ │ │ │ ▼ │ │ Base85 encode entire project data │ │ │ │ │ ▼ │ │ Generate HTML with: │ │ - Embedded scaffolding.js (VM + Renderer + Storage + Audio) │ │ - Embedded project data (base85 chunks) │ │ - Runtime initialization code │ └─────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────┐ │ RUN TIME (In Browser) │ ├─────────────────────────────────────────────────────────────────────┤ │ HTML loads in browser │ │ │ │ │ ▼ │ │ Decode base85 → ArrayBuffer │ │ │ │ │ ▼ │ │ JSZip.loadAsync() → Extract project.json from ZIP │ │ │ │ │ ▼ │ │ vm.loadProject(projectData): │ │ 1. Parse project.json │ │ 2. Load assets (images, sounds) from ZIP via storage helper │ │ 3. COMPILE BLOCKS TO JAVASCRIPT (JIT compiler) │ │ │ │ │ ▼ │ │ scaffolding.start() → vm.greenFlag() → Execute compiled JS │ └─────────────────────────────────────────────────────────────────────┘

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.).