@joycostudio/susano 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,211 +1,436 @@
1
- # <img src="./static/JOYCO.png" alt="JOYCO Logo" height="36" width="36" align="top" />&nbsp;&nbsp;JOYCO Susano
1
+ # <img src="./static/JOYCO.png" alt="JOYCO Logo" height="36" width="36" align="top" />&nbsp;&nbsp;Susano
2
2
 
3
- Asset load orchestration made easy.
3
+ [![npm](https://img.shields.io/npm/v/@joycostudio/susano?label=npm&color=0344DC&labelColor=white)](https://www.npmjs.com/package/@joycostudio/susano)
4
+ [![gzip](https://deno.bundlejs.com/badge?q=@joycostudio/susano&treeshake=[*])](https://bundlejs.com/?q=%40joycostudio%2Fsusano&treeshake=%5B*%5D)
5
+
6
+ Asset load orchestration made easy. A typed, promise-friendly loader for images, video, audio, and arbitrary data — with built-in progress tracking, per-call postprocessing, and pluggable loader types.
7
+
8
+ ## Mental model
9
+
10
+ Susano separates two things that are usually conflated:
11
+
12
+ - **Content** — the expensive fetch (`HTMLImageElement`, decoded buffer, parsed JSON). **Cached and deduped per URL.** Loaded once, no matter how many call sites ask for it.
13
+ - **Result** — what *your* call wants out of that content, via an optional `postprocess`. **Per call, never cached.** Two call sites can derive different results from the same shared content.
14
+
15
+ So `load()` returns a **`Promise<R>`** of your call's result, and `batch()` returns a **`Batch`** handle that tracks a fixed set. Reach the shared content loader (raw content, events) via `susano.get(url)`. Want to cache a transformed result too? Encode the transform in a custom loader, so its output *becomes* the content.
16
+
17
+ ## Features
18
+
19
+ | Feature | Description |
20
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
21
+ | **Scoped batches** | `batch([...])` loads a fixed set and tracks **only** that set's progress/completion. Auto-starts; returns a handle with `.promise` / `.progress` / `.completed`. |
22
+ | **Content dedup** | One content fetch per URL across every `load()` / `batch()` — in-flight or already-loaded assets are joined, never re-fetched. |
23
+ | **Per-call postprocess** | `postprocess` is your call's projection of the shared content. It always runs for your call; results aren't cached. |
24
+ | **Built-in loaders** | First-class `image`, `video`, `audio`, and `generic` out of the box. |
25
+ | **Pluggable types** | Pass a `{ [type]: LoaderClass }` map to `new Susano(...)` — the keys become the valid `type`s, fully inferred for `load()` / `batch()`. |
26
+ | **Stable promises** | `load()` resolves with your result; every `Batch` exposes a `.promise`. Await one result or a whole set. |
27
+ | **Typed manifest** | TypeScript knows which `loaderArgs` and `postprocess` content type are valid for each registered `type`. |
28
+
29
+ ## Install
4
30
 
5
31
  ```bash
6
32
  pnpm add @joycostudio/susano
7
33
  ```
8
34
 
9
- ## Quick Start
35
+ ## Quick start
10
36
 
11
37
  ```ts
12
38
  import { susano } from '@joycostudio/susano'
13
39
 
14
- // Queue assets
15
- susano.add('/hero.png', { type: 'image' })
16
- susano.add('/intro.mp4', { type: 'video' })
40
+ const batch = susano.batch(
41
+ [
42
+ { url: '/hero.png', type: 'image' },
43
+ { url: '/intro.mp4', type: 'video' },
44
+ { url: '/music.mp3', type: 'audio' },
45
+ ],
46
+ {
47
+ onProgress: ({ value }) => console.log(`${Math.round(value * 100)}%`),
48
+ onCompleted: () => console.log('All assets loaded!'),
49
+ }
50
+ )
51
+
52
+ await batch.promise
53
+ ```
54
+
55
+ ## Quick start (React)
56
+
57
+ ```tsx
58
+ import { useEffect, useState } from 'react'
59
+ import { susano, type BatchProgressEventArgs } from '@joycostudio/susano'
60
+
61
+ function App() {
62
+ const [progress, setProgress] = useState(0)
63
+
64
+ useEffect(() => {
65
+ susano.batch(
66
+ [
67
+ { url: '/hero.png', type: 'image' },
68
+ { url: '/intro.mp4', type: 'video' },
69
+ ],
70
+ { onProgress: ({ value }: BatchProgressEventArgs) => setProgress(value) }
71
+ )
72
+ }, [])
73
+
74
+ return <div>Loading: {Math.round(progress * 100)}%</div>
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Architecture
81
+
82
+ | Layer | Responsibility |
83
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
84
+ | **Susano** | Registry of loader classes keyed by `type`, plus the `items` cache (one content loader per URL). Exposes `load()`, `batch()`, `get()`. Its private per-call projection ensures content, then applies that call's `postprocess`. |
85
+ | **SusanoLoader** | Caches one piece of raw **content** per URL. Idempotent `load()`, tracks `status` / `progress`, emits `loaded` / `progress` / `error`. Does **not** postprocess. |
86
+ | **Batch** | Tracks progress + completion for a fixed set of loads. Owns its own counter and completion promise — concurrent batches never interfere. |
87
+ | **Loader types** | `ImageLoader`, `VideoLoader`, `AudioLoader`, `GenericLoader` — each wraps a native element or a custom `loadFn`. |
88
+
89
+ > Completion is the resolution of a `Promise.all` over the batch — one-shot by construction. A failed load is *fail-soft*: it surfaces via `onError` and still advances the batch so a load screen can finish.
90
+
91
+ ---
92
+
93
+ ## Default instance
94
+
95
+ The package ships a pre-configured `susano` singleton with `image`, `video`, `audio`, and `generic` loaders registered. For most apps this is the only instance you need.
17
96
 
18
- // Start loading
19
- susano.start({
20
- onProgress: ({ value }) => console.log(`${Math.round(value * 100)}%`),
21
- onCompleted: () => console.log('All assets loaded!'),
97
+ ```ts
98
+ import { susano } from '@joycostudio/susano'
99
+ ```
100
+
101
+ Need a second, isolated registry? Construct your own — the loader map is the single source of truth, and `load()` / `batch()` are fully inferred from it:
102
+
103
+ ```ts
104
+ import { Susano, ImageLoader, VideoLoader, AudioLoader, GenericLoader } from '@joycostudio/susano'
105
+
106
+ const s = new Susano({
107
+ image: ImageLoader,
108
+ video: VideoLoader,
109
+ audio: AudioLoader,
110
+ generic: GenericLoader,
22
111
  })
112
+
113
+ s.load('/x.png', { type: 'image' }) // `type` is 'image' | 'video' | 'audio' | 'generic'
23
114
  ```
24
115
 
25
- ## API
116
+ Add your own loader by adding a key — no separate type map, no `registerLoader`, nothing to keep in sync:
117
+
118
+ ```ts
119
+ const s = new Susano({ image: ImageLoader, json: JSONLoader })
120
+ // ^ adds 'json' as a valid type, fully typed
121
+ ```
26
122
 
27
- ### `susano` singleton
123
+ ---
28
124
 
29
- A pre-configured instance with built-in loaders for `image`, `video`, `audio`, and `generic` types.
125
+ ## Core API
30
126
 
31
- ### `add(url, { type, loaderArgs? })`
127
+ ### Susano
32
128
 
33
- Queue an asset for batch loading. Returns the loader instance.
129
+ `new Susano(loaders)` `loaders` is a `{ [type]: LoaderClass }` map. Its keys become the valid `type`s; each loader's content/`loaderArgs` types are inferred per key.
130
+
131
+ | Member | Description |
132
+ | ------------------------------- | ------------------------------------------------------------------------- |
133
+ | `load(url, cnfg)` | Load one asset; returns a `Promise<R>` for this call's result. |
134
+ | `batch(entries, handlers?)` | Load a fixed set and track its progress/completion; returns a `Batch`. |
135
+ | `get(url)` | The shared content loader cached for `url`, or `undefined`. Shorthand for `items.get(url)`. |
136
+ | `items` | `Map<url, loader>` — every content loader, keyed by URL. |
137
+
138
+ A load config splits **construction** from **projection**:
34
139
 
35
140
  ```ts
36
- const img = susano.add('/photo.png', {
37
- type: 'image',
38
- loaderArgs: { srcSet: '480w.png 480w, 800w.png 800w', sizes: '(max-width: 600px) 480px, 800px' },
141
+ susano.load(url, {
142
+ type, // which loader
143
+ loaderArgs?, // construction: how to produce the content (srcSet, loadEvent, loadFn…)
144
+ cache?, // false → force a fresh content fetch (default true)
145
+ postprocess?, // per call: (content, loader) => R | Promise<R>
146
+ onLoaded?, // per call: (result, loader) => void
147
+ onProgress?, // observes the shared content fetch: (value, loader) => void
148
+ onError?, // per call: (error, loader) => void
39
149
  })
40
150
  ```
41
151
 
42
- ### `load(url, { type, loaderArgs? })`
152
+ `loaderArgs` defines the content and is honored once (when the loader is first created for a URL). `postprocess` / `onLoaded` / `onError` are **per call** — each `load()`/batch entry gets its own.
43
153
 
44
- Load a single asset immediately (standalone, outside the batch queue). Returns the loader instance.
154
+ #### `load()`
45
155
 
46
156
  ```ts
47
- const img = susano.load('/standalone.png', { type: 'image' })
157
+ const bitmap = await susano.load('/hero.png', {
158
+ type: 'image',
159
+ postprocess: async (img) => createImageBitmap(img),
160
+ }) // ImageBitmap (this call's result)
161
+
162
+ susano.get('/hero.png')?.content // the shared HTMLImageElement (cached)
48
163
  ```
49
164
 
50
- ### `start({ onProgress?, onCompleted? })`
165
+ Calling `load()` again for the same URL reuses the cached content (no second fetch) and runs *this* call's `postprocess`. Pass `cache: false` to force a fresh content fetch that overwrites the cached content.
51
166
 
52
- Start loading all queued assets. Emits `start`, `progress`, and `completed` events.
167
+ #### `batch()`
53
168
 
54
169
  ```ts
55
- susano.start({
56
- onProgress: ({ value, item }) => {
57
- // value: 0 to 1
58
- },
59
- onCompleted: (susano) => {
60
- // all assets loaded
61
- },
62
- })
170
+ const batch = susano.batch(
171
+ [
172
+ { url: '/studio.glb', type: 'gltf', postprocess: parseScene },
173
+ { url: '/env.exr', type: 'exr' },
174
+ { url: '/noise.png', type: 'texture', cache: false },
175
+ ],
176
+ { onProgress, onCompleted, onError }
177
+ )
63
178
  ```
64
179
 
65
- ### `registerLoader(type, loader)`
180
+ Each entry runs a per-call load. Content fetches dedupe across entries (and across any concurrent `load()`), so an in-flight or already-loaded asset is **joined**, not re-fetched. Auto-starts; returns a [`Batch`](#batch). An empty batch completes immediately with `value: 1`.
181
+
182
+ ### Batch
183
+
184
+ The handle returned by `susano.batch()`. Auto-starts on the next microtask, so handlers passed to `batch()` (or `.on()` listeners attached immediately after) never miss a tick.
66
185
 
67
- Register a custom loader class for a given type.
186
+ | Member | Type | Description |
187
+ | ------------- | ------------------ | -------------------------------------------- |
188
+ | `promise` | `Promise<Batch>` | Resolves once the whole set has settled. |
189
+ | `progress` | `number` | `0 → 1`, readable at any time. |
190
+ | `completed` | `boolean` | `true` once finished. |
191
+ | `loadCount` | `number` | Loads settled so far. |
192
+ | `loadLength` | `number` | Total loads in the batch. |
193
+ | `items` | `BatchItem[]` | `{ loader, promise }` per entry (entry order). |
68
194
 
69
195
  ```ts
70
- susano.registerLoader('custom', CustomLoader)
196
+ type BatchHandlers = {
197
+ onProgress?: (e: BatchProgressEventArgs) => void // { value, loader, batch }
198
+ onCompleted?: (batch: Batch) => void
199
+ onError?: (e: BatchErrorEventArgs) => void // { loader, error, batch }
200
+ }
71
201
  ```
72
202
 
73
- ## Built-in Loaders
203
+ `Batch` extends `TinyEmitter` — subscribe directly with `.on('progress' | 'error' | 'completed', …)`. The `'completed'` event fires exactly once; `'progress'`'s `loader` is `null` for the empty-batch tick.
74
204
 
75
- ### `image`
205
+ ---
76
206
 
77
- Loads images via `HTMLImageElement`.
207
+ ### SusanoLoader
208
+
209
+ Base class for every loader. Caches one piece of raw content per URL. You instantiate subclasses; reach any loader via `susano.get(url)`.
210
+
211
+ #### Properties
212
+
213
+ | Property | Type | Description |
214
+ | ---------- | --------------------------------------------- | ---------------------------------------------------- |
215
+ | `url` | `string` | The source URL. |
216
+ | `status` | `'idle' \| 'loading' \| 'loaded' \| 'error'` | Content lifecycle state. |
217
+ | `loading` | `boolean` | `true` while the content fetch is in flight. |
218
+ | `loaded` | `boolean` | `true` once content has loaded. |
219
+ | `content` | `T` | The raw loaded content. |
220
+ | `progress` | `number` | `0 → 1`. |
221
+ | `promise` | `Promise<T>` | Stable promise resolving with the raw **content**. |
222
+
223
+ #### Methods
224
+
225
+ | Method | Description |
226
+ | -------------------- | --------------------------------------------------------------------------------------------------- |
227
+ | `load(cache = true)` | Start (or join) the content fetch. Idempotent: repeat calls return the same promise without re-fetching. `cache: false` forces a fresh fetch that overwrites the content. |
228
+
229
+ #### Events
230
+
231
+ ```ts
232
+ susano.load('/photo.png', { type: 'image' })
233
+ const loader = susano.get('/photo.png')!
234
+ loader.on('loaded', (l) => console.log(l.content))
235
+ loader.on('error', (err) => console.error(err))
236
+ ```
237
+
238
+ #### Postprocess
78
239
 
79
- | Option | Type | Description |
80
- | -------- | -------- | ----------------------------- |
81
- | `srcSet` | `string` | Maps to `img.srcset` |
82
- | `sizes` | `string` | Maps to `img.sizes` |
240
+ `postprocess` is a **per-call** projection from the shared content to your result. It runs every call (never cached), and an async `postprocess` blocks that call's `loaded` / `promise` until it resolves.
83
241
 
84
242
  ```ts
85
- susano.add('/photo.png', {
243
+ const bitmap: ImageBitmap = await susano.load('/hero.png', {
86
244
  type: 'image',
87
- loaderArgs: { srcSet: '/photo-2x.png 2x', sizes: '100vw' },
245
+ postprocess: async (img) => createImageBitmap(img),
88
246
  })
89
247
  ```
90
248
 
91
- ### `video`
249
+ > Two calls for the same URL share one content fetch but each run their own `postprocess` — so there's no composition or "which transform wins" ambiguity. If a transform is expensive and you want its output cached/shared, promote it into a custom loader so the transformed value becomes the content.
92
250
 
93
- Loads video via `HTMLVideoElement`.
251
+ ---
94
252
 
95
- | Option | Type | Default | Description |
96
- | ----------- | ---------------------------------- | ----------- | ---------------------------------- |
97
- | `video` | `HTMLVideoElement` | new element | Existing video element to load into |
98
- | `loadEvent` | `'canplay'` \| `'canplaythrough'` | `'canplay'` | Event that signals load completion |
253
+ ## Built-in loaders
254
+
255
+ ### image
256
+
257
+ Loads images via `HTMLImageElement`.
99
258
 
100
259
  ```ts
101
- susano.add('/intro.mp4', {
102
- type: 'video',
103
- loaderArgs: { loadEvent: 'canplaythrough' },
104
- })
260
+ import { ImageLoader, type SusanoImageLoaderConfig } from '@joycostudio/susano'
105
261
  ```
106
262
 
107
- ### `audio`
263
+ | `loaderArgs` | Type | Description |
264
+ | ------------ | -------- | -------------------- |
265
+ | `srcSet` | `string` | Maps to `img.srcset` |
266
+ | `sizes` | `string` | Maps to `img.sizes` |
108
267
 
109
- Loads audio via `HTMLAudioElement`.
268
+ ```ts
269
+ susano.load('/photo.png', {
270
+ type: 'image',
271
+ loaderArgs: { srcSet: '/photo.png 1x, /photo-2x.png 2x', sizes: '100vw' },
272
+ })
273
+ ```
274
+
275
+ ### video / audio
110
276
 
111
- | Option | Type | Default | Description |
112
- | ----------- | ---------------------------------- | ----------- | ---------------------------------- |
113
- | `audio` | `HTMLAudioElement` | new element | Existing audio element to load into |
114
- | `loadEvent` | `'canplay'` \| `'canplaythrough'` | `'canplay'` | Event that signals load completion |
277
+ Load via `HTMLVideoElement` / `HTMLAudioElement`.
115
278
 
116
279
  ```ts
117
- susano.add('/music.mp3', {
118
- type: 'audio',
119
- loaderArgs: { loadEvent: 'canplaythrough' },
120
- })
280
+ import { VideoLoader, AudioLoader } from '@joycostudio/susano'
121
281
  ```
122
282
 
123
- ### `generic`
283
+ | `loaderArgs` | Type | Default | Description |
284
+ | --------------- | --------------------------------- | ------------- | ------------------------------------ |
285
+ | `video`/`audio` | element | new element | Existing element to load into. |
286
+ | `loadEvent` | `'canplay' \| 'canplaythrough'` | `'canplay'` | Event that signals load completion. |
124
287
 
125
- Fully custom loader. Requires a `loadFn` callback.
288
+ ```ts
289
+ susano.load('/intro.mp4', { type: 'video', loaderArgs: { loadEvent: 'canplaythrough' } })
290
+ ```
291
+
292
+ ### generic
126
293
 
127
- | Option | Type | Description |
128
- | -------- | ------------- | ------------------------------------ |
129
- | `loadFn` | `GenericLoadFn` | **Required.** Custom load function |
294
+ Fully custom content production. You provide `loadFn`; Susano handles dedup, progress, and promises.
130
295
 
131
- The `loadFn` receives `{ url, done, error, progress }`:
296
+ ```ts
297
+ import { GenericLoader, type GenericLoadFn } from '@joycostudio/susano'
298
+
299
+ type GenericLoadFn = (ctx: {
300
+ url: string
301
+ done: (content: any) => void
302
+ error: (error: Error) => void
303
+ progress: (value: number) => void // 0 → 1
304
+ }) => void
305
+ ```
132
306
 
133
307
  ```ts
134
- susano.add('/data.json', {
308
+ susano.load('/data.json', {
135
309
  type: 'generic',
136
310
  loaderArgs: {
137
- loadFn: ({ url, done, error, progress }) => {
138
- fetch(url)
139
- .then((res) => res.json())
140
- .then((data) => done(data))
141
- .catch((err) => error(err))
311
+ loadFn: ({ url, done, error }) => {
312
+ fetch(url).then((r) => r.json()).then(done).catch(error)
142
313
  },
143
314
  },
144
315
  })
145
316
  ```
146
317
 
147
- ## Events
148
-
149
- The `susano` instance extends `TinyEmitter` and emits:
318
+ ---
150
319
 
151
- | Event | Payload | Description |
152
- | ----------- | ---------------------------------------------------- | ------------------------- |
153
- | `start` | `susano` | Batch loading started |
154
- | `progress` | `{ value: number, item: SusanoLoader, susano }` | An asset finished loading |
155
- | `completed` | `susano` | All assets loaded |
320
+ ## Recipes
156
321
 
157
- Individual loader instances also emit `loaded` and `progress` events.
322
+ ### Awaiting a single result
158
323
 
159
- ## Batched vs Standalone Loading
324
+ ```ts
325
+ const img = susano.load('/hero.png', { type: 'image' })
326
+ document.body.appendChild(await img.promise)
327
+ ```
160
328
 
161
- **Batched** queue multiple assets with `add()`, then call `start()` to load them all. Progress is tracked across the entire batch.
329
+ ### Awaiting a whole batch
162
330
 
163
331
  ```ts
164
- susano.add('/a.png', { type: 'image' })
165
- susano.add('/b.mp4', { type: 'video' })
166
- susano.start({ onCompleted: () => console.log('done') })
332
+ const [a, b] = await Promise.all([
333
+ susano.load('/a.png', { type: 'image' }),
334
+ susano.load('/b.mp4', { type: 'video' }),
335
+ ])
336
+ // …or drive a progress bar with a batch over the same URLs (dedupes the fetches):
337
+ await susano.batch([
338
+ { url: '/a.png', type: 'image' },
339
+ { url: '/b.mp4', type: 'video' },
340
+ ]).promise
167
341
  ```
168
342
 
169
- **Standalone** use `load()` to immediately load a single asset without affecting the batch queue.
343
+ ### Per-call postprocess, shared fetch
170
344
 
171
345
  ```ts
172
- const item = susano.load('/lazy.png', { type: 'image' })
346
+ // Both share one /noise.png fetch; each gets its own result.
347
+ const bitmap = await susano.load('/noise.png', { type: 'image', postprocess: (i) => createImageBitmap(i) })
348
+ const raw = await susano.load('/noise.png', { type: 'image' }) // raw HTMLImageElement, shared fetch
173
349
  ```
174
350
 
175
- ## 🤖 Automatic Workflows
351
+ ### Fetch with streamed progress
176
352
 
177
- 1. **Release Workflow** (`.github/workflows/release.yml`): Automates the release process using Changesets. When enabled, it will automatically create release pull requests and publish to npm when changes are pushed to the main branch.
353
+ ```ts
354
+ susano.load('/big.bin', {
355
+ type: 'generic',
356
+ loaderArgs: {
357
+ loadFn: async ({ url, done, error, progress }) => {
358
+ try {
359
+ const res = await fetch(url)
360
+ const total = Number(res.headers.get('content-length')) || 0
361
+ const reader = res.body!.getReader()
362
+ const chunks: Uint8Array[] = []
363
+ let received = 0
364
+ while (true) {
365
+ const { done: d, value } = await reader.read()
366
+ if (d) break
367
+ chunks.push(value)
368
+ received += value.length
369
+ if (total) progress(received / total)
370
+ }
371
+ done(new Blob(chunks))
372
+ } catch (e) {
373
+ error(e as Error)
374
+ }
375
+ },
376
+ },
377
+ })
378
+ ```
178
379
 
179
- 2. **Publish Any Commit** (`.github/workflows/publish-any-commit.yml`): A utility workflow that can build and publish packages for any commit or pull request.
380
+ ### Custom loader type
180
381
 
181
- ## 🦋 Version Management
382
+ A custom loader implements `protected _load()` — the raw content fetch. The base owns the promise, dedup, and status; call `_onLoaded()` / `_onError()` / `_onProgress()` when the work settles. (If your loader's job is to *produce* a transformed value, that value becomes the cached content here.)
383
+
384
+ ```ts
385
+ import { Susano, SusanoLoader } from '@joycostudio/susano'
386
+
387
+ class JSONLoader extends SusanoLoader<unknown> {
388
+ protected _load() {
389
+ fetch(this.url)
390
+ .then((res) => res.json())
391
+ .then((content) => {
392
+ this.content = content
393
+ this._onLoaded()
394
+ })
395
+ .catch((e) => this._onError(e))
396
+ }
397
+ }
398
+
399
+ const s = new Susano({ json: JSONLoader })
400
+
401
+ await s.batch([{ url: '/config.json', type: 'json' }]).promise
402
+ ```
182
403
 
183
- This library uses [Changesets](https://github.com/changesets/changesets) to manage versions and publish releases. Here's how to use it:
404
+ ### Lazy load + batch, same URL
184
405
 
185
- ### Adding a changeset
406
+ `load()` and `batch()` share one content loader per URL — so an eager preload and a later manifest batch share the fetch.
186
407
 
187
- When you make changes that need to be released:
408
+ ```ts
409
+ susano.load('/hero.png', { type: 'image' }) // fires immediately
410
+ const batch = susano.batch([{ url: '/hero.png', type: 'image' }]) // joins it — no second fetch
188
411
 
189
- ```bash
190
- pnpm changeset
412
+ await batch.promise
413
+ susano.get('/hero.png') // the one shared content loader both used
191
414
  ```
192
415
 
193
- This will prompt you to:
416
+ ---
194
417
 
195
- 1. Select which packages you want to include in the changeset
196
- 2. Choose whether it's a major/minor/patch bump
197
- 3. Provide a summary of the changes
418
+ ## Package exports
198
419
 
199
- ### Creating a release
420
+ | Import path | Contents |
421
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
422
+ | `@joycostudio/susano` | `susano` (default instance), `Susano`, `Batch`, `SusanoLoader`, `ImageLoader`, `VideoLoader`, `AudioLoader`, `GenericLoader`, `VERSION`, and all types (`LoadOptions`, `BatchEntry`, `BatchItem`, `BatchHandlers`, `BatchProgressEventArgs`, `BatchErrorEventArgs`, `ContentOf`, …). |
200
423
 
201
- To create a new version and update the changelog:
424
+ ---
202
425
 
203
- ```bash
204
- # 1. Create new versions of packages
205
- pnpm version:package
426
+ ## Development
206
427
 
207
- # 2. Release (builds and publishes to npm)
208
- pnpm release
428
+ ```bash
429
+ pnpm install
430
+ pnpm test # Vitest suite (run); pnpm test:watch for watch mode
431
+ pnpm typecheck # tsc --noEmit
432
+ pnpm build # tsup bundle
209
433
  ```
210
434
 
211
- Remember to commit all changes after creating a release.
435
+ - [`docs/architecture.md`](./docs/architecture.md) how Susano is built and why (content-vs-result, the dedup state machine, batch completion), with diagrams.
436
+ - [`docs/testing.md`](./docs/testing.md) — the test setup (Vitest + jsdom), conventions, and patterns, written to be replicated across JOYCO libraries.