packager/node-api-docs
GarboMuffin 2f0010294f
Fix packaging very large projects as HTML files in Chrome (#862)
Actually fixes https://github.com/TurboWarp/packager/issues/528

https://github.com/TurboWarp/packager/pull/861 fixed running large projects
in Chrome, but packaging still used string concatenation so it remained broken.
Now the concatenation part is done using TextEncoder & Uint8Arrays and a
template tag function to keep it readable.

This time I have actually tested it with a 1.0GB sb3.

Breaking Node API change: The data property returned by Packager#package() is
now always a Uint8Array instead of sometimes string and sometimes ArrayBuffer.
2024-05-23 23:47:40 -05:00
..
demo-simple.js
demo.js
README.md Fix packaging very large projects as HTML files in Chrome (#862) 2024-05-23 23:47:40 -05:00
test-icon.png

Node.js API

Installing

npm install --save-exact @turbowarp/packager

We suggest that you use --save-exact (or, with yarn, --exact) to make sure you always install the same version. This is important because we don't promise API compatibility across even minor updates.

About the API

Stability

The Node.js API is still in beta.

There are no promises of API stability between updates even across minor updates. Always pin to an exact version and don't update without testing. We don't go out of our way to break the API, but we don't let it stop us from making changes. We try to mention noteworthy changes in the GitHub releases changelog.

Release cadence

We intend to release an updated version of the npm module to npm with every update of TurboWarp Desktop, which currently happens about once a month.

Feature support

All features should work, with the following exceptions:

  • macOS apps in the NW.js or WKWebView environments do not support custom icons and must always use the default icon

Browser support

The Node.js module as published on npm is not intended to work in a browser regardless of any build tool such as webpack. If you need to run in a browser, fork this repository directly and modify the interface as you see fit.

Large assets

Large assets such as Electron binaries are not stored in this repository and will be downloaded from a remote server on demand. While we aren't actively removing old files, we can't promise they will exist forever. Downloads are validated with a SHA-256 checksum and cached locally.

Large assets are cached in node_modules/@turbowarp/packager/.packager-cache. You may want to periodically clean this folder.

Using the API

See demo.js or demo-simple.js for a full example.

First, you can import the module like this:

const Packager = require('@turbowarp/packager');

Load a project

Next you have to get a project file from somewhere. It can be a project.json or a full sb, sb2, or sb3 file. The packager doesn't provide any API for this, you have to find it on your own. Your data must be an ArrayBuffer, Uint8Array, or Node.js Buffer.

// Fetch a remote URL:
const fetch = require('cross-fetch').default; // or whatever your favorite HTTP library is
const projectData = await (await fetch('https://packager.turbowarp.org/example.sb3')).arrayBuffer();

// or use a local file:
const fs = require('fs');
const projectData = fs.readFileSync('project.sb3');

// or fetch a shared Scratch project:
const fetch = require('cross-fetch').default; // or whatever your favorite HTTP library is
const id = '437419376';
const projectMetadata = await (await fetch(`https://trampoline.turbowarp.org/api/projects/${id}`)).json();
const token = projectMetadata.project_token;
const projectData = await (await fetch(`https://projects.scratch.mit.edu/${id}?token=${token}`)).arrayBuffer();

Now you have to tell the packager to load the project. The packager will parse it, do some analysis, and download any needed assets if the input was just a project.json. This must be done once for every project. The result of this processes can be reused as many times as you want.

You may specify a "progress callback" that the loader may call periodically with progress updates. type will be a string like assets or compress. Depending on the type, a might be "loaded" and b might be "total", or a might be a percent [0-1] in which case b is unused.

const progressCallback = (type, a, b) => {};
const loadedProject = await Packager.loadProject(projectData, progressCallback);

Package the project

Now you can make a Packager.

const packager = new Packager.Packager();
packager.project = loadedProject;

packager.options has a lot of options on it for you to consider. You can log the object or see packager.js and look for DEFAULT_OPTIONS to see what options are available.

We recommend that you avoid overwriting the entirety of packager.options as this will cause issues when the structure of the options object changes in future updates. Instead, just update the properties you want to change from the defaults.

// GOOD:
packager.options.turbo = true;
packager.options.custom.js = "/* */";

// BAD (DO NOT DO THIS):
packager.options = {
  turbo: true,
  custom: {
    js: "/* */"
  },
  // ...
};

Even if you add ...packager.options the second example is still broken: options.custom also has a css property which is accidentally being set to undefined which is undefined behavior. Instead of remembering to do ...packager.options.xyz everywhere, it's best to just avoid completely redefining options whenever possible.

Some options expect an image as an argument. In the Node.js module, there is a special class to use for these, new Packager.Image(mimeType, buffer):

packager.options.app.icon = new Packager.Image('image/png', fs.readFileSync('icon.png'));

Note that a Packager is a single-use object; you must make a new Packager each time you want to package a project.

Now you can finally actually package the project.

const result = await packager.package();

// Suggested file name including file extension based on packager's options.
// This is not sanitized so it could contain things like path traversal exploits. Be careful.
const filename = result.filename;

// MIME type of the packaged project. Either "text/html" or "application/zip"
const type = result.type;

// The packaged project's data. Will always be a Uint8Array.
const data = result.data;

You can also add progress listeners on the packager using something similar to the addEventListener you're familiar with. Note that these aren't actually EventTargets, just a tiny shim that looks similar, so some things like once won't work and the events don't have very many properties on them.

// do this before calling package()
packager.addEventListener('zip-progress', ({detail}) => {
  // Used when compressing projects as zips
  console.log('Packager progress zip-progress', detail);
});
packager.addEventListener('large-asset-fetch', ({detail}) => {
  // Used when fetching large assets such as Electron binaries
  console.log('Packager progress large-asset-fetch', detail);
});

What you do with data is now entirely up to you.

Be mindful of the copyright on the projects you package.