@uniweb/press 0.4.1 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/press",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Press your React components into downloadable files — Word, Excel, PDF. Registration-based pattern with lazy-loaded format adapters.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -129,8 +129,13 @@ export async function buildDocument(input, options = {}) {
129
129
  if (footer) collectFootnotes(footer, footnotesState)
130
130
 
131
131
  // Flatten all blocks' IR trees into one array of section children.
132
- // Use async conversion for image support.
133
- const children = await convertChildren(sections.flat())
132
+ // Use async conversion for image support. The host-supplied loadAsset
133
+ // (when present) is threaded down to irToImageParagraph → fetchImageData
134
+ // → fetchAsset, so config-level paths like `assets/diagram.png` resolve
135
+ // to bytes via the framework's content-collector manifest the same way
136
+ // the typst path does.
137
+ const { loadAsset } = options
138
+ const children = await convertChildren(sections.flat(), loadAsset)
134
139
 
135
140
  const sectionOptions = {
136
141
  properties: {
@@ -144,7 +149,7 @@ export async function buildDocument(input, options = {}) {
144
149
  }
145
150
 
146
151
  if (header) {
147
- const headerChildren = await convertChildren(header)
152
+ const headerChildren = await convertChildren(header, loadAsset)
148
153
  const headerObj = new Header({ children: headerChildren })
149
154
  const defaultHeaderObj = createDefaultHeaderFooter(true)
150
155
 
@@ -160,7 +165,7 @@ export async function buildDocument(input, options = {}) {
160
165
  }
161
166
 
162
167
  if (footer) {
163
- const footerChildren = await convertChildren(footer)
168
+ const footerChildren = await convertChildren(footer, loadAsset)
164
169
  const footerObj = new Footer({ children: footerChildren })
165
170
  const defaultFooterObj = createDefaultHeaderFooter(false)
166
171
 
@@ -280,10 +285,14 @@ export function createDefaultHeaderFooter(isHeader) {
280
285
 
281
286
  /**
282
287
  * Convert an array of IR nodes to section children, handling async
283
- * image fetches via Promise.all.
288
+ * image fetches via Promise.all. `loadAsset` (when provided) is forwarded
289
+ * to image-emitting paths so host-supplied byte loaders take precedence
290
+ * over the URL-based fetch fallback.
284
291
  */
285
- async function convertChildren(nodes) {
286
- const results = await Promise.all(nodes.map(irToSectionChildrenAsync))
292
+ async function convertChildren(nodes, loadAsset) {
293
+ const results = await Promise.all(
294
+ nodes.map((node) => irToSectionChildrenAsync(node, loadAsset)),
295
+ )
287
296
  return results.flat()
288
297
  }
289
298
 
@@ -294,12 +303,12 @@ async function convertChildren(nodes) {
294
303
  /**
295
304
  * Convert an IR node into section-level docx children (async for images).
296
305
  */
297
- async function irToSectionChildrenAsync(node) {
306
+ async function irToSectionChildrenAsync(node, loadAsset) {
298
307
  switch (node.type) {
299
308
  case 'table':
300
309
  return [await irToTableAsync(node)]
301
310
  case 'image':
302
- return [await irToImageParagraph(node)]
311
+ return [await irToImageParagraph(node, loadAsset)]
303
312
  case 'tableOfContents':
304
313
  return [irToTableOfContents(node)]
305
314
  case 'webOnly':
@@ -804,12 +813,12 @@ function inferImageType(src, data) {
804
813
  * valid type). Do not simplify the altText spread — `name: ''` looks
805
814
  * like a no-op but Word-for-Mac rejects the file without it.
806
815
  */
807
- async function irToImageParagraph(node) {
816
+ async function irToImageParagraph(node, loadAsset) {
808
817
  try {
809
818
  const src = node.src || ''
810
819
  if (!src) return new DocxParagraph({})
811
820
 
812
- const imageData = await fetchImageData(src)
821
+ const imageData = await fetchImageData(src, loadAsset)
813
822
 
814
823
  const width = toInt(node.transformation?.width) ?? 400
815
824
  const height = toInt(node.transformation?.height) ?? 300
@@ -847,7 +856,7 @@ async function irToImageParagraph(node) {
847
856
  * adapter use; see principle 6 ("Extract shared logic when a second
848
857
  * adapter needs it") in docs/architecture/principles.md.
849
858
  */
850
- async function fetchImageData(url) {
851
- const { bytes } = await fetchAsset(url)
859
+ async function fetchImageData(url, loadAsset) {
860
+ const { bytes } = await fetchAsset(url, { loadAsset })
852
861
  return bytes
853
862
  }
@@ -59,7 +59,7 @@ import { fetchAssets } from '../assets/fetch.js'
59
59
  */
60
60
  export async function compileEpub(input, options = {}) {
61
61
  const { sections = [], metadata = null } = input || {}
62
- const { meta = {}, stylesheet, identifier, cover } = options
62
+ const { meta = {}, stylesheet, identifier, cover, loadAsset } = options
63
63
 
64
64
  const resolvedMeta = { ...(metadata || {}), ...meta }
65
65
  const css = stylesheet || DEFAULT_STYLESHEET
@@ -74,13 +74,17 @@ export async function compileEpub(input, options = {}) {
74
74
  // Deduplicate image URLs across all chapters — and the cover, if any —
75
75
  // and fetch them in parallel. Failed fetches are logged and the original
76
76
  // URL is left in place; the EPUB still opens but may show broken images.
77
+ // The host-supplied `loadAsset` (when present) is tried before fetch —
78
+ // browser hosts wrap fetch; Node hosts (unipress) wrap node:fs so config-
79
+ // level paths like `assets/cover.jpg` resolve to bytes via the manifest
80
+ // the framework's content collector populated.
77
81
  const imageUrls = new Set()
78
82
  for (const ch of chapters) {
79
83
  for (const url of ch.images) imageUrls.add(url)
80
84
  }
81
85
  if (coverUrl) imageUrls.add(coverUrl)
82
86
  const fetched = imageUrls.size
83
- ? await fetchAssets(imageUrls)
87
+ ? await fetchAssets(imageUrls, { loadAsset })
84
88
  : new Map()
85
89
 
86
90
  // Assign a manifest-relative path to every successfully-fetched image,
@@ -22,16 +22,47 @@
22
22
  /**
23
23
  * Fetch a single asset URL and return normalized bytes + metadata.
24
24
  *
25
+ * Resolution order:
26
+ * 1. `options.loadAsset(url)` — host-supplied byte loader. Browser hosts
27
+ * typically wrap `fetch`; Node hosts (unipress) wrap `node:fs`. When
28
+ * present, tried first; on throw or null/undefined return, falls
29
+ * through to `fetch`. This is how foundations stay environment-
30
+ * agnostic — they declare `book.covers.front: assets/front.jpg` and
31
+ * the host's loadAsset turns that into bytes the same way the host
32
+ * handles every other config-level asset.
33
+ * 2. `fetch()` (or `options.fetch` for tests) — the legacy URL-based
34
+ * path. Required for absolute http(s) URLs and as a fallback for
35
+ * anything `loadAsset` can't handle.
36
+ *
25
37
  * @param {string} url
26
38
  * @param {Object} [options]
39
+ * @param {(src: string) => Promise<Uint8Array | null>} [options.loadAsset]
40
+ * - host-supplied byte loader. See above.
27
41
  * @param {typeof fetch} [options.fetch] - override for tests / node-fetch.
28
42
  * @returns {Promise<{ bytes: Uint8Array, mime: string, hash: string, ext: string }>}
29
43
  */
30
44
  export async function fetchAsset(url, options = {}) {
45
+ const { loadAsset } = options
46
+
47
+ if (typeof loadAsset === 'function') {
48
+ try {
49
+ const bytes = await loadAsset(url)
50
+ if (bytes && bytes.length > 0) {
51
+ const mime = detectMime(url, bytes)
52
+ const ext = extForMime(mime) || extFromUrl(url) || 'bin'
53
+ const hash = await hashContent(bytes)
54
+ return { bytes, mime, hash, ext }
55
+ }
56
+ } catch {
57
+ // Fall through to fetch — loadAsset may not handle every URL
58
+ // shape (e.g., absolute http(s) URLs in a Node host).
59
+ }
60
+ }
61
+
31
62
  const fetchImpl = options.fetch || globalThis.fetch
32
63
  if (typeof fetchImpl !== 'function') {
33
64
  throw new Error(
34
- 'fetchAsset: no fetch() available. Pass options.fetch or run in an environment that provides globalThis.fetch.',
65
+ 'fetchAsset: no fetch() available. Pass options.fetch / options.loadAsset or run in an environment that provides globalThis.fetch.',
35
66
  )
36
67
  }
37
68
  const res = await fetchImpl(url)