@uniweb/build 0.1.2 → 0.1.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/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @uniweb/build
2
2
 
3
- Foundation build tooling for the Uniweb Component Web Platform.
3
+ Build tooling for the Uniweb Component Web Platform.
4
4
 
5
5
  ## Overview
6
6
 
7
- This package provides build utilities for creating Uniweb Foundations—React component libraries that define the vocabulary and rendering logic for content-driven websites.
7
+ This package provides Vite plugins and utilities for building both **Foundations** (component libraries) and **Sites** (content-driven websites).
8
8
 
9
9
  ## Installation
10
10
 
@@ -14,17 +14,23 @@ npm install @uniweb/build --save-dev
14
14
 
15
15
  ## Features
16
16
 
17
+ **For Foundations:**
17
18
  - **Component Discovery** - Automatically discovers components from `src/components/*/meta.js`
18
19
  - **Entry Generation** - Generates the foundation entry point with all exports
19
20
  - **Schema Building** - Creates `schema.json` with full component metadata for editors
20
- - **Image Processing** - Converts preview images to WebP format with dimension extraction
21
+ - **Image Processing** - Converts preview images to WebP format
21
22
  - **Vite Plugin** - Integrates seamlessly with Vite builds
22
23
 
24
+ **For Sites:**
25
+ - **Content Collection** - Collects pages from `pages/` directory with YAML/Markdown
26
+ - **Dev Server Integration** - Watches for content changes with hot reload
27
+ - **Foundation Dev Server** - Serves a local foundation during development
28
+
23
29
  ## Usage
24
30
 
25
- ### Vite Plugin
31
+ ### Foundation Plugin
26
32
 
27
- Add the foundation plugin to your `vite.config.js`:
33
+ Add the foundation plugin to your foundation's `vite.config.js`:
28
34
 
29
35
  ```js
30
36
  import { defineConfig } from 'vite'
@@ -49,6 +55,165 @@ export default defineConfig({
49
55
  })
50
56
  ```
51
57
 
