Private betav0.1.0
Docs
Format developer documentation

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 zip
pnpm dlx format add zip
yarn dlx format add zip
bun x format add zip

Usage

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()/@import in <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

EntryRuns inContents
@format.dev/zipNodeEverything, plus filesystem support. zip(html, dir) reads from disk
@format.dev/zip/webNode, Browser, Workers/Edge)zip(html, resolver) and the full API, with no node:* imports
@format.dev/zip/scanNode, Browser, Workers/Edge)scanAssetRefs only. The slim entry compiled static-mode documents inline

Reference

NameDescription
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

Description
Log and skip missing assets instead of rejecting on the first one.
Type
boolean

zlib

Description
Deflate options. Only level (0–9) is read.
Type
{ level: number }

remoteAssets

Description
Fetch remote references when calling 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.
Type
{ 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

CodeWhen
ASSET_MISMATCHRendered HTML references files the bundle can't supply
REMOTE_ASSETS_DISABLEDHTML references remote URLs and remoteAssets isn't enabled
REMOTE_ASSET_FETCHA remote URL is configured but the fetch failed (network, DNS, timeout)
MISSING_ASSETThe resolver returned null for a referenced path
UNRESOLVABLE_REFA local reference that names no file (an empty src/href/url(""), or one pointing at a directory)
INVALID_EXTENSIONA 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.

TriggerFix
A data-driven path resolved to a file nobody bundled, e.g. ./badge-${tier}.pngAdd the file to sharedAssetsDir or the document's assets folder.
The asset set genuinely varies per requestSet 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.

TriggerFix
A remote image, stylesheet, or CSS url()/@import in the documentReference local files instead, the recommended path.
The remote references are intentionalTo 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 ...).

TriggerFix
The build machine has no route to the host, the classic case is a CI runner that blocks outbound trafficAllow network access from the build environment, or remove the remote reference.
DNS failure or timeoutCheck 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.

TriggerFix
The referenced file doesn't exist in the directory or store the resolver reads fromAdd the file, or fix the reference's path.
Missing files are expected and tolerablePass 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).

TriggerFix
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 tolerablePass 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.

TriggerFix
The URL has no extension and the server sends no usable content-typeUse 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 / errorSourceMost common cause
AssetMismatchErrorCompiled render()Data-driven path to an unbundled file
RemoteAssetsDisabledErrorzip() / dynamic getAssetsWebStream()Remote URL without remoteAssets enabled
RemoteAssetFetchErrorRemote fetchingNo network access (CI runners)
MissingAssetErrorzip()Referenced file missing from the asset source
UnresolvableRefErrorzip()Empty reference, or one pointing at a directory
InvalidExtensionErrorRemote fetchingExtension-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.

Was this page helpful?