@uniweb/build 0.2.3 → 0.2.4

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/build",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -50,8 +50,8 @@
50
50
  "sharp": "^0.33.2"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@uniweb/content-reader": "1.0.5",
54
- "@uniweb/runtime": "0.3.0"
53
+ "@uniweb/content-reader": "1.0.6",
54
+ "@uniweb/runtime": "0.3.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -25,11 +25,12 @@
25
25
  * await writeCollectionFiles(siteDir, collections)
26
26
  */
27
27
 
28
- import { readFile, readdir, stat, writeFile, mkdir } from 'node:fs/promises'
29
- import { join, basename, extname } from 'node:path'
28
+ import { readFile, readdir, stat, writeFile, mkdir, copyFile } from 'node:fs/promises'
29
+ import { join, basename, extname, dirname, relative } from 'node:path'
30
30
  import { existsSync } from 'node:fs'
31
31
  import yaml from 'js-yaml'
32
32
  import { applyFilter, applySort } from './data-fetcher.js'
33
+ import { resolveAssetPath, walkContentAssets } from './assets.js'
33
34
 
34
35
  // Try to import content-reader for markdown parsing
35
36
  let markdownToProseMirror
@@ -202,6 +203,116 @@ function extractFirstImage(node) {
202
203
  return null
203
204
  }
204
205
 
206
+ /**
207
+ * Check if a path is external (http/https/data URL)
208
+ */
209
+ function isExternalUrl(src) {
210
+ return /^(https?:)?\/\//.test(src) || src.startsWith('data:')
211
+ }
212
+
213
+ /**
214
+ * Process assets in collection content
215
+ * - Resolves relative paths to site-root-relative paths
216
+ * - Copies co-located assets to public/library/<collection>/
217
+ * - Updates paths in the content in place
218
+ *
219
+ * @param {Object} content - ProseMirror document
220
+ * @param {string} itemPath - Path to the markdown file
221
+ * @param {string} siteRoot - Site root directory
222
+ * @param {string} collectionName - Name of the collection (e.g., 'articles')
223
+ * @returns {Promise<Object>} Asset manifest for this item
224
+ */
225
+ async function processCollectionAssets(content, itemPath, siteRoot, collectionName) {
226
+ const assets = {}
227
+ const itemDir = dirname(itemPath)
228
+ const publicDir = join(siteRoot, 'public')
229
+ const targetDir = join(publicDir, 'library', collectionName)
230
+
231
+ // Walk content and collect asset paths
232
+ const assetNodes = []
233
+ walkContentAssets(content, (node, path, attrName) => {
234
+ assetNodes.push({ node, attrName })
235
+ })
236
+
237
+ for (const { node, attrName } of assetNodes) {
238
+ const src = node.attrs.src
239
+ if (!src || isExternalUrl(src)) continue
240
+
241
+ // Resolve the path
242
+ const result = resolveAssetPath(src, itemPath, siteRoot)
243
+ if (result.external || !result.resolved) continue
244
+
245
+ let finalPath = src
246
+
247
+ // Handle relative paths (co-located assets)
248
+ if (src.startsWith('./') || src.startsWith('../')) {
249
+ // Check if file exists at resolved location
250
+ if (existsSync(result.resolved)) {
251
+ // Copy to public/library/<collection>/
252
+ const assetFilename = basename(result.resolved)
253
+ const targetPath = join(targetDir, assetFilename)
254
+
255
+ // Ensure target directory exists
256
+ await mkdir(targetDir, { recursive: true })
257
+
258
+ // Copy the asset
259
+ await copyFile(result.resolved, targetPath)
260
+
261
+ // Update path to site-root-relative
262
+ finalPath = `/library/${collectionName}/${assetFilename}`
263
+
264
+ assets[src] = {
265
+ original: src,
266
+ resolved: result.resolved,
267
+ copied: targetPath,
268
+ publicPath: finalPath
269
+ }
270
+ }
271
+ }
272
+ // Handle absolute site paths - just validate they exist
273
+ else if (src.startsWith('/')) {
274
+ const publicPath = join(publicDir, src)
275
+ if (existsSync(publicPath)) {
276
+ assets[src] = {
277
+ original: src,
278
+ resolved: publicPath,
279
+ publicPath: src
280
+ }
281
+ }
282
+ }
283
+
284
+ // Update the node's src attribute if path changed
285
+ if (finalPath !== src) {
286
+ node.attrs.src = finalPath
287
+ }
288
+
289
+ // Also handle poster/preview attributes
290
+ if (node.attrs.poster && !isExternalUrl(node.attrs.poster)) {
291
+ const posterResult = resolveAssetPath(node.attrs.poster, itemPath, siteRoot)
292
+ if (posterResult.resolved && existsSync(posterResult.resolved)) {
293
+ const posterFilename = basename(posterResult.resolved)
294
+ const posterTarget = join(targetDir, posterFilename)
295
+ await mkdir(targetDir, { recursive: true })
296
+ await copyFile(posterResult.resolved, posterTarget)
297
+ node.attrs.poster = `/library/${collectionName}/${posterFilename}`
298
+ }
299
+ }
300
+
301
+ if (node.attrs.preview && !isExternalUrl(node.attrs.preview)) {
302
+ const previewResult = resolveAssetPath(node.attrs.preview, itemPath, siteRoot)
303
+ if (previewResult.resolved && existsSync(previewResult.resolved)) {
304
+ const previewFilename = basename(previewResult.resolved)
305
+ const previewTarget = join(targetDir, previewFilename)
306
+ await mkdir(targetDir, { recursive: true })
307
+ await copyFile(previewResult.resolved, previewTarget)
308
+ node.attrs.preview = `/library/${collectionName}/${previewFilename}`
309
+ }
310
+ }
311
+ }
312
+
313
+ return assets
314
+ }
315
+
205
316
  // Filter and sort utilities are imported from data-fetcher.js