58
+ ### Site Plugins
59
+
60
+ For sites, use the content and dev plugins in your site's `vite.config.js`:
61
+
62
+ ```js
63
+ import { defineConfig } from 'vite'
64
+ import react from '@vitejs/plugin-react'
65
+ import { siteContentPlugin } from '@uniweb/build/site'
66
+ import { foundationDevPlugin } from '@uniweb/build/dev'
67
+
68
+ export default defineConfig({
69
+ plugins: [
70
+ react(),
71
+
72
+ // Collect content from pages/ directory
73
+ siteContentPlugin({
74
+ sitePath: './',
75
+ inject: true, // Inject into HTML
76
+ }),
77
+
78
+ // Serve local foundation during development
79
+ foundationDevPlugin({
80
+ path: '../foundation',
81
+ serve: '/foundation',
82
+ }),
83
+ ]
84
+ })
85
+ ```
86
+
87
+ #### Site Content Plugin Options
88
+
89
+ ```js
90
+ siteContentPlugin({
91
+ sitePath: './', // Path to site directory
92
+ pagesDir: 'pages', // Pages subdirectory name
93
+ inject: true, // Inject content into HTML
94
+ filename: 'site-content.json', // Output filename
95
+ watch: true, // Watch for changes (dev mode)
96
+ seo: { // SEO configuration (optional)
97
+ baseUrl: 'https://example.com',
98
+ defaultImage: '/og-image.png',
99
+ twitterHandle: '@example',
100
+ locales: [
101
+ { code: 'en', default: true },
102
+ { code: 'es' }
103
+ ],
104
+ robots: {
105
+ disallow: ['/admin', '/api'],
106
+ crawlDelay: 1
107
+ }
108
+ },
109
+ assets: { // Asset processing (optional)
110
+ process: true, // Process assets in production (default: true)
111
+ convertToWebp: true, // Convert images to WebP (default: true)
112
+ quality: 80, // WebP quality 1-100 (default: 80)
113
+ outputDir: 'assets', // Output subdirectory (default: 'assets')
114
+ videoPosters: true, // Extract poster from videos (default: true, requires ffmpeg)
115
+ pdfThumbnails: true // Generate PDF thumbnails (default: true, requires pdf-lib)
116
+ }
117
+ })
118
+ ```
119
+
120
+ #### SEO Features
121
+
122
+ When `seo.baseUrl` is provided, the plugin generates:
123
+
124
+ **sitemap.xml** - Auto-generated from collected pages with:
125
+ - Last modified dates from file timestamps
126
+ - Per-page `changefreq` and `priority` from page frontmatter
127
+ - Hreflang entries for multi-locale sites
128
+
129
+ **robots.txt** - Generated with sitemap reference and optional rules
130
+
131
+ **Meta Tags** - Injected into HTML `<head>`:
132
+ - Open Graph tags (`og:title`, `og:description`, `og:image`, etc.)
133
+ - Twitter Card tags (`twitter:card`, `twitter:site`, etc.)
134
+ - Canonical URL
135
+ - Hreflang links for multi-locale sites
136
+
137
+ **Page-level SEO** - Configure in `page.yml`:
138
+ ```yaml
139
+ title: About Us
140
+ description: Learn about our company
141
+ seo:
142
+ noindex: false # Exclude from sitemap
143
+ image: /about-og.png # Page-specific OG image
144
+ changefreq: monthly # Sitemap changefreq
145
+ priority: 0.8 # Sitemap priority
146
+ ```
147
+
148
+ #### Asset Processing
149
+
150
+ The plugin automatically discovers and processes assets referenced in your content. In content-driven sites, markdown acts as "code" - local asset references are like implicit imports and get optimized during build.
151
+
152
+ **Supported path formats:**
153
+ - `./image.png` - Relative to the markdown file
154
+ - `../shared/logo.png` - Relative paths with parent traversal
155
+ - `/images/hero.png` - Absolute paths (resolved from `public/` or `assets/` folder)
156
+
157
+ **What gets processed:**
158
+ - Images in markdown content: `![Alt](./photo.jpg)`
159
+ - Media in frontmatter fields: `background`, `image`, `thumbnail`, `poster`, `avatar`, `logo`, `icon`, `video`, `pdf`, etc.
160
+
161
+ **Image processing:**
162
+ - PNG, JPG, JPEG, GIF → Converted to WebP for smaller file sizes
163
+ - SVG, WebP, AVIF → Copied as-is (already optimized formats)
164
+ - All processed assets get content-hashed filenames for cache busting
165
+
166
+ **Video poster extraction** (requires `ffmpeg` on system):
167
+ - MP4, WebM, MOV, AVI, MKV → Poster frame extracted at 1 second
168
+ - Poster images converted to WebP and added to `_assetMeta.posters`
169
+ - Skipped if an explicit `poster` attribute is provided in markdown
170
+
171
+ **PDF thumbnail generation** (requires `pdf-lib` package):
172
+ - PDF files → Placeholder thumbnail with page count
173
+ - Thumbnails added to `_assetMeta.thumbnails`
174
+ - Skipped if an explicit `preview` attribute is provided in markdown
175
+
176
+ **Explicit poster/preview images:**
177
+
178
+ When you provide explicit `poster` or `preview` attributes in your markdown, those images are collected and optimized alongside other assets:
179
+
180
+ ```markdown
181
+ ![Video](./intro.mp4){role=video poster=./custom-poster.jpg}
182
+ ![PDF](./guide.pdf){role=pdf preview=./guide-preview.png}
183
+ ```
184
+
185
+ - The explicit images (`./custom-poster.jpg`, `./guide-preview.png`) are processed and optimized
186
+ - Auto-generation via ffmpeg/pdf-lib is skipped for these files
187
+ - This gives you full control over preview images while still benefiting from optimization
188
+
189
+ **Build output:**
190
+ ```
191
+ dist/
192
+ ├── assets/
193
+ │ ├── hero-a1b2c3d4.webp # Converted from hero.jpg
194
+ │ ├── logo-e5f6g7h8.svg # Copied as-is
195
+ │ ├── intro-poster-9i0j1k2l.webp # Video poster frame
196
+ │ └── guide-thumb-3m4n5o6p.webp # PDF thumbnail
197
+ └── site-content.json # Paths rewritten, _assetMeta included
198
+ ```
199
+
200
+ **Graceful degradation:**
201
+ - If `ffmpeg` is not installed, video posters are silently skipped
202
+ - If `pdf-lib` is not installed, PDF thumbnails are silently skipped
203
+ - Missing assets are logged as warnings but don't fail the build
204
+
205
+ #### Foundation Dev Plugin Options
206
+
207
+ ```js
208
+ foundationDevPlugin({
209
+ name: 'foundation', // Name for logging
210
+ path: '../foundation', // Path to foundation package
211
+ serve: '/foundation', // URL path to serve from
212
+ watch: true, // Watch for source changes
213
+ buildOnStart: true // Build when dev server starts
214
+ })
215
+ ```
216
+
52
217
  ### Programmatic API
