@uniweb/build 0.7.3 → 0.7.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
@@ -387,7 +387,7 @@ The generated `_entry.generated.js` file exports:
387
387
  |--------|-------------|
388
388
  | `components` | Object map of component name → React component |
389
389
  | Named exports | Each component exported by name (e.g., `Hero`, `Features`) |
390
- | `capabilities` | Custom Layout and props from `src/exports.js` (or `null`) |
390
+ | `capabilities` | Custom Layout and props from `src/foundation.js` (or `null`) |
391
391
  | `meta` | Runtime metadata extracted from component `meta.js` files |
392
392
 
393
393
  #### Runtime Metadata (`meta` export)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -51,8 +51,8 @@
51
51
  },
52
52
  "optionalDependencies": {
53
53
  "@uniweb/content-reader": "1.1.2",
54
- "@uniweb/runtime": "0.6.0",
55
- "@uniweb/schemas": "0.2.1"
54
+ "@uniweb/schemas": "0.2.1",
55
+ "@uniweb/runtime": "0.6.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -61,7 +61,7 @@
61
61
  "@tailwindcss/vite": "^4.0.0",
62
62
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
63
63
  "vite-plugin-svgr": "^4.0.0",
64
- "@uniweb/core": "0.5.0"
64
+ "@uniweb/core": "0.5.1"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "vite": {
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Exports:
7
7
  * - `components` - Object map of component name -> React component
8
- * - `capabilities` - Custom Layout and props from src/exports.js (if present)
8
+ * - `capabilities` - Custom Layout and props from src/foundation.js (if present)
9
9
  * - `meta` - Per-component runtime metadata extracted from meta.js files
10
10
  *
11
11
  * The `meta` export contains only properties needed at runtime:
@@ -19,18 +19,16 @@
19
19
  * Foundation identity (name, description) comes from package.json in the editor schema.
20
20
  */
21
21
 
22
- import { writeFile, mkdir } from 'node:fs/promises'
22
+ import { writeFile, readFile, mkdir } from 'node:fs/promises'
23
23
  import { existsSync } from 'node:fs'
24
24
  import { join, dirname } from 'node:path'
25
25
  import { discoverComponents, discoverLayoutsInPath } from './schema.js'
26
26
  import { extractAllRuntimeSchemas, extractAllLayoutRuntimeSchemas } from './runtime-schema.js'
27
27
 
28
28
  /**
29
- * Detect foundation config/exports file (for props, vars, etc.)
29
+ * Detect foundation config file (for props, vars, etc.)
30
30
  *
31
- * Looks for (in order of preference):
32
- * 1. foundation.js - New consolidated format
33
- * 2. exports.js - Legacy format (for backward compatibility)
31
+ * Looks for: foundation.js or foundation.jsx
34
32
  *
35
33
  * The file should export:
36
34
  * - props (optional) - Foundation-wide props
@@ -40,29 +38,14 @@ import { extractAllRuntimeSchemas, extractAllLayoutRuntimeSchemas } from './runt
40
38
  * Note: Layout components are now discovered from src/layouts/
41
39
  */
42
40
  function detectFoundationExports(srcDir) {
43
- // Prefer foundation.js (new consolidated format)
44
- const foundationCandidates = [
41
+ const candidates = [
45
42
  { path: 'foundation.js', ext: 'js' },
46
43
  { path: 'foundation.jsx', ext: 'jsx' },
47
44
  ]
48
45
 
49
- for (const { path, ext } of foundationCandidates) {
46
+ for (const { path, ext } of candidates) {
50
47
  if (existsSync(join(srcDir, path))) {
51
- return { path: `./${path}`, ext, isFoundationJs: true }
52
- }
53
- }
54
-
55
- // Fall back to exports.js (legacy format)
56
- const legacyCandidates = [
57
- { path: 'exports.js', ext: 'js' },
58
- { path: 'exports.jsx', ext: 'jsx' },
59
- { path: 'exports/index.js', ext: 'js' },
60
- { path: 'exports/index.jsx', ext: 'jsx' },
61
- ]
62
-
63
- for (const { path, ext } of legacyCandidates) {
64
- if (existsSync(join(srcDir, path))) {
65
- return { path: `./${path.replace(/\/index\.(js|jsx)$/, '')}`, ext, isFoundationJs: false }
48
+ return { path: `./${path}`, ext }
66
49
  }
67
50
  }
68
51
  return null
@@ -132,13 +115,9 @@ function generateEntrySource(components, options = {}) {
132
115
 
133
116
  lines.push('')
134
117
 
135
- // Export components object
118
+ // Named exports — one per component
136
119
  if (componentNames.length > 0) {
137
- lines.push(`export const components = { ${componentNames.join(', ')} }`)
138
- lines.push('')
139
120
  lines.push(`export { ${componentNames.join(', ')} }`)
140
- } else {
141
- lines.push('export const components = {}')
142
121
  }
143
122
 
144
123
  // Foundation capabilities (props, vars, etc. + discovered layouts)
@@ -152,31 +131,23 @@ function generateEntrySource(components, options = {}) {
152
131
  capParts.push(`layouts: { ${layoutNames.join(', ')} }`)
153
132
  }
154
133
  lines.push(`const capabilities = { ${capParts.join(', ')} }`)
155
- lines.push('export { capabilities }')
156
134
  } else {
157
- lines.push('export const capabilities = null')
135
+ lines.push('const capabilities = null')
158
136
  }
159
137
 
160
- // Per-component metadata (defaults, context, initialState, background, data)
138
+ // Per-component runtime metadata (defaults, context, initialState, background, data)
161
139
  lines.push('')
162
- if (Object.keys(meta).length > 0) {
163
- const metaJson = JSON.stringify(meta, null, 2)
164
- lines.push(`// Per-component runtime metadata (from meta.js)`)
165
- lines.push(`export const meta = ${metaJson}`)
166
- } else {
167
- lines.push('export const meta = {}')
168
- }
140
+ const metaJson = JSON.stringify(Object.keys(meta).length > 0 ? meta : {}, null, 2)
141
+ lines.push(`const meta = ${metaJson}`)
169
142
 
170
143
  // Per-layout runtime metadata (areas, transitions, defaults)
171
144
  lines.push('')
172
- if (Object.keys(layoutMeta).length > 0) {
173
- const layoutMetaJson = JSON.stringify(layoutMeta, null, 2)
174
- lines.push(`// Per-layout runtime metadata (from meta.js)`)
175
- lines.push(`export const layoutMeta = ${layoutMetaJson}`)
176
- } else {
177
- lines.push('export const layoutMeta = {}')
178
- }
145
+ const layoutMetaJson = JSON.stringify(Object.keys(layoutMeta).length > 0 ? layoutMeta : {}, null, 2)
146
+ lines.push(`const layoutMeta = ${layoutMetaJson}`)
179
147
 
148
+ // Default export — non-component data (naturally unforgeable key)
149
+ lines.push('')
150
+ lines.push('export default { meta, capabilities, layoutMeta }')
180
151
  lines.push('')
181
152
 
182
153
  return lines.join('\n')
@@ -277,12 +248,23 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
277
248
  layoutMeta,
278
249
  })
279
250
 
280
- // Write to file
251
+ // Write to file (skip if content unchanged to avoid unnecessary watcher triggers)
281
252
  const output = outputPath || join(srcDir, '_entry.generated.js')
282
253
  await mkdir(dirname(output), { recursive: true })
283
- await writeFile(output, source, 'utf-8')
284
254
 
285
- console.log(`Generated entry point: ${output}`)
255
+ let written = false
256
+ if (existsSync(output)) {
257
+ const existing = await readFile(output, 'utf-8')
258
+ if (existing !== source) {
259
+ await writeFile(output, source, 'utf-8')
260
+ written = true
261
+ }
262
+ } else {
263
+ await writeFile(output, source, 'utf-8')
264
+ written = true
265
+ }
266
+
267
+ console.log(`${written ? 'Generated' : 'Unchanged'} entry point: ${output}`)
286
268
  console.log(` - ${componentNames.length} components: ${componentNames.join(', ')}`)
287
269
  if (layoutNames.length > 0) {
288
270
  console.log(` - ${layoutNames.length} layouts: ${layoutNames.join(', ')}`)
@@ -302,13 +284,75 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
302
284
  }
303
285
 
304
286
  /**
305
- * Check if entry point needs regeneration
306
- * (Compare discovered components with existing generated file)
287
+ * Check if a file change should trigger entry point regeneration.
288
+ *
289
+ * Used by both the foundation dev plugin and the site's bundled-mode plugin
290
+ * to decide when to re-run generateEntryPoint().
291
+ *
292
+ * The content-comparison guard in generateEntryPoint() makes false positives
293
+ * cheap (discovery runs but no write), so we err on the side of regenerating.
294
+ *
295
+ * @param {string} file - Absolute path of the changed file
296
+ * @param {string} srcDir - Foundation source directory (absolute)
297
+ * @returns {string|null} Reason string if regeneration needed, null otherwise
307
298
  */
308
- export async function shouldRegenerateEntry(srcDir, entryPath) {
309
- if (!existsSync(entryPath)) return true
299
+ export function shouldRegenerateForFile(file, srcDir) {
300
+ if (!file.startsWith(srcDir + '/')) return null
301
+
302
+ const rel = file.slice(srcDir.length + 1)
310
303
 
311
- // Could add more sophisticated checking here
312
- // For now, always regenerate during build
313
- return true
304
+ // meta.js anywhere affects runtime metadata
305
+ if (rel.endsWith('/meta.js') || rel === 'meta.js') {
306
+ return 'meta.js changed'
307
+ }
308
+
309
+ // foundation.js / foundation.jsx at root — affects capabilities import
310
+ if (/^foundation\.(js|jsx)$/.test(rel)) {
311
+ return 'foundation config changed'
312
+ }
313
+
314
+ // styles.css / index.css at root — affects CSS import line
315
+ if (/^(styles|index)\.css$/.test(rel)) {
316
+ return 'foundation styles changed'
317
+ }
318
+
319
+ // sections/ — relaxed discovery (bare files + entry files in PascalCase dirs)
320
+ if (rel.startsWith('sections/')) {
321
+ const inner = rel.slice('sections/'.length)
322
+ const parts = inner.split('/')
323
+
324
+ // Bare file at sections root: sections/Hero.jsx
325
+ if (parts.length === 1 && /^[A-Z].*\.(jsx|tsx|js|ts)$/.test(parts[0])) {
326
+ return `section file: ${parts[0]}`
327
+ }
328
+
329
+ // Entry file in a PascalCase directory: sections/Hero/index.jsx or sections/Hero/Hero.jsx
330
+ if (parts.length === 2 && /^[A-Z]/.test(parts[0]) && /\.(jsx|tsx|js|ts)$/.test(parts[1])) {
331
+ const base = parts[1].replace(/\.(jsx|tsx|js|ts)$/, '')
332
+ if (base === 'index' || base === parts[0]) {
333
+ return `section entry: ${inner}`
334
+ }
335
+ }
336
+ }
337
+
338
+ // layouts/ — bare files and entry files
339
+ if (rel.startsWith('layouts/')) {
340
+ const inner = rel.slice('layouts/'.length)
341
+ const parts = inner.split('/')
342
+
343
+ // Bare file at layouts root: layouts/docs.jsx
344
+ if (parts.length === 1 && /\.(jsx|tsx|js|ts)$/.test(parts[0])) {
345
+ return `layout file: ${parts[0]}`
346
+ }
347
+
348
+ // Entry file in a directory: layouts/docs/index.jsx or layouts/docs/docs.jsx
349
+ if (parts.length === 2 && /\.(jsx|tsx|js|ts)$/.test(parts[1])) {
350
+ const base = parts[1].replace(/\.(jsx|tsx|js|ts)$/, '')
351
+ if (base === 'index' || base === parts[0]) {
352
+ return `layout entry: ${inner}`
353
+ }
354
+ }
355
+ }
356
+
357
+ return null
314
358
  }
package/src/index.js CHANGED
@@ -17,7 +17,6 @@ export {
17
17
  // Entry point generation
18
18
  export {
19
19
  generateEntryPoint,
20
- shouldRegenerateEntry,
21
20
  } from './generate-entry.js'
22
21
 
23
22
  // Image processing
package/src/prerender.js CHANGED
@@ -786,8 +786,13 @@ export async function prerenderSite(siteDir, options = {}) {
786
786
  }
787
787
 
788
788
  // Set foundation capabilities (Layout, props, etc.)
789
- if (foundation.capabilities) {
790
- uniweb.setFoundationConfig(foundation.capabilities)
789
+ if (foundation.default?.capabilities) {
790
+ uniweb.setFoundationConfig(foundation.default.capabilities)
791
+ }
792
+
793
+ // Attach layout metadata (areas, transitions, defaults)
794
+ if (foundation.default?.layoutMeta && uniweb.foundationConfig) {
795
+ uniweb.foundationConfig.layoutMeta = foundation.default.layoutMeta
791
796
  }
792
797
 
793
798
  // Pre-fetch icons for SSR embedding
@@ -23,7 +23,7 @@
23
23
  import { existsSync, readFileSync } from 'node:fs'
24
24
  import { resolve, dirname, join } from 'node:path'
25
25
  import yaml from 'js-yaml'
26
- import { generateEntryPoint } from '../generate-entry.js'
26
+ import { generateEntryPoint, shouldRegenerateForFile } from '../generate-entry.js'
27
27
 
28
28
  /**
29
29
  * Normalize a base path for Vite compatibility
@@ -241,16 +241,16 @@ export async function defineSiteConfig(options = {}) {
241
241
  },
242
242
 
243
243
  configureServer(server) {
244
- // Watch foundation src for meta.js changes to regenerate entry
244
+ // Watch foundation src for structural changes that affect the entry
245
245
  const srcDir = join(foundationInfo.path, 'src')
246
246
  const entryPath = join(srcDir, '_entry.generated.js')
247
247
 
248
- server.watcher.add(join(srcDir, '**', 'meta.js'))
248
+ server.watcher.add(srcDir)
249
249
 
250
250
  server.watcher.on('all', async (event, path) => {
251
- // Regenerate entry when meta.js files change (new/deleted components)
252
- if (path.includes(srcDir) && path.endsWith('meta.js')) {
253
- console.log(`[site] Foundation meta.js changed, regenerating entry...`)
251
+ const reason = shouldRegenerateForFile(path, srcDir)
252
+ if (reason) {
253
+ console.log(`[site] Foundation ${reason}, regenerating entry...`)
254
254
  try {
255
255
  await generateEntryPoint(srcDir, entryPath)
256
256
  server.ws.send({ type: 'full-reload' })
@@ -10,7 +10,7 @@
10
10
  import { writeFile, mkdir } from 'node:fs/promises'
11
11
  import { join, resolve } from 'node:path'
12
12
  import { buildSchema } from './schema.js'
13
- import { generateEntryPoint } from './generate-entry.js'
13
+ import { generateEntryPoint, shouldRegenerateForFile } from './generate-entry.js'
14
14
  import { processAllPreviews } from './images.js'
15
15
 
16
16
  /**
@@ -120,33 +120,10 @@ export function foundationDevPlugin(options = {}) {
120
120
  },
121
121
 
122
122
  async handleHotUpdate({ file, server }) {
123
- const entryPath = join(resolvedSrcDir, entryFileName)
124
-
125
- // Regenerate entry when meta.js files change
126
- if (file.startsWith(resolvedSrcDir) && file.endsWith('/meta.js')) {
127
- console.log('Component meta.js changed, regenerating entry...')
128
- await generateEntryPoint(resolvedSrcDir, entryPath, { componentPaths })
129
- server.ws.send({ type: 'full-reload' })
130
- return
131
- }
132
-
133
- // Regenerate when component files are added/removed in sections/ root
134
- // (bare file discovery means any .jsx/.tsx/.js/.ts at sections root is a section type)
135
- const sectionsDir = join(resolvedSrcDir, 'sections')
136
- if (file.startsWith(sectionsDir)) {
137
- const relative = file.slice(sectionsDir.length + 1)
138
- // Direct child of sections/ (no further slashes) — could be a new/removed bare file
139
- if (!relative.includes('/') && /\.(jsx|tsx|js|ts)$/.test(relative)) {
140
- console.log('Section file changed, regenerating entry...')
141
- await generateEntryPoint(resolvedSrcDir, entryPath, { componentPaths })
142
- server.ws.send({ type: 'full-reload' })
143
- return
144
- }
145
- }
146
-
147
- // Also regenerate if exports.js changes
148
- if (file.endsWith('/exports.js') || file.endsWith('/exports.jsx')) {
149
- console.log('Foundation exports changed, regenerating entry...')
123
+ const reason = shouldRegenerateForFile(file, resolvedSrcDir)
124
+ if (reason) {
125
+ console.log(`[foundation] ${reason}, regenerating entry...`)
126
+ const entryPath = join(resolvedSrcDir, entryFileName)
150
127
  await generateEntryPoint(resolvedSrcDir, entryPath, { componentPaths })
151
128
  server.ws.send({ type: 'full-reload' })
152
129
  }