# Resolvers

Load file-based layers and templates for rendering

Canonical URL: https://docs.paradoc.dev/sdk/resolvers/

Resolvers provide a unified interface for loading file-based content (templates, assets) when rendering forms. They abstract away the underlying storage mechanism, whether in-memory, filesystem, or remote.

> **Warning:** The `@paradoc/resolvers` package provides filesystem, HTTP, and memory resolvers. Install it separately from the SDK with `npm install @paradoc/resolvers`, and import the entrypoint for your environment.

## Resolver Interface

All resolvers implement this simple interface:

```ts
interface Resolver {
  read(path: string): Promise<Uint8Array>
}
```

A resolver is bound once, when an artifact is constructed — `p.form(schema, { resolver })`, a builder's `.build({ resolver })`, or `p.load(content, { resolver })`. Every instance you derive from it afterward — every `fill`, every mutator, every render call — carries that same resolver, so you never pass it again.

## ArtifactInstanceOptions

The options object every construction entry point accepts:

```ts
interface ArtifactInstanceOptions {
  resolver?: Resolver
}
```

It's accepted by:

* `p.form(input, options?)`, `form.from(input, options?)`, `form.safeFrom(input, options?)`, and a form builder's `.build(options?)` — and the same four entry points for `document` and `checklist`.
* `load`, `loadFromObject`, `safeLoad`, and `safeLoadFromObject`, when reconstructing a form, document, or checklist. (Loading a bundle takes no `resolver`: a bundle has no file-backed layers of its own, only content entries that carry their own.)
* `runtimeFormFromJSON`, `runtimeDocumentFromJSON`, and `runtimeChecklistFromJSON`, when rebuilding a runtime (filled) instance from its JSON.

`resolver` is the only field, and it's optional: pass it only when the artifact has file-backed layers. It's bound once, at construction, and every instance you derive afterward — every `fill`, mutator, and `clone` — carries it. A render call never takes one.

Render a file-backed layer with no resolver bound, and Paradoc throws
`UnboundResolverError`, naming the layer, the path it wanted, and where to
bind one.

## Memory Resolver (Browser-Compatible)

The `createMemoryResolver` function is exported from `@paradoc/resolvers/memory` and works in any environment, including browsers.

```ts
import { p } from '@paradoc/core'
import { createMemoryResolver } from '@paradoc/resolvers/memory'

const resolver = createMemoryResolver({
  contents: {
    '/templates/receipt.html': '<h1>Receipt for {{fields.customer}}</h1>',
    '/templates/contract.docx': docxTemplateBytes,
    '/assets/logo.png': logoBytes,
  }
})

// Bind the resolver when the form is built — the form has file-based layers
const form = p.form(formSchema, { resolver })

const filled = form.fill({
  fields: { customer: 'Acme Corp' }
})

const output = await filled.render()
```

### createMemoryResolver

```ts
function createMemoryResolver(options: MemoryResolverOptions): Resolver
```

#### MemoryResolverOptions

| Name | Type | Description |
| --- | --- | --- |
| `contents` | `Record<string, Uint8Array \| string>` | Map of paths to content. Keys match the read path exactly. Strings are UTF-8 encoded automatically. |

Binary content, including a Node.js `Buffer`, is copied when the resolver is created, and every read returns a new copy. A path with no entry rejects with `ERR_RESOLVER_NOT_FOUND`. Missing `contents`, or a value that is not a string or `Uint8Array`, throws `ERR_RESOLVER_INVALID_OPTIONS`.

## Filesystem Resolver (Node.js Only)

> **Warning:** The `@paradoc/resolvers/fs` entrypoint requires Node.js.

```ts
import { createFsResolver } from '@paradoc/resolvers/fs'

const resolver = createFsResolver({
  root: process.cwd()
})

// Reads files relative to the root directory
const bytes = await resolver.read('/templates/form.docx')
```

### createFsResolver

```ts
function createFsResolver(options: FsResolverOptions): Resolver
```

#### FsResolverOptions

| Name | Type | Description |
| --- | --- | --- |
| `root` | `string` | Absolute or current-working-directory-relative path to the root directory. Relative roots are anchored when the resolver is created. |