53
218
 
54
219
  ```js
@@ -161,10 +326,11 @@ After building, your foundation will contain:
161
326
  dist/
162
327
  ├── foundation.js # Bundled components (~6KB typical)
163
328
  ├── foundation.js.map # Source map
164
- ├── schema.json # Full metadata for editors
165
- └── assets/
166
- └── Hero/
167
- └── default.webp # Processed preview images
329
+ └── meta/ # Editor metadata (not needed at runtime)
330
+ ├── schema.json # Full component metadata for editors
331
+ └── previews/ # Preset preview images
332
+ └── Hero/
333
+ └── default.webp
168
334
  ```
169
335
 
170
336
  ## API Reference
@@ -194,16 +360,28 @@ dist/
194
360
 
195
361
  ### Vite Plugins
196
362
 
363
+ **Foundation plugins** (`@uniweb/build`):
364
+
197
365
  | Plugin | Description |
198
366
  |--------|-------------|
199
367
  | `foundationPlugin(options)` | Combined dev + build plugin |
200
368
  | `foundationBuildPlugin(options)` | Build-only plugin |
201
369
  | `foundationDevPlugin(options)` | Dev-only plugin with HMR |
202
370
 
371
+ **Site plugins** (`@uniweb/build/site` and `@uniweb/build/dev`):
372
+
373
+ | Plugin | Description |
374
+ |--------|-------------|
375
+ | `siteContentPlugin(options)` | Collect and inject site content |
376
+ | `collectSiteContent(sitePath)` | Programmatic content collection |
377
+ | `foundationDevPlugin(options)` | Serve foundation during site dev |
378
+
203
379
  ## Related Packages
204
380
 
