@molecule/api-deploy-target
Core interface · deploy-target · API (Node) · v1.1.0 · Apache-2.0
Deploy target interface for molecule.dev — publish a built app to static storage, a machine, or any host behind one contract
npm install @molecule/api-deploy-targetHow it works
@molecule/api-deploy-target is the deploy-target core interface on the API (Node) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 1 provider: @molecule/api-deploy-target-s3.
import { requireProvider, setProvider } from '@molecule/api-deploy-target'
import { createS3DeployTarget } from '@molecule/api-deploy-target-s3'
// At startup:
setProvider(
'static',
createS3DeployTarget({
bucket: 'my-sites',
endpoint: 'https://fly.storage.tigris.dev',
publicBaseUrl: 'https://my-sites.fly.storage.tigris.dev',
}),
)
// Per deploy — the caller knows what the app is:
const target = requireProvider(app.hasServer ? 'machine' : 'static')
const release = await target.deploy({
siteId: app.id,
releaseId: Date.now().toString(36),
files, // [{ path: '/index.html', body }, …] from the build output
routing: { basePath: '/blog', unmatchedPaths: 'not-found' },
})
// Route the app's public host to release.origin, then reclaim old releases:
await target.remove(app.id, { keepReleaseIds: [release.releaseId] })Providers (1): @molecule/api-deploy-target-s3
Works with: @molecule/api-bond
Reference
Deploy target core interface for molecule.dev.
A deploy target is where a BUILT app is published so the public can reach
it. This package defines the one contract every target implements —
deploy(request) → release and remove(siteId) — so where an app is hosted
is a choice of bond, not a code change: a prerendered blog goes to static
object storage, an app with an API goes to a machine that runs its server,
and switching one app between them is a different requireProvider(name).
Interface only. Bond targets by NAME at startup (setProvider('static', …),
setProvider('machine', …)) and pick one per deploy.
Bonds:
@molecule/api-deploy-target-s3— static files on any S3-compatible bucket (Tigris, Cloudflare R2, AWS S3, MinIO), one immutable prefix per release plus a manifest an edge serves from.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { requireProvider, setProvider } from '@molecule/api-deploy-target'
import { createS3DeployTarget } from '@molecule/api-deploy-target-s3'
// At startup:
setProvider(
'static',
createS3DeployTarget({
bucket: 'my-sites',
endpoint: 'https://fly.storage.tigris.dev',
publicBaseUrl: 'https://my-sites.fly.storage.tigris.dev',
}),
)
// Per deploy — the caller knows what the app is:
const target = requireProvider(app.hasServer ? 'machine' : 'static')
const release = await target.deploy({
siteId: app.id,
releaseId: Date.now().toString(36),
files, // [{ path: '/index.html', body }, …] from the build output
routing: { basePath: '/blog', unmatchedPaths: 'not-found' },
})
// Route the app's public host to release.origin, then reclaim old releases:
await target.remove(app.id, { keepReleaseIds: [release.releaseId] })
Type
core
Installation
npm install @molecule/api-deploy-target @molecule/api-bond
API
Interfaces
DeployTargetFile
One file of the build output.
path is SITE-RELATIVE with a leading slash (/index.html,
/assets/app-3f2a.js, /blog/post/index.html) — the path a visitor would
request if the site were mounted at the root. Never .., never a backslash,
never empty segments; a target must refuse such a path rather than normalize
it, because a normalized path is a file the caller did not ask for.
interface DeployTargetFile {
/** Site-relative path with a leading slash, e.g. `/index.html`. */
path: string
/** The file's bytes, exactly as they should be served. */
body: Uint8Array
/**
* The `Content-Type` to serve it with. Optional: a target derives one from
* the extension when absent (`application/octet-stream` for an unknown one).
*/
contentType?: string
}
DeployRouting
How the published site answers requests.
interface DeployRouting {
/**
* The sub-path the app was BUILT for (`/blog`), with a leading slash and no
* trailing one, or `null` for the site root. A build made with a base emits
* every URL under it and writes its files relative to the output directory,
* so the target serves the files AT the base: `/blog/` is `/index.html`,
* `/blog/assets/x.js` is `/assets/x.js`, and the bare origin redirects to
* `/blog/`. A path outside the base is not the site's.
*/
basePath: string | null
/** What an unmatched path gets. */
unmatchedPaths: DeployUnmatchedPaths
}
DeployTargetRequest
One deploy, as handed to a target.
interface DeployTargetRequest {
/**
* The stable identity of the site across deploys — one per app (a project
* id). Every release of the site lives under it, and
* `DeployTargetProvider.remove` removes everything under it.
* Must match `[A-Za-z0-9_-]{1,128}`.
*/
siteId: string
/**
* This deploy's identity, unique within the site (a timestamp tag, a
* deployment id). A release is immutable once published: a new deploy is a
* new release, never an overwrite, so the previous one keeps serving until
* the caller switches over. Must match `[A-Za-z0-9_-]{1,128}`.
*/
releaseId: string
/**
* The build output. Required by `static-files` targets, which publish
* exactly these files. An `app-server` target implemented beside the build
* (it can reach the build environment directly) may ignore it and move the
* app its own way; one that cannot must refuse a request without it.
*/
files?: readonly DeployTargetFile[]
/** How the published site answers requests. */
routing: DeployRouting
/**
* Runtime environment for targets that run a server. Static targets have no
* runtime and ignore it — a value a static bundle needs has to be present at
* BUILD time, because that is when the bundler inlines it.
*/
env?: Readonly<Record<string, string>>
/** Progress lines for the person watching the deploy. Never throws into the target. */
log?: (line: string) => void | Promise<void>
/**
* Phase transitions, for a caller that mirrors them onto its own record:
* `building` (a target that builds the app itself is about to) and
* `publishing` (the release is being written or launched).
*/
onPhase?: (phase: DeployTargetPhase) => void | Promise<void>
/**
* Cooperative cancellation, consulted between steps. When it answers true
* the target stops, removes what it created for THIS release (never an
* earlier one), and rejects with an error whose `code` is `'cancelled'`.
*/
isCancelled?: () => boolean | Promise<boolean>
}
DeployTargetRelease
A published release, as a target reports it.
interface DeployTargetRelease {
/** The provider's `name`. */
target: string
/** The site this release belongs to. */
siteId: string
/** This release's identity. */
releaseId: string
/** Where it is served from. */
origin: DeployOrigin
/**
* The public URL, when the TARGET mints one (a third-party host's
* `*.example.app` address). `null` when the caller owns the public address
* and routes it to `origin` itself.
*/
url: string | null
/** The routing the release was published with. */
routing: DeployRouting
/** Number of files published (0 for a target that does not publish files). */
fileCount: number
/** Total bytes published. */
bytes: number
}
StaticSiteManifestEntry
One file entry of a StaticSiteManifest.
interface StaticSiteManifestEntry {
/** The `Content-Type` to serve the file with. */
contentType: string
/** Size in bytes. */
size: number
/** A strong validator for the bytes (a hex digest), usable as an `ETag`. */
etag: string
}
StaticSiteManifest
The description of one immutable static release — written by a
static-files target next to the files, read by whatever serves them.
It is the whole routing contract: the file set (so a path can be answered
with no probe of the store — a directory index, a fallback and a 404 are all
decided from here) and the routing the release was built for. A reader must
treat an unknown format as unreadable rather than guess.
interface StaticSiteManifest {
/** Format marker; bumped on any incompatible change. */
format: 'molecule-static-site/1'
/** The site this release belongs to. */
siteId: string
/** This release's identity. */
releaseId: string
/** ISO-8601 time the release was published. */
publishedAt: string
/** How the release answers requests. */
routing: DeployRouting
/** Every published file, keyed by site-relative path (`/index.html`). */
files: Record<string, StaticSiteManifestEntry>
}
DeployTargetRemoveOptions
Options for DeployTargetProvider.remove.
interface DeployTargetRemoveOptions {
/**
* Releases to KEEP. Absent or empty removes the whole site. Used after a
* switch-over to reclaim the releases nothing serves any more.
*/
keepReleaseIds?: readonly string[]
}
DeployTargetProvider
A place built apps are published to.
Bonded by NAME under the deploy-target category ('static', 'machine',
'netlify', …) so an application can wire several and pick one per app.
interface DeployTargetProvider {
/** Stable provider name, recorded on every release it publishes. */
readonly name: string
/** What kind of app it can host. */
readonly hosts: DeployTargetHosts
/**
* Publish one release. Resolves once the release is COMPLETE at its origin
* (every file and the manifest written, or the server answering), and not
* before — a caller switches traffic to it on the strength of this promise.
* Publishing never touches another release of the same site.
*
* @param request - The deploy.
* @returns The published release.
* @throws {Error} When the release could not be published completely; what
* this call created is removed first. A cancelled deploy rejects with an
* error whose `code` is `'cancelled'`.
*/
deploy(request: DeployTargetRequest): Promise<DeployTargetRelease>
/**
* Remove a site's releases — all of them, or all but `keepReleaseIds`.
* Idempotent: removing a site that has nothing published resolves.
*
* @param siteId - The site.
* @param options - Releases to keep.
* @throws {Error} When the target could not remove them (a failed listing is
* an error, never "nothing to remove").
*/
remove(siteId: string, options?: DeployTargetRemoveOptions): Promise<void>
}
Types
DeployTargetHosts
What kind of app a target can host.
static-files— serves the build output as files and nothing else. No server process, no database, no websockets. A prerendered site or a client-routed single-page app fits; anything with an API does not.app-server— runs the app's own server process (and whatever it needs beside it), so it can host anything a static target can plus an API.
type DeployTargetHosts = 'static-files' | 'app-server'
DeployUnmatchedPaths
What a request for a path the build did not produce gets.
serve-index— the build's/index.html, status 200. For a client-routed single-page app, whose router owns every path.not-found— status 404, with the build's own/404.htmlwhen it emitted one. For a prerendered site, which emits a file per route, so an unmatched path is simply missing.
Decided by what the BUILD is — the caller knows, so it says so. It never
depends on who is asking (an Accept: text/html browser and a feed reader
get the same answer for the same path).
type DeployUnmatchedPaths = 'serve-index' | 'not-found'
DeployTargetPhase
A step of a deploy, reported through DeployTargetRequest.onPhase.
type DeployTargetPhase = 'building' | 'publishing'
DeployOrigin
Where a published release is served FROM — what an edge in front of the target needs to route a visitor's request to it.
http-upstream— a server answering HTTP aturlfor every path; the edge forwards requests there unchanged.static-files— immutable files underfilesBaseUrl(a public URL prefix; the file/index.htmlis${filesBaseUrl}index.html) described by theStaticSiteManifestatmanifestUrl. Object stores do not do directory indexes, base paths or fallbacks, so the edge answers those from the manifest and fetches only files it names.
type DeployOrigin =
| {
/** A server answering every path. */
kind: 'http-upstream'
/** Its routable URL, scheme included. */
url: string
}
| {
/** Immutable files described by a manifest. */
kind: 'static-files'
/** Public URL prefix the files live under, ending with `/`. */
filesBaseUrl: string
/** Public URL of the release's `StaticSiteManifest`. */
manifestUrl: string
}
Functions
setProvider(name, provider)
Register a deploy target under a name.
function setProvider(name: string, provider: DeployTargetProvider): void
name— The slot to bond it in ('static','machine', …).provider— The target.
getProvider(name)
The target bonded under name, or null.
function getProvider(name: string): DeployTargetProvider | null
name— The slot.
Returns: The target, or null when nothing is bonded there.
hasProvider(name)
Whether a target is bonded under name.
function hasProvider(name: string): boolean
name— The slot.
Returns: true when one is.
requireProvider(name)
The target bonded under name.
function requireProvider(name: string): DeployTargetProvider
name— The slot.
Returns: The target.
listProviders()
Every bonded target, by slot name.
function listProviders(): Map<string, DeployTargetProvider>
Returns: A map of slot name → target (empty when none are bonded).
Constants
DEPLOY_TARGET_BOND_TYPE
The bond category deploy targets are registered under.
const DEPLOY_TARGET_BOND_TYPE: 'deploy-target'
Available Providers
| Provider | Package |
|---|---|
| S3-compatible static sites | @molecule/api-deploy-target-s3 |
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1
Runtime Dependencies
-
@molecule/api-bond -
Choose the target from what the app IS, and let the owner override it. The facts are known before the deploy starts — whether the app has a server, whether it needs a database — so pass them in; never probe a deployed site to find out. A
static-filestarget can host only an app with no server at all: a prerendered site (unmatchedPaths: 'not-found') or a client-routed SPA (unmatchedPaths: 'serve-index'). Anything with an API needs anapp-servertarget. -
Releases are immutable. Every deploy publishes a NEW
releaseId; the previous release keeps serving until the caller switches its routing to the new one. Only then reclaim the old ones withremove(siteId, { keepReleaseIds: [current] })— and not in the same instant if an edge caches the old origin for a few seconds. -
basePathis part of the build, not of the host. A site built for/blogemits/blog/assets/…URLs; pass the SAME base the build used (read it from the build's own environment), or every asset 404s. Files are still given site-relative (/index.html, not/blog/index.html). -
A
static-filesorigin is not a web server. Object stores do not do directory indexes, base paths or fallbacks. Whatever serves the files reads the release'sStaticSiteManifest(fetched once per release — it never changes) and answers/blog/→/index.html,/about/→/about/index.html, an unmatched path →/index.htmlor/404.html, fetching only files the manifest names. -
Values a static bundle needs must exist at BUILD time.
envon the request reaches only server targets; a bundler inlinesVITE_*/public values when it builds, and nothing can change them afterwards. -
remove()on a failed listing THROWS — "could not look" must never read as "nothing to remove" to a caller that is tearing a project down.