Zip
Build the assets.zip for a document. API reference for zip(), resolvers, options, and errors.
@format.dev/zip builds the assets bundle a document ships with. It scans rendered HTML for asset references, resolves each one to bytes, and returns a ZIP archive. Studio uses it internally. Install it directly when you render documents without Studio or build zips at render time.
Installation
Use the Format CLI to install the @format.dev/zip package.
npx format add zippnpm dlx format add zipyarn dlx format add zipbun x format add zipUsage
In a Node or server environment
Where your raw asset files are in the directory ./assets:
import { zip } from '@format.dev/zip'
const assets = await zip(html, './assets')In a browser or serverless runtime
import { zip, urlResolver } from '@format.dev/zip/web'
const resolve = urlResolver({
'logo.png': 'https://cdn.example.com/logo.png'
})
const assets = await zip(html, resolve)Calling zip() returns a ZIP archive as a Uint8Array, or undefined when the HTML references no assets. It scans the following elements in your HTML and returns a ZIP that only contains assets that are referenced:
<img src>- Inline SVG
<image>/<use>hrefs <link rel="stylesheet">- CSS
url()/@importin<style>blocks and style attributes
Import chains
A stylesheet can @import other stylesheets, which can import more, and any of them can use fonts or images. zip() follows the whole trail and adds every file it finds, so you only have to link the first stylesheet. Files on disk work as-is. For a stylesheet hosted at a URL, turn on remoteAssets and its imports are fetched too.
Referenced .svg files' contents are not parsed, so a file the SVG references internally through its own <image href> or <use href> must be added to the bundle manually.
The third (optional) argument to zip(), referred to as ZipOptions.
Entry points
| Entry | Runs in | Contents |
|---|---|---|
@format.dev/zip | Node | Everything, plus filesystem support. zip(html, dir) reads from disk |
@format.dev/zip/web | Node, Browser, Workers/Edge) | zip(html, resolver) and the full API, with no node:* imports |
@format.dev/zip/scan | Node, Browser, Workers/Edge) | scanAssetRefs only. The slim entry compiled static-mode documents inline |
Reference
| Name | Description |
|---|---|
| Log and skip missing assets instead of rejecting on the first one. | |
Deflate options. Only level (0–9) is read. | |
Control whether zip() fetches remote asset references in your HTML. |
skipMissing
booleanzlib
level (0–9) is read.{ level: number }remoteAssets
zip(). By default, remote assets are not fetched. Unless enabled, a remote asset path found at zip() will throw RemoteAssetsDisabledError.Generally not recommended on a production render path — fetching per render reintroduces the network as a failure mode and repeats on every render. Prefer local files.
{ enabled: boolean, headers: Headers | Record<string, string> | [string, string][], timeoutMs: number }Resolvers
For more fine-grained control of individual asset locations, you can use a resolver.
There are two in-built resolvers for directories and URLs:
import { dirResolver, urlResolver } from '@format.dev/zip/node'
const local = dirResolver('./assets')
const remote = urlResolver('https://cdn.example.com/assets/')
const resolver: AssetResolver = async (path) => {
return (await local(path)) ?? (await remote(path))
}Or you can write your own custom resolver:
function tenantResolver(tenantId: string): AssetResolver {
return async (path) => {
const row = await db.tenantAssets.findOne({ tenantId, path })
return row ? new Uint8Array(row.bytes) : null
}
}
app.post('/render/:tenantId', async (req, res) => {
const html = await invoice.render(req.body)
const archive = await zip(html, tenantResolver(req.params.tenantId))
// ...
})If you're using Format Studio and have imported assets via module import, these references cannot be mapped via a resolver, as they don't have a stable path. A simple rule is to only module import assets if they will never change at runtime.
Errors
These fire in your own runtime, before the document ever reaches Format, thrown by @format.dev/zip or by a compiled document while building or checking its assets zip. Every one extends FormatZipError, so you catch the umbrella class and switch on code. Mirrors how FormatError works for API errors.
import { FormatClient } from '@format.dev/client'
import { FormatZipError } from '@format.dev/zip'
import { invoice } from './_generated'
try {
const doc = await invoice.render(data)
const format = new FormatClient()
return await format.pdf(doc)
} catch (err) {
if (err instanceof FormatZipError) {
switch (err.code) {
case 'ASSET_MISMATCH':
// the render referenced files the bundle can't supply
break
case 'REMOTE_ASSETS_DISABLED':
// the document references remote URLs — opt in or reference local files
break
case 'MISSING_ASSET':
// the resolver returned null for a referenced path
break
case 'UNRESOLVABLE_REF':
// a reference names no file (empty, or points at a folder)
break
}
}
throw err
}Codes
| Code | When |
|---|---|
ASSET_MISMATCH | Rendered HTML references files the bundle can't supply |
REMOTE_ASSETS_DISABLED | HTML references remote URLs and remoteAssets isn't enabled |
REMOTE_ASSET_FETCH | A remote URL is configured but the fetch failed (network, DNS, timeout) |
MISSING_ASSET | The resolver returned null for a referenced path |
UNRESOLVABLE_REF | A local reference that names no file (an empty src/href/url(""), or one pointing at a directory) |
INVALID_EXTENSION | A fetched remote asset has no usable file extension |
For typed access to per-error properties (err.missing, err.urls, etc.), check the specific subclass instead:
import { AssetMismatchError, RemoteAssetsDisabledError } from '@format.dev/zip'
if (err instanceof AssetMismatchError) {
console.error(`Unbundled: ${err.missing.join(', ')}`)
}
if (err instanceof RemoteAssetsDisabledError) {
console.error(`Remote URLs: ${err.urls.join(', ')}`)
}AssetMismatchError
A compiled document's render produced HTML that references assets its bundle can't supply. Thrown by render() in both asset modes; the error carries documentName, mode, missing, and known.
| Trigger | Fix |
|---|---|
A data-driven path resolved to a file nobody bundled, e.g. ./badge-${tier}.png | Add the file to sharedAssetsDir or the document's assets folder. |
| The asset set genuinely varies per request | Set the --assets flag to dynamic. Read more. |
RemoteAssetsDisabledError
The HTML references remote (http/https) URLs and remoteAssets is not enabled. Thrown by zip() and by a dynamic document's getAssetsWebStream(); the message names every offending URL.
| Trigger | Fix |
|---|---|
A remote image, stylesheet, or CSS url()/@import in the document | Reference local files instead, the recommended path. |
| The remote references are intentional | To opt in: either use --remote-assets at compile, or setZipOptions({ remoteAssets: { enabled: true } }) on a dynamic renderer, or the remoteAssets option to zip(). |
RemoteAssetFetchError
remoteAssets is enabled but the network itself failed while fetching a remote asset. The error carries the url. A reachable server answering with an error status throws a plain Error (HTTP 404 for ...).
| Trigger | Fix |
|---|---|
| The build machine has no route to the host, the classic case is a CI runner that blocks outbound traffic | Allow network access from the build environment, or remove the remote reference. |
| DNS failure or timeout | Check the URL's host; raise remoteAssets.timeoutMs for slow sources. |
MissingAssetError
zip() collected a reference the resolver couldn't supply, the resolver returned null and skipMissing wasn't set. The error carries relPath.
| Trigger | Fix |
|---|---|
| The referenced file doesn't exist in the directory or store the resolver reads from | Add the file, or fix the reference's path. |
| Missing files are expected and tolerable | Pass skipMissing: true, missing assets are logged and skipped instead. |
UnresolvableRefError
zip() collected a local reference that names no file, and skipMissing wasn't set. The error carries ref (the offending reference).
| Trigger | Fix |
|---|---|
An empty src, href, or url(""), or a reference that points at a directory such as src="/" or url(.) | Fill in or correct the reference, or remove the element. |
| Broken references are expected and tolerable | Pass skipMissing: true, broken references are logged and skipped instead. |
A same-document fragment such as fill: url(#gradient) is not a trigger. It points at an element already in the document, not a file, so zip() skips it.
InvalidExtensionError
A fetched remote asset can't be given a zip entry name: neither the URL nor the response's MIME type yields a file extension.
| Trigger | Fix |
|---|---|
The URL has no extension and the server sends no usable content-type | Use a URL with a file extension, or fix the server's content-type header. |
Quick lookup table
A summary of every code and error with where it comes from. Codes are raised by Format at parse time; the *Error classes fire in your own runtime while bundling assets:
| Code / error | Source | Most common cause |
|---|---|---|
AssetMismatchError | Compiled render() | Data-driven path to an unbundled file |
RemoteAssetsDisabledError | zip() / dynamic getAssetsWebStream() | Remote URL without remoteAssets enabled |
RemoteAssetFetchError | Remote fetching | No network access (CI runners) |
MissingAssetError | zip() | Referenced file missing from the asset source |
UnresolvableRefError | zip() | Empty reference, or one pointing at a directory |
InvalidExtensionError | Remote fetching | Extension-less URL with no usable MIME type |
zip() throws RemoteAssetsDisabledError when the HTML references remote URLs without remoteAssets enabled, MissingAssetError when the resolver can't supply a referenced path, UnresolvableRefError when a reference names no file at all, and RemoteAssetFetchError when the network itself fails during a remote fetch. Each is documented with triggers and fixes under Errors.
Related
- Assets, what goes in a bundle and how references resolve
- Assets in Format Studio, how to use and bundle assets
- Advanced assets, real-world advanced examples