Corefunction

defineAssetSource

Keep authored asset names at call sites while a planner selects the optimized output.

Demo

authored pathcommon/textures/noise.pngresolved URLhttps://r2.joyco.studio/jam/common/textures/compressed/noise.webproutingoptimized
'use client'

import { defineAssetSource } from '@joycostudio/xyz'
import { useMemo, useState } from 'react'

import { Button } from '@/components/ui/button'
import { Cluster } from '@/components/ui/cluster'
import { DemoPanel, ValueRow, ValueRows } from '@/components/demos/demo-ui'

export default function AssetDemo() {
  const [optimized, setOptimized] = useState(true)
  const resolve = useMemo(
    () =>
      defineAssetSource({
        url: new URL('https://r2.joyco.studio/jam/'),
        optimize: optimized,
        planner: (input) => ({
          ok: true,
          data: input.endsWith('.png') ? [{ output: input.replace(/([^/]+)\.png$/, 'compressed/$1.webp') }] : [],
        }),
        segments: { common: 'common' },
      }),
    [optimized]
  )

  return (
    <DemoPanel>
      <ValueRows>
        <ValueRow label="authored path">common/textures/noise.png</ValueRow>
        <ValueRow label="resolved URL">{resolve.common('textures/noise.png')}</ValueRow>
        <ValueRow label="routing">{optimized ? 'optimized' : 'source'}</ValueRow>
      </ValueRows>
      <Cluster className="mt-5">
        <Button onClick={() => setOptimized((value) => !value)}>Use {optimized ? 'source' : 'optimized'} asset</Button>
      </Cluster>
    </DemoPanel>
  )
}

Usage

import { defineAssetSource } from '@joycostudio/xyz'

const asset = defineAssetSource({
  url: new URL('https://cdn.example.com/'),
  segments: { textures: 'textures', models: 'models' },
})

asset.textures('noise.png') // https://cdn.example.com/textures/noise.png
asset.models('scene.glb') // https://cdn.example.com/models/scene.glb

API

defineAssetSource

defineAssetSource<const Segments extends Record<string, string> = Record<never, never>>(
  definition: AssetSourceDefinition<Segments>
): AssetSource<Segments>

Creates a callable URL resolver with typed builders for each configured segment. Absolute URLs pass through unchanged. Relative paths keep their query and hash suffixes, including when routed to a planned output.

ArgumentDescription
definition.urlRequired URL for the CDN source root. Its pathname is also the prefix passed to the planner.
definition.planner?Receives a source path without its query or hash. Return { ok: true, data: [{ output: 'path' }] } to replace it. Unsuccessful results or results with zero or multiple outputs keep the source path.
definition.segments?Map of names to path prefixes, exposed as methods on the resolver, such as { textures: 'textures' }.
definition.optimize?Boolean or getter controlling planned outputs. Defaults to true; a getter is evaluated on each relative-path resolution.
const asset = defineAssetSource({
  url: new URL('https://cdn.example.com/assets/'),
  segments: { textures: 'textures' },
  optimize: true,
  planner: (input) => ({
    ok: true,
    data: [{ output: input.replace(/\.png$/, '.webp') }],
  }),
})

asset.textures('noise.png') // https://cdn.example.com/assets/textures/noise.webp

asset

asset(file: string): string

The returned resolver builds a URL from the configured root and optional planner.

ArgumentDescription
fileAn authored relative asset path or an absolute URL to pass through unchanged.
asset('noise.png') // https://cdn.example.com/noise.png

asset[segment]

asset.textures(file: string): string

Each name in definition.segments becomes a method. It adds the configured prefix before resolving the URL; absolute URLs still pass through unchanged.

ArgumentDescription
fileAsset path relative to the segment, or an absolute URL.
asset.textures('noise.png') // https://cdn.example.com/textures/noise.png