Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ yarn test:unit

This will also generate a coverage report in the `coverage` directory.

### Viewer without a framework

`yarn test:img:fixture --core-only` packs Viewer and Utils, installs them with npm in an isolated
consumer, and exercises the public model, synchronous server signing and authorized Web handlers.
It verifies that React, React DOM and Next.js are absent. npm registry access is required, but no
service credentials or media requests. The ordinary `test:img:fixture` CI job runs this check before its Next.js/browser tests;
`--core-only` skips those heavier tests for focused work.

### SDK Edge packaging regression

`yarn test:sdk:edge` packs both SDK names, installs each in a fresh consumer without omitting
Expand Down
19 changes: 14 additions & 5 deletions packages/img/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ Native responsive images from Smart CDN (`<workspace>.tlcdn.com`); bytes bypass

- **Build-time Next images:** use the catalog Quickstart below.
- **Live uploads, gallery previews and private video:** [React receipts](docs/react-storage.md) (video requires 0.0.8+).
- **Plain JavaScript or another framework:** [framework-neutral core](docs/reference.md#framework-neutral-core).
- **HTTP/S3 assets:** use a [restricted Template](#existing-http-or-s3-assets).

## Quickstart

Next.js 16.3.3+ App Router, React 19, Node.js runtime.
`npm install @transloadit/viewer@alpha && npm install --save-dev @transloadit/node`.
pnpm: `pnpm add @transloadit/viewer@alpha && pnpm add -D @transloadit/node`;
Yarn: `yarn add @transloadit/viewer@alpha && yarn add -D @transloadit/node`.
Install the tested versions in your existing app:

```bash
npm install --save-exact @transloadit/viewer@0.1.0
npm install --save-dev --save-exact @transloadit/node@6.9.0
```

Keep your project's package manager and lockfile: Yarn uses `yarn add --exact` and
`yarn add --dev --exact`; pnpm uses `pnpm add --save-exact` and `pnpm add --save-dev --save-exact`.
For a newer alpha, explicitly select `@transloadit/viewer@alpha` with `--save-exact` and test it
before updating. A bare Viewer install currently selects the older `latest` release.
Storage needs a matching API2 deployment; [existing HTTP/S3 assets](#existing-http-or-s3-assets) do not.

Run beside `package.json`. Start with `auth login` even without an account: choose Sign up in the
Expand All @@ -22,8 +31,8 @@ browser it opens, create a free workspace, and approve the CLI. For `./hero.jpg`
Prefer `--hashed` for images you will replace; use the printed JSX path with its immutable filename.

```bash
npx transloadit auth login
npx transloadit storage store ./hero.jpg website/hero.jpg --public --hashed
npx @transloadit/node@6.9.0 auth login
npx @transloadit/node@6.9.0 storage store ./hero.jpg website/hero.jpg --public --hashed
```

`--public` publishes the directory recursively, including future uploads. In `next.config.ts`:
Expand Down
2 changes: 1 addition & 1 deletion packages/img/docs/react-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ yarn workspace @transloadit/viewer pack --out /tmp/transloadit-viewer-preview.tg
In your React 19 application, install that tarball (packing runs the package's build):

```bash
yarn add @transloadit/viewer@file:/tmp/transloadit-viewer-preview.tgz
npm install --save-exact /tmp/transloadit-viewer-preview.tgz
node --input-type=module -e "import('@transloadit/viewer/react/preview').then(m => console.log(Object.keys(m)))"
```

Expand Down
142 changes: 113 additions & 29 deletions packages/img/docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,87 @@

Start with the [Quickstart](../README.md). This reference covers policy, advanced layouts and operations.

## Framework-neutral core

The package has separate entry points, not a single all-purpose gallery component:

| Entry | Use |
| --- | --- |
| `@transloadit/viewer` | Serializable responsive image model; no React or Next.js dependency |
| `@transloadit/viewer/server` | Server-only Web `Request`/`Response` handlers with explicit authorization |
| `@transloadit/viewer/react` | React image components for canonical receipts; no Next.js dependency |
| `@transloadit/viewer/next` | Next.js rendering and catalog integration |

React and Next.js are optional peers. The config plugin and automatic catalog bindings are
Next.js-specific; another framework supplies its own rendering and application data. Viewer does
not own album membership, routing or gallery order.

For a public image in plain JavaScript, install the packages you import directly:

```bash
npm install --save-exact @transloadit/viewer@0.1.0 @transloadit/utils@4.11.0
```

Use a canonical image receipt returned by your upload or read from your application's trusted
metadata. Its `workspace`, `asset_id`, `version_id`, `path`, `width` and `height` identify one
retained image. The example assumes `receipt` is that record, not a path string:

```ts
import { getSmartCdnUrl } from '@transloadit/utils'
import {
createTransloaditImageModel,
transloaditPublicStoragePreviewTemplate,
} from '@transloadit/viewer'

const model = createTransloaditImageModel(
{
src: receipt,
expiresAt: undefined,
template: transloaditPublicStoragePreviewTemplate,
widths: [320, 640, 960],
},
({ input, template, urlParams }) =>
getSmartCdnUrl({ workspace: receipt.workspace, input, template, urlParams }),
)
```

This only builds URLs. It neither uploads nor publishes anything: the server must already declare
the image's directory public. Do not use it as a fallback when private delivery is denied.
Public URLs require no application signing key. A browser can render the model with native elements:

```ts
const picture = document.createElement('picture')
for (const source of model.sources) {
const element = document.createElement('source')
element.type = `image/${source.format}`
element.srcset = source.candidates.map(({ url, width }) => `${url} ${width}w`).join(', ')
element.sizes = '(max-width: 960px) 100vw, 960px'
picture.append(element)
}
const image = document.createElement('img')
image.src = model.fallbackUrl
image.alt = 'A canal house beside the water'
image.width = Math.min(receipt.width, 960)
image.height = Math.round(image.width * receipt.height / receipt.width)
image.loading = 'lazy'
image.style.maxWidth = '100%'
image.style.height = 'auto'
picture.append(image)
document.body.append(picture)
```

Replace the alt text and layout hints for your page. With a bundler, these browser imports need
neither React nor Next.js. A server renderer can consume the same model using its own escaped HTML
templates instead of DOM methods.

The model's URL resolver is synchronous. Private Node rendering can use the synchronous signer in
`@transloadit/utils/node`, exclusively on the server after authorization. Do not pass the async
root signer as this callback or put signing secrets in browser code. For per-request access checks,
`createStorageRoute` from `/server` returns standard `GET` and `HEAD` handlers; the
[authorized delivery recipe](./react-storage.md) describes the receipt and permission contract.
These handlers do not provide a complete non-React gallery or a client route-URL builder. The
Next.js adapter's runtime restrictions do not apply to the pure model.

## Next.js plugin and catalog convention

`Image` from `@transloadit/viewer/next` is an App Router Server Component. Wrap the existing
Expand Down Expand Up @@ -785,20 +866,21 @@ same-page sign-in/refresh needs an explicit retry identity. See [When it breaks]

### Receipt integrity and recovery

Older deployments may watermark Community-plan uploads. The CLI reports changed bytes and saves
metadata for the actual stored image; it does not suggest overwriting that completed upload.

The store command wraps `client.storeImage()`, waits for completion and validates `asset_id`,
the exact destination, stored byte count/MD5 and positive EXIF-oriented display dimensions.
The receipt lands in `results[':original']`, not `results.stored`. Older deployments can apply
Community-plan watermarks before Storage runs; newer API2 deployments preserve stored originals.
The receipt describes the stored bytes, not an assumption that they equal the local file.
The CLI warns about a changed size or checksum and saves that authoritative receipt normally.
`--log-level debug` adds the Assembly ID, bounded receipt summary and input comparison, never
raw Assembly responses or credentials. SDK callers can observe `(receipt, input, assemblyId)`
through the optional `onReceipt` callback. Observers are not awaited; synchronous exceptions and
asynchronous rejections do not discard completed writes.
Rendering requires no metadata lookup.
With Node SDK/CLI 6.9.0 or newer, `storage store` uses `client.storeAsset()` to store originals of
any media type, not only images. It verifies the returned size and MD5 against the local file.
If the stored bytes differ, the CLI checkpoints the actual completed receipt and stops with an
error. This is a completed write, not an invitation to retry or overwrite it. The Node SDK's
[mixed-media originals contract](https://github.com/transloadit/node-sdk/blob/main/packages/node/README.md#mixed-media-originals) owns byte verification,
replay, recovery and overwrite behavior.

The catalog keeps dimensioned `image/*` receipts in `images` and other media, unknown MIME types
and images without usable dimensions in `files`. Only `images` are Viewer sources; do not invent
dimensions or infer an image MIME type from dimensions alone. Rendering uses saved metadata,
without a runtime metadata lookup.

`storeImage()` remains available to SDK callers that deliberately accept transformed image bytes.
It is not the CLI's original-preserving upload contract. If a workspace applies a watermark,
renaming or overwriting the destination does not bypass that policy.

`storage store ./images/*.jpg website/` accepts shell-expanded files and a directory destination.
Each successful upload is checkpointed before the next; a later failure preserves earlier receipts.
Expand All @@ -807,8 +889,8 @@ by content (identical bytes reuse the first receipt, even during an explicit wor
The printed snippet uses a
filename-derived alt; replace it with an accurate description, or an empty alt for a decorative image.

The CLI atomically appends to the catalog's `images` object keyed by Storage path, preserving earlier receipts
on failure. Parent directories must exist. A sibling lock prevents concurrent writers from losing
The CLI atomically updates the catalog's `images` and `files` objects keyed by Storage path,
preserving earlier receipts on failure. Parent directories must exist. A sibling lock prevents concurrent writers from losing
each other's records. Ctrl-C cancels active uploads and catalog reads, releases the lock, and checkpoints
any receipt that already returned before stopping. An accepted Assembly may still finish remotely:
check Storage or sync receipts before retrying a write. A forced exit or crash can leave a lock;
Expand All @@ -819,11 +901,12 @@ Receipt validation occurs after the Storage write, not as a rollback. Do not re-
`--overwrite` to fix missing metadata. Existing paths conflict by default.
`storage store --overwrite` explicitly replaces an occupied path; it is never the default. Prefer
[hashed filenames](#cache-and-markup-cost) for repository organization; delivery pins the actual
asset version, and cached bytes can outlive deletion or revocation. On older deployments that transform uploaded
bytes, the CLI still saves the authoritative receipt; a differing checksum makes a hashed replay
fail without uploading or replacing anything. Restoring that same transformed receipt cannot fix
the mismatch. Choose a fresh destination basename; hashed replay requires an origin that preserves
the uploaded bytes. The same refusal protects against a short-hash collision.
asset version, and cached bytes can outlive deletion or revocation. A differing checksum makes a
hashed replay fail without uploading or replacing anything. Restoring that same transformed receipt
cannot fix the mismatch. After ensuring that the upload policy preserves originals, choose a fresh
destination basename for the new upload; changing the policy alone does not fix the saved receipt,
and `--hashed` cannot be combined with `--overwrite`. The same refusal protects against a short-hash
collision.

### Recovery

Expand Down Expand Up @@ -859,7 +942,7 @@ for private delivery. The CLI and factory explain this missing delivery choice.
The server returns the same canonical shape as storing: `workspace`, `asset_id`, `version_id`,
final `path`, `size`, `mime`, available `md5hash`/`sha256`, version-specific `width`/`height`, and
optional server `thumbhash`/`has_alpha` metadata.
Sync records the verified `apiOrigin` on the catalog and every recovered image and refuses to mix environments.
Sync records the verified `apiOrigin` on the catalog and every recovered asset and refuses to mix environments.
Unbound legacy catalogs must be recovered into a new `--receipts` file and reviewed before replacing
the old catalog. A custom delivery host is preserved separately, never treated as API provenance.
Sync recovers real version identities, not a path-only approximation. Local `source`,
Expand All @@ -869,11 +952,13 @@ alias is then removed. A fresh sync recovers these fields when the server versio
without downloading original bytes or generating new hashes. Otherwise they remain absent.
Changed versions drop stale local evidence even if their MD5 matches.

Sync adds or refreshes matching paths and never prunes unmatched entries. Any missing/invalid
dimensions, failed page or incomplete listing leaves the existing file intact; a failed atomic
replacement retains the complete temporary catalog for recovery. Choose an image-only prefix;
older objects without dimensions need a catalog backfill. Storage records EXIF-oriented display
dimensions for new image uploads, so sync matches `storeImage` receipts for rotated photos too.
Sync adds or refreshes matching paths and never prunes unmatched entries. Node SDK/CLI 6.9.0 or
newer supports mixed-media prefixes and classifies recovered receipts into `images` or `files` by
their current metadata. Missing image dimensions do not prevent recovery: that asset stays in
`files`, unavailable to Viewer until usable image metadata exists. Failed pages or incomplete
listings leave the existing catalog intact; a failed atomic replacement retains the complete
temporary catalog for recovery. Storage records EXIF-oriented display dimensions for new image
uploads, so Viewer can use recovered receipts for rotated photos too.
Commit `transloadit.images.json` before building so rendering needs no runtime metadata lookup; it can now
be regenerated from Storage rather than being the only copy of rendering metadata.

Expand Down Expand Up @@ -1007,8 +1092,7 @@ the workspace and private signing pair on first use; it loads no files and uses
unless the complete Smart CDN override pair is supplied. The factory accepts a trusted compatible `template`,
`baseUrl` and transport `urlParams`. Never derive these signing policies from request input.

`@transloadit/viewer` exposes `createTransloaditImageModel` and serializable model types for other
framework adapters. `@transloadit/viewer/next` renders a resolved model without owning credentials.
For rendering outside React/Next.js, see the [framework-neutral core](#framework-neutral-core).

“Native props” means serializable image attributes such as `alt`, `className`, `aria-*`, `data-*`,
`decoding` and `referrerPolicy`. Event callbacks and refs do not cross this Server Component
Expand Down
5 changes: 4 additions & 1 deletion packages/img/test/packaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ test('React and Utils install no new runtime schema or SDK dependencies', async
})

test.each([
['@transloadit/viewer', 'neutral', 20_000, 7_000],
['@transloadit/viewer/react', 'browser', 24_000, 8_000],
['@transloadit/viewer/react/video', 'browser', 12_000, 4_000],
['@transloadit/viewer/server', 'neutral', 20_000, 7_000],
Expand All @@ -31,7 +32,9 @@ test.each([
platform,
minify: true,
metafile: true,
external: ['react', 'react-dom', 'react/jsx-runtime'],
external: entry.startsWith('@transloadit/viewer/react')
? ['react', 'react-dom', 'react/jsx-runtime']
: [],
define: { 'process.env.NODE_ENV': '"production"' },
})
const output = bundled.outputFiles[0]?.contents
Expand Down
11 changes: 6 additions & 5 deletions packages/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ Both prompt commands use the credential resolution above and support these compi
to install it. In a Next.js 16.3.3+ App Router project with Storage enabled, run:

```bash
yarn transloadit auth login
yarn transloadit storage store ./hero.jpg website/hero.jpg --public --hashed
yarn dev
npx @transloadit/node@6.9.0 auth login
npx @transloadit/node@6.9.0 storage store ./hero.jpg website/hero.jpg --public --hashed
npm run dev
```

Follow the image package Quickstart to add its Next plugin and import Image from the package.
Expand All @@ -196,7 +196,9 @@ approval URL; `--stdin` accepts dotenv credentials for automation. Existing cred
fails, it saves the login and prints a Console link. This check does not prove upload availability.

Store creates `transloadit.images.json` and derived `transloadit-images.d.ts`; commit both. The
catalog carries workspace, public prefixes and image receipts: no public app env is needed.
catalog carries workspace, public prefixes, image receipts and other file receipts: no public
app env is needed. Viewer reads its `images` entries; see [Mixed-media originals](#mixed-media-originals)
for other media and the original-byte verification contract.
`--public` declares the destination directory recursively, including future uploads, after saving
the receipt. Plain store never publishes. `image init website/ --example` is an optional example
generator; private init (`image init uploads/ --private`) creates `transloadit.authorize.ts` and a
Expand Down Expand Up @@ -2389,4 +2391,3 @@ Thanks to [Ian Hansen](https://github.com/supershabam) for donating the `translo
## Development

See [CONTRIBUTING](./CONTRIBUTING.md).

11 changes: 6 additions & 5 deletions packages/transloadit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ Both prompt commands use the credential resolution above and support these compi
to install it. In a Next.js 16.3.3+ App Router project with Storage enabled, run:

```bash
yarn transloadit auth login
yarn transloadit storage store ./hero.jpg website/hero.jpg --public --hashed
yarn dev
npx @transloadit/node@6.9.0 auth login
npx @transloadit/node@6.9.0 storage store ./hero.jpg website/hero.jpg --public --hashed
npm run dev
```

Follow the image package Quickstart to add its Next plugin and import Image from the package.
Expand All @@ -196,7 +196,9 @@ approval URL; `--stdin` accepts dotenv credentials for automation. Existing cred
fails, it saves the login and prints a Console link. This check does not prove upload availability.

Store creates `transloadit.images.json` and derived `transloadit-images.d.ts`; commit both. The
catalog carries workspace, public prefixes and image receipts: no public app env is needed.
catalog carries workspace, public prefixes, image receipts and other file receipts: no public
app env is needed. Viewer reads its `images` entries; see [Mixed-media originals](#mixed-media-originals)
for other media and the original-byte verification contract.
`--public` declares the destination directory recursively, including future uploads, after saving
the receipt. Plain store never publishes. `image init website/ --example` is an optional example
generator; private init (`image init uploads/ --private`) creates `transloadit.authorize.ts` and a
Expand Down Expand Up @@ -2389,4 +2391,3 @@ Thanks to [Ian Hansen](https://github.com/supershabam) for donating the `translo
## Development

See [CONTRIBUTING](./CONTRIBUTING.md).

Loading
Loading