@joycostudio/chopper
v0.8.0
Published
Isomorphic asset optimization pipeline
Readme
@joycostudio/chopper
Isomorphic image and GLB optimization for browser workers, Node, and the CLI.
import { createNodeChopper } from '@joycostudio/chopper/node'
const chopper = createNodeChopper()
const result = await chopper.optimize(
{ bytes, fileName: 'hero.png' },
{
kind: 'image',
encode: { codec: 'webp', quality: 82 },
}
)
if (!result.ok) console.error(result.error)Use @joycostudio/chopper for runtime-neutral recipes and result types,
@joycostudio/chopper/node for filesystem and batch APIs,
@joycostudio/chopper/browser for the worker-backed browser host, and
@joycostudio/chopper/config for createChopper.
Before using the browser host, publish its pinned codecs:
chopper codecs sync public/chopper-codecsThe browser defaults to a module worker and verifies the codec manifest and each downloaded artifact. Node resolves the same artifacts from this package.
Batch results expose the same complete optimization provenance as the core API:
import { runConfig } from '@joycostudio/chopper/node'
const result = await runConfig({ mode: 'report' })
if (result.ok) {
for (const file of result.data.files) {
if (file.optimization) {
console.log(file.input, file.optimization.recipe, file.optimization.manifestHash)
}
}
}optimization is present for successful write, check, and report entries
and omitted for failures. The existing beforeBytes, afterBytes, and
engineId fields remain available on each successful entry.
When an asset key is already known, the config entry can route it without filesystem or network access:
import { createChopper } from '@joycostudio/chopper/config'
const config = createChopper({
rules: [
{
include: ['assets/**/*.{png,jpg}'],
recipe: { kind: 'image', encode: { codec: 'webp', quality: 82 } },
},
],
})
const planned = config.resolve('assets/hero.png')
if (planned.ok) {
// planned.data[0].output === 'assets/compressed/hero.webp'
}outputDirectory is the sole logical routing authority and defaults to
compressed. It places every generated asset in a reserved sibling directory
while preserving the winning recipe's extension. The reservation applies at every depth, so
assets/compressed/hero.webp is classified as a generated output before source
rules run—even if a broad assets/**/* rule would otherwise match it.
Use route() when consumers need that classification explicitly:
config.route('assets/hero.png')
// { ok: true, data: { kind: 'source', input: 'assets/hero.png', outputs: [...] } }
config.route('assets/compressed/hero.webp')
// { ok: true, data: { kind: 'output', input: 'assets/compressed/hero.webp' } }
config.route('scripts/build.ts')
// { ok: true, data: { kind: 'unmatched', input: 'scripts/build.ts' } }resolve() is the compact planning API: it returns the planned outputs for a
source and a successful empty array for generated or unmatched paths. One source
may produce several distinct outputs.
The optional buildDirectory setting is separate: it is the physical
filesystem root where Node and the CLI write generated bytes. It defaults to
.chopper and is never included in PlannedConfigAsset.output; changing it
does not change logical asset routes. Set it only when the build root needs to
move:
createChopper({
buildDirectory: '.custom-build-root',
outputDirectory: 'optimized',
rules: [
/* ... */
],
})outputDirectory controls the reserved sibling segment in those logical routes.
Override it at config level when needed. Rules only decide which assets and
recipes apply; per-rule output path templates are intentionally unsupported so
destination routing has one owner.
See the repository README for recipes, CLI commands, config, and the implementation plan.