> **Note:** The filesystem resolver validates that paths and symlinks stay within the root directory. Policy failures expose `ERR_RESOLVER_OUTSIDE_ROOT` or `ERR_RESOLVER_INVALID_PATH`; missing files retain native Node.js error codes. An empty or non-string `root` throws `ERR_RESOLVER_INVALID_OPTIONS`. The root is checked on each read until it is found, so a root created after a failed read becomes readable.

## HTTP Resolver

The `createHttpResolver` function is exported from `@paradoc/resolvers/http` and works in any environment with the Fetch API. It reads each path beneath a base URL.

```ts
import { createHttpResolver } from '@paradoc/resolvers/http'

const resolver = createHttpResolver({
  baseUrl: 'https://example.com/forms/w-9/'
})

// Fetches https://example.com/forms/w-9/templates/form.pdf
const bytes = await resolver.read('/templates/form.pdf')
```

### createHttpResolver

```ts
function createHttpResolver(options: HttpResolverOptions): Resolver
```

#### HttpResolverOptions

| Name | Type | Description |
| --- | --- | --- |
| `baseUrl` | `string` | Absolute http or https URL of the root directory. Its query and fragment are ignored. |
| `fetch` | `(url: string) => Promise<Response>` | Optional. The fetch used for each read. Defaults to the global fetch. Supply one to add allowed hosts, timeouts, redirect handling, or body size limits. |

> **Note:** Paths follow the filesystem resolver's rules: forward slashes only, and a single leading slash means the base URL, not the origin root. Each path segment is percent-encoded. A path that resolves outside the base URL rejects with `ERR_RESOLVER_OUTSIDE_ROOT`, and one that names the base URL itself with `ERR_RESOLVER_INVALID_PATH`, before any request. Redirects follow the fetch function's own policy. A 404 rejects with `ERR_RESOLVER_NOT_FOUND`, and any other failed status rejects with `ERR_RESOLVER_FETCH_FAILED`. An error thrown by a supplied `fetch` passes through unchanged.

## Usage with File-Based Layers

Resolvers are needed when forms define file-based layers:

```ts
import { p } from '@paradoc/core'
import { createFsResolver } from '@paradoc/resolvers/fs'

const resolver = createFsResolver({ root: process.cwd() })

const form = p.form({
  name: 'w9',
  fields: {
    name: { type: 'text' },
    taxId: { type: 'text' }
  },
  layers: {
    pdf: {
      kind: 'file',                    // File-based layer
      mimeType: 'application/pdf',
      path: '/templates/w9-form.pdf',  // Resolved via resolver
      bindings: { /* ... */ }
    }
  },
  defaultLayer: 'pdf'
}, { resolver })

const filled = form.fill({
  fields: { name: 'John Smith', taxId: '12-3456789' }
})

const output = await filled.render()
```

Render a file layer with no resolver bound and Paradoc throws
`UnboundResolverError`, naming the layer, the path it wanted, and where to
bind one.

## Inline vs File Layers

* **Inline layers** (`kind: 'inline'`) embed content directly in the form definition. No resolver needed.
* **File layers** (`kind: 'file'`) reference external files by path. A resolver must be bound when the artifact is constructed.

```ts
// Inline layer - no resolver needed
layers: {
  html: {
    kind: 'inline',
    mimeType: 'text/html',
    text: '<h1>{{fields.title}}</h1>'
  }
}

// File layer - resolver required
layers: {
  pdf: {
    kind: 'file',
    mimeType: 'application/pdf',
    path: '/templates/form.pdf'
  }
}
```

## Custom Resolvers

Implement the `Resolver` interface for custom storage backends:

```ts
import type { Resolver } from '@paradoc/types'

// Example: S3 resolver
function createS3Resolver(bucket: string, s3Client: S3Client): Resolver {
  return {
    async read(path: string): Promise<Uint8Array> {
      const response = await s3Client.getObject({ Bucket: bucket, Key: path })
      return new Uint8Array(await response.Body.transformToByteArray())
    }
  }
}
```

## Related

* [Renderers](/sdk/renderers) - How renderers use resolvers
* [Form Layers](/sdk/artifacts/form) - Layer configuration