205
- - [`uniweb`](https://github.com/uniweb/cli) - CLI for creating Uniweb projects
206
- - [`@uniweb/runtime`](https://github.com/uniweb/runtime) - Runtime loader for sites
381
+ - [`@uniweb/core`](https://github.com/uniweb/core) - Core classes (Uniweb, Website, Block)
382
+ - [`@uniweb/kit`](https://github.com/uniweb/kit) - Component library for foundations
383
+ - [`@uniweb/runtime`](https://github.com/uniweb/runtime) - Browser runtime for sites
384
+ - [`uniweb`](https://github.com/uniweb/cli) - CLI for creating projects
207
385
 
208
386
  ## License
209
387
 
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.1.2",
4
- "description": "Foundation build tooling for the Uniweb Component Web Platform",
3
+ "version": "0.1.4",
4
+ "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
8
8
  "./schema": "./src/schema.js",
9
9
  "./images": "./src/images.js",
10
10
  "./generate-entry": "./src/generate-entry.js",
11
- "./vite-plugin": "./src/vite-foundation-plugin.js"
11
+ "./vite-plugin": "./src/vite-foundation-plugin.js",
12
+ "./site": "./src/site/index.js",
13
+ "./dev": "./src/dev/index.js",
14
+ "./prerender": "./src/prerender.js"
12
15
  },
13
16
  "files": [
14
17
  "src"
@@ -16,6 +19,7 @@
16
19
  "keywords": [
17
20
  "uniweb",
18
21
  "foundation",
22
+ "site",
19
23
  "build",
20
24
  "vite",
21
25
  "components"
@@ -34,14 +38,30 @@
34
38
  "node": ">=20.19"
35
39
  },
36
40
  "dependencies": {
41
+ "js-yaml": "^4.1.0",
37
42
  "sharp": "^0.33.2"
38
43
  },
44
+ "optionalDependencies": {
45
+ "@uniweb/content-reader": "1.0.2"
46
+ },
39
47
  "peerDependencies": {
40
- "vite": "^5.0.0 || ^6.0.0"
48
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
49
+ "react": "^18.0.0 || ^19.0.0",
50
+ "react-dom": "^18.0.0 || ^19.0.0",
51
+ "@uniweb/core": "0.1.2"
41
52
  },
42
53
  "peerDependenciesMeta": {
43
54
  "vite": {
44
55
  "optional": true
56
+ },
57
+ "react": {
58
+ "optional": true
59
+ },
60
+ "react-dom": {
61
+ "optional": true
62
+ },
63
+ "@uniweb/core": {
64
+ "optional": true
45
65
  }
46
66
  }
47
67
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Development Build Tools
3
+ *
4
+ * Vite plugins for developing Uniweb sites with local foundations.
5
+ *
6
+ * @module @uniweb/build/dev
7
+ */
8
+
9
+ export { foundationDevPlugin, default } from './plugin.js'
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Vite Plugin: Foundation Dev Server
3
+ *
4
+ * Builds and serves a foundation within the site's dev server.
5
+ * This enables a single dev server for both site and foundation development.
6
+ *
7
+ * @module @uniweb/build/dev
8
+ *
9
+ * @example
10
+ * import { foundationDevPlugin } from '@uniweb/build/dev'
11
+ *
12
+ * export default defineConfig({
13
+ * plugins: [
14
+ * foundationDevPlugin({
15
+ * name: 'my-foundation',
16
+ * path: '../my-foundation', // Path to foundation package
17
+ * serve: '/foundation', // URL path to serve from
18
+ * })
19
+ * ]
20
+ * })
21
+ */
22
+
23
+ import { resolve, join } from 'node:path'
24
+ import { watch } from 'node:fs'
25
+ import { readFile } from 'node:fs/promises'
26
+ import { existsSync } from 'node:fs'
27
+ import { build } from 'vite'
28
+
29
+ /**
30
+ * Create the foundation dev plugin
31
+ *
32
+ * @param {Object} options
33
+ * @param {string} [options.name='foundation'] - Foundation name (for logging)
34
+ * @param {string} [options.path='../foundation'] - Path to foundation package
35
+ * @param {string} [options.serve='/foundation'] - URL path to serve from
36
+ * @param {boolean} [options.watch=true] - Watch for source changes
37
+ * @param {boolean} [options.buildOnStart=true] - Build on server start
38
+ */
39
+ export function foundationDevPlugin(options = {}) {
40
+ const {
41
+ name = 'foundation',
42
+ path: foundationPath = '../foundation',
43
+ serve: servePath = '/foundation',
44
+ watch: shouldWatch = true,
45
+ buildOnStart = true
46
+ } = options
47
+
48
+ let resolvedFoundationPath = null
49
+ let resolvedDistPath = null
50
+ let server = null
51
+ let watcher = null
52
+ let isBuilding = false
53
+ let lastBuildTime = 0
54
+
55
+ /**
56
+ * Build the foundation using Vite
57
+ */
58
+ async function buildFoundation() {
59
+ if (isBuilding) return
60
+ isBuilding = true
61
+
62
+ const startTime = Date.now()
63
+ console.log(`[foundation] Building ${name}...`)
64
+
65
+ try {
66
+ // Use Vite's native config loading by specifying configFile
67
+ const configPath = join(resolvedFoundationPath, 'vite.config.js')
68
+
69
+ // Build using Vite with the foundation's own config file
70
+ await build({
71
+ root: resolvedFoundationPath,
72
+ configFile: existsSync(configPath) ? configPath : false,
73
+ logLevel: 'warn',
74
+ build: {
75
+ outDir: 'dist',
76
+ emptyOutDir: true,
77
+ watch: null // Don't use Vite's watch, we handle it ourselves
78
+ }
79
+ })
80
+
81
+ lastBuildTime = Date.now()
82
+ console.log(`[foundation] Built ${name} in ${lastBuildTime - startTime}ms`)
83
+
84
+ // Trigger HMR reload if server is running
85
+ if (server) {
86
+ server.ws.send({ type: 'full-reload' })
87
+ }
88
+ } catch (err) {
89
+ console.error(`[foundation] Build failed:`, err.message)
90
+ } finally {
91
+ isBuilding = false
92
+ }
93
+ }
94
+
95
+ return {
96
+ name: 'uniweb:foundation-dev',
97
+ // Run before other plugins to intercept foundation requests
98
+ enforce: 'pre',
99
+
100
+ configResolved(config) {
101
+ resolvedFoundationPath = resolve(config.root, foundationPath)
102
+ resolvedDistPath = join(resolvedFoundationPath, 'dist')
103
+ },
104
+
105
+ async buildStart() {
106
+ if (buildOnStart) {
107
+ await buildFoundation()
108
+ }
109
+ },
110
+
111
+ configureServer(devServer) {
112
+ server = devServer
113
+
114
+ // Serve foundation files via middleware
115
+ // For JS files, use Vite's transform pipeline to properly resolve imports
116
+ devServer.middlewares.use(async (req, res, next) => {
117
+ const urlPath = req.url.split('?')[0]
118
+
119
+ if (!urlPath.startsWith(servePath)) {
120
+ return next()
121
+ }
122
+
123
+ const filePath = urlPath.slice(servePath.length) || '/foundation.js'
124
+ const fullPath = join(resolvedDistPath, filePath)
125
+
126
+ if (!existsSync(fullPath)) {
127
+ return next()
128
+ }
129
+
130
+ try {
131
+ let content = await readFile(fullPath, 'utf-8')
132
+ let contentType = 'application/octet-stream'
133
+
134
+ if (filePath.endsWith('.js')) {
135
+ contentType = 'application/javascript'
136
+
137
+ // Use Vite's transform pipeline to resolve bare imports
138
+ // This properly handles React ESM/CJS interop
139
+ const result = await devServer.transformRequest(`/@fs${fullPath}`, {
140
+ html: false
141
+ })
142
+
143
+ if (result) {
144
+ content = result.code
145
+ }
146
+ } else if (filePath.endsWith('.css')) {
147
+ contentType = 'text/css'
148
+ } else if (filePath.endsWith('.json')) {
149
+ contentType = 'application/json'
150
+ }
151
+
152
+ res.setHeader('Content-Type', contentType)
153
+ res.setHeader('Cache-Control', 'no-cache')
154
+ res.setHeader('Access-Control-Allow-Origin', '*')
155
+ res.end(content)
156
+ } catch (err) {
157
+ next(err)
158
+ }
159
+ })
160
+
161
+ // Watch foundation source for changes
162
+ if (shouldWatch) {
163
+ const srcPath = join(resolvedFoundationPath, 'src')
164
+
165
+ // Debounce rebuilds
166
+ let rebuildTimeout = null
167
+ const scheduleRebuild = () => {
168
+ if (rebuildTimeout) clearTimeout(rebuildTimeout)
169
+ rebuildTimeout = setTimeout(() => {
170
+ buildFoundation()
171
+ }, 200)
172
+ }
173
+
174
+ try {
175
+ watcher = watch(srcPath, { recursive: true }, (eventType, filename) => {
176
+ // Ignore non-source files
177
+ if (
178
+ filename &&
179
+ (filename.endsWith('.js') ||
180
+ filename.endsWith('.jsx') ||
181
+ filename.endsWith('.ts') ||
182
+ filename.endsWith('.tsx') ||
183
+ filename.endsWith('.css') ||
184
+ filename.endsWith('.svg'))
185
+ ) {
186
+ console.log(`[foundation] ${filename} changed`)
187
+ scheduleRebuild()
188
+ }
189
+ })
190
+ console.log(`[foundation] Watching ${srcPath}`)
191
+ } catch (err) {
192
+ console.warn(`[foundation] Could not watch source:`, err.message)
193
+ }
194
+ }
195
+ },
196
+
197
+ closeBundle() {
198
+ if (watcher) {
199
+ watcher.close()
200
+ watcher = null
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ export default foundationDevPlugin
package/src/images.js CHANGED
@@ -2,6 +2,8 @@
2
2
  * Image Processing Utilities
3
3
  *
4
4
  * Handles preview image discovery, conversion to webp, and metadata extraction.
5
+ * Preview images are editor metadata (for preset visualization) and are output
6
+ * to dist/meta/previews/ to keep them separate from runtime assets.
5
7
  */
6
8
 
7
9
  import { readdir, mkdir, copyFile } from 'node:fs/promises'
@@ -71,8 +73,8 @@ export async function processComponentPreviews(componentDir, componentName, outp
71
73
  return previews
72
74
  }
73
75
 
74
- // Create output directory
75
- const componentOutputDir = join(outputDir, 'assets', componentName)
76
+ // Create output directory for preview images (editor metadata)
77
+ const componentOutputDir = join(outputDir, 'meta', 'previews', componentName)
76
78
  await mkdir(componentOutputDir, { recursive: true })
77
79
 
78
80
  // Get all image files
@@ -115,7 +117,7 @@ export async function processComponentPreviews(componentDir, componentName, outp
115
117
  }
116
118
 
117
119
  previews[presetName] = {
118
- path: `assets/${componentName}/${outputFilename}`,
120
+ path: `meta/previews/${componentName}/${outputFilename}`,
119
121
  width: metadata.width,
120
122
  height: metadata.height,
121
123
  type: finalFormat,
package/src/index.js CHANGED
@@ -35,5 +35,10 @@ export {
35
35
  foundationDevPlugin,
36
36
  } from './vite-foundation-plugin.js'
37
37
 
38
+ // SSG Prerendering
39
+ export {
40
+ prerenderSite,
41
+ } from './prerender.js'
42
+
38
43
  // Default export is the combined Vite plugin
39
44
  export { default } from './vite-foundation-plugin.js'