206
317
 
207
318
  /**
@@ -210,9 +321,10 @@ function extractFirstImage(node) {
210
321
  * @param {string} dir - Collection directory path
211
322
  * @param {string} filename - Markdown filename
212
323
  * @param {Object} config - Collection configuration
324
+ * @param {string} siteRoot - Site root directory for asset resolution
213
325
  * @returns {Promise<Object|null>} Processed item or null if unpublished
214
326
  */
215
- async function processContentItem(dir, filename, config) {
327
+ async function processContentItem(dir, filename, config, siteRoot) {
216
328
  const filepath = join(dir, filename)
217
329
  const raw = await readFile(filepath, 'utf-8')
218
330
  const slug = basename(filename, extname(filename))
@@ -228,10 +340,15 @@ async function processContentItem(dir, filename, config) {
228
340
  // Parse markdown body to ProseMirror
229
341
  const content = markdownToProseMirror(body)
230
342
 
343
+ // Process assets (resolve paths, copy co-located files)
344
+ // This modifies content in place, updating paths to site-root-relative
345
+ await processCollectionAssets(content, filepath, siteRoot, config.name)
346
+
231
347
  // Extract excerpt
232
348
  const excerpt = extractExcerpt(frontmatter, content, config.excerpt)
233
349
 
234
350
  // Extract first image (frontmatter takes precedence)
351
+ // Note: paths in content have already been updated by processCollectionAssets
235
352
  const image = frontmatter.image || extractFirstImage(content)
236
353
 
237
354
  // Get file stats for lastModified
@@ -271,7 +388,7 @@ async function collectItems(siteDir, config) {
271
388
 
272
389
  // Process all markdown files
273
390
  let items = await Promise.all(
274
- mdFiles.map(file => processContentItem(collectionDir, file, config))
391
+ mdFiles.map(file => processContentItem(collectionDir, file, config, siteDir))
275
392
  )
276
393
 
277
394
  // Filter out nulls (unpublished items)