@uniweb/build 0.1.17 → 0.1.20

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
@@ -343,7 +343,6 @@ dist/
343
343
  | `loadComponentMeta(componentDir)` | Load meta file for a component |
344
344
  | `loadFoundationMeta(srcDir)` | Load foundation-level meta |
345
345
  | `buildSchema(srcDir)` | Build complete schema object |
346
- | `buildRuntimeConfig(srcDir)` | Build minimal runtime config |
347
346
 
348
347
  ### Entry Generation
349
348
 
@@ -351,6 +350,53 @@ dist/
351
350
  |----------|-------------|
352
351
  | `generateEntryPoint(srcDir, outputPath)` | Generate foundation entry file |
353
352
 
353
+ #### Generated Entry Exports
354
+
355
+ The generated `_entry.generated.js` file exports:
356
+
357
+ | Export | Description |
358
+ |--------|-------------|
359
+ | `components` | Object map of component name → React component |
360
+ | Named exports | Each component exported by name (e.g., `Hero`, `Features`) |
361
+ | `capabilities` | Custom Layout and props from `src/exports.js` (or `null`) |
362
+ | `meta` | Runtime metadata extracted from component `meta.js` files |
363
+
364
+ #### Runtime Metadata (`meta` export)
365
+
366
+ Some properties in `meta.js` are needed at runtime, not just editor-time. These are extracted into the `meta` export to keep them available without loading the full `schema.json`.
367
+
368
+ Currently extracted properties:
369
+ - `input` - Form input schemas (for components that accept user input)
370
+
371
+ Example `meta.js` with form schema:
372
+ ```javascript
373
+ export default {
374
+ title: 'Contact Form',
375
+ // ... editor-only properties ...
376
+
377
+ // This gets extracted to the runtime `meta` export
378
+ input: {
379
+ name: { type: 'text', label: 'Name', required: true },
380
+ email: { type: 'email', label: 'Email', required: true },
381
+ message: { type: 'textarea', label: 'Message' }
382
+ }
383
+ }
384
+ ```
385
+
386
+ Generated entry will include:
387
+ ```javascript
388
+ export const meta = {
389
+ "ContactForm": {
390
+ "input": {
391
+ "name": { "type": "text", "label": "Name", "required": true },
392
+ // ...
393
+ }
394
+ }
395
+ }
396
+ ```
397
+
398
+ To add more runtime properties, update `RUNTIME_META_KEYS` in `src/generate-entry.js`.
399
+
354
400
  ### Image Processing
355
401
 
356
402
  | Function | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.1.17",
3
+ "version": "0.1.20",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,7 +59,7 @@
59
59
  "@tailwindcss/vite": "^4.0.0",
60
60
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
61
61
  "vite-plugin-svgr": "^4.0.0",
62
- "@uniweb/core": "0.1.7"
62
+ "@uniweb/core": "0.1.8"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "vite": {
@@ -2,28 +2,43 @@
2
2
  * Foundation Entry Point Generator
3
3
  *
4
4
  * Auto-generates the foundation entry point based on discovered components.
5
- * The generated file exports components and runtime configuration.
5
+ *
6
+ * Exports:
7
+ * - `components` - Object map of component name -> React component
8
+ * - `capabilities` - Custom Layout and props from src/exports.js (if present)
9
+ * - `meta` - Runtime metadata extracted from component meta.js files
10
+ *
11
+ * The `meta` export contains properties from meta.js that are needed at runtime,
12
+ * not just editor-time. Currently this includes:
13
+ * - `input` - Form input schemas (for components that accept user input)
14
+ *
15
+ * Full component metadata lives in schema.json (for the visual editor).
16
+ * Only runtime-essential properties are extracted here to keep bundles small.
6
17
  */
7
18
 
8
19
  import { writeFile, mkdir } from 'node:fs/promises'
9
20
  import { existsSync } from 'node:fs'
10
21
  import { join, dirname } from 'node:path'
11
- import {
12
- discoverComponents,
13
- loadFoundationMeta,
14
- extractRuntimeConfig,
15
- } from './schema.js'
22
+ import { discoverComponents } from './schema.js'
23
+
24
+ /**
25
+ * Keys from meta.js that should be included in the runtime bundle.
26
+ * These are properties needed at runtime, not just editor-time.
27
+ *
28
+ * - input: Form schemas for components that accept user input
29
+ */
30
+ const RUNTIME_META_KEYS = ['input']
16
31
 
17
32
  /**
18
- * Detect runtime configuration file (for custom Layout, props, etc.)
19
- * Looks for: src/runtime.js, src/runtime.jsx, src/runtime/index.js, src/runtime/index.jsx
33
+ * Detect foundation exports file (for custom Layout, props, etc.)
34
+ * Looks for: src/exports.js, src/exports.jsx, src/exports/index.js, src/exports/index.jsx
20
35
  */
21
- function detectRuntimeExports(srcDir) {
36
+ function detectFoundationExports(srcDir) {
22
37
  const candidates = [
23
- { path: 'runtime.js', ext: 'js' },
24
- { path: 'runtime.jsx', ext: 'jsx' },
25
- { path: 'runtime/index.js', ext: 'js' },
26
- { path: 'runtime/index.jsx', ext: 'jsx' },
38
+ { path: 'exports.js', ext: 'js' },
39
+ { path: 'exports.jsx', ext: 'jsx' },
40
+ { path: 'exports/index.js', ext: 'js' },
41
+ { path: 'exports/index.jsx', ext: 'jsx' },
27
42
  ]
28
43
 
29
44
  for (const { path, ext } of candidates) {
@@ -48,117 +63,90 @@ function detectCssFile(srcDir) {
48
63
  return null
49
64
  }
50
65
 
66
+ /**
67
+ * Extract runtime-needed properties from component meta
68
+ */
69
+ function extractRuntimeMeta(componentsMeta) {
70
+ const meta = {}
71
+
72
+ for (const [name, componentMeta] of Object.entries(componentsMeta)) {
73
+ const extracted = {}
74
+ for (const key of RUNTIME_META_KEYS) {
75
+ if (componentMeta[key] !== undefined) {
76
+ extracted[key] = componentMeta[key]
77
+ }
78
+ }
79
+ if (Object.keys(extracted).length > 0) {
80
+ meta[name] = extracted
81
+ }
82
+ }
83
+
84
+ return meta
85
+ }
86
+
51
87
  /**
52
88
  * Generate the entry point source code
53
89
  */
54
- function generateEntrySource(componentNames, runtimeConfig, options = {}) {
55
- const { cssPath = null, componentExtensions = {}, runtimeExports = null } = options
90
+ function generateEntrySource(componentNames, options = {}) {
91
+ const { cssPath = null, componentExtensions = {}, foundationExports = null, runtimeMeta = {} } = options
56
92
 
57
- const imports = []
93
+ const lines = [
94
+ '// Auto-generated foundation entry point',
95
+ '// DO NOT EDIT - This file is regenerated during build',
96
+ ''
97
+ ]
58
98
 
59
99
  // CSS import
60
100
  if (cssPath) {
61
- imports.push(`import '${cssPath}'`)
101
+ lines.push(`import '${cssPath}'`)
62
102
  }
63
103
 
64
- // Runtime exports import (for custom Layout, props, etc.)
65
- if (runtimeExports) {
66
- imports.push(`import runtime from '${runtimeExports.path}'`)
104
+ // Foundation capabilities import (for custom Layout, props, etc.)
105
+ if (foundationExports) {
106
+ lines.push(`import capabilities from '${foundationExports.path}'`)
67
107
  }
68
108
 
69
109
  // Component imports (use detected extension or default to .js)
70
110
  for (const name of componentNames) {
71
111
  const ext = componentExtensions[name] || 'js'
72
- imports.push(`import ${name} from './components/${name}/index.${ext}'`)
112
+ lines.push(`import ${name} from './components/${name}/index.${ext}'`)
73
113
  }
74
114
 
75
- // Build components object
76
- const componentsObj = `const components = {\n ${componentNames.join(',\n ')},\n}`
77
-
78
- // Runtime config (serialized)
79
- const configStr = JSON.stringify(runtimeConfig, null, 2)
80
- .split('\n')
81
- .map((line, i) => (i === 0 ? line : ' ' + line))
82
- .join('\n')
83
-
84
- const runtimeConfigBlock = `
85
- // Runtime configuration (extracted from meta files)
86
- // Only includes properties needed at render time
87
- const runtimeConfig = ${configStr}`
88
-
89
- // Export functions
90
- const exportFunctions = `
91
- /**
92
- * Get a component by name
93
- */
94
- export function getComponent(name) {
95
- return components[name]
96
- }
97
-
98
- /**
99
- * List all available component names
100
- */
101
- export function listComponents() {
102
- return Object.keys(components)
103
- }
115
+ lines.push('')
104
116
 
105
- /**
106
- * Get runtime config for a specific component
107
- * Returns input schema and other render-time properties
108
- */
109
- export function getComponentConfig(name) {
110
- return runtimeConfig.components[name] || {}
111
- }
117
+ // Export components object
118
+ if (componentNames.length > 0) {
119
+ lines.push(`export const components = { ${componentNames.join(', ')} }`)
120
+ lines.push('')
121
+ lines.push(`export { ${componentNames.join(', ')} }`)
122
+ } else {
123
+ lines.push('export const components = {}')
124
+ }
112
125
 
113
- /**
114
- * Get foundation-level runtime config
115
- */
116
- export function getFoundationConfig() {
117
- return runtimeConfig.foundation
118
- }
126
+ // Foundation capabilities (Layout, props, etc.)
127
+ lines.push('')
128
+ if (foundationExports) {
129
+ lines.push('export { capabilities }')
130
+ } else {
131
+ lines.push('export const capabilities = null')
132
+ }
119
133
 
120
- /**
121
- * Get all component schemas (for compatibility)
122
- * Note: Full schemas are in schema.json, this only returns runtime-relevant config
123
- */
124
- export function getAllSchemas() {
125
- const schemas = {}
126
- for (const name of Object.keys(components)) {
127
- if (components[name].schema) {
128
- schemas[name] = components[name].schema
129
- }
134
+ // Runtime meta (form schemas, etc.) - only if non-empty
135
+ lines.push('')
136
+ if (Object.keys(runtimeMeta).length > 0) {
137
+ const metaJson = JSON.stringify(runtimeMeta, null, 2)
138
+ .split('\n')
139
+ .map((line, i) => (i === 0 ? line : line))
140
+ .join('\n')
141
+ lines.push(`// Runtime metadata (form schemas, etc.)`)
142
+ lines.push(`export const meta = ${metaJson}`)
143
+ } else {
144
+ lines.push('export const meta = {}')
130
145
  }
131
- return schemas
132
- }
133
146
 
134
- /**
135
- * Get schema for a specific component (for compatibility)
136
- */
137
- export function getSchema(name) {
138
- return components[name]?.schema
139
- }`
140
-
141
- // Named exports for direct imports
142
- const namedExports = componentNames.length > 0
143
- ? `\n// Named exports for direct imports\nexport { ${componentNames.join(', ')} }`
144
- : ''
145
-
146
- // Runtime exports (Layout, props, etc.)
147
- const runtimeExport = runtimeExports
148
- ? `\n// Runtime exports (Layout, props, etc.)\nexport { runtime }`
149
- : `\n// No runtime exports provided\nexport const runtime = null`
150
-
151
- return `// Auto-generated foundation entry point
152
- // DO NOT EDIT - This file is regenerated during build
153
-
154
- ${imports.join('\n')}
155
-
156
- ${componentsObj}
157
- ${runtimeConfigBlock}
158
- ${exportFunctions}
159
- ${namedExports}
160
- ${runtimeExport}
161
- `
147
+ lines.push('')
148
+
149
+ return lines.join('\n')
162
150
  }
163
151
 
164
152
  /**
@@ -178,7 +166,7 @@ function detectComponentExtension(srcDir, componentName) {
178
166
  * Generate the foundation entry point file
179
167
  */
180
168
  export async function generateEntryPoint(srcDir, outputPath = null) {
181
- // Discover components
169
+ // Discover components (includes meta from meta.js files)
182
170
  const components = await discoverComponents(srcDir)
183
171
  const componentNames = Object.keys(components).sort()
184
172
 
@@ -192,32 +180,21 @@ export async function generateEntryPoint(srcDir, outputPath = null) {
192
180
  componentExtensions[name] = detectComponentExtension(srcDir, name)
193
181
  }
194
182
 
195
- // Load foundation meta and build runtime config
196
- const foundationMeta = await loadFoundationMeta(srcDir)
197
-
198
- const runtimeConfig = {
199
- foundation: extractRuntimeConfig(foundationMeta),
200
- components: {},
201
- }
202
-
203
- for (const [name, meta] of Object.entries(components)) {
204
- const config = extractRuntimeConfig(meta)
205
- if (Object.keys(config).length > 0) {
206
- runtimeConfig.components[name] = config
207
- }
208
- }
209
-
210
183
  // Check for CSS file
211
184
  const cssPath = detectCssFile(srcDir)
212
185
 
213
- // Check for runtime exports (custom Layout, props, etc.)
214
- const runtimeExports = detectRuntimeExports(srcDir)
186
+ // Check for foundation exports (custom Layout, props, etc.)
187
+ const foundationExports = detectFoundationExports(srcDir)
188
+
189
+ // Extract runtime-needed meta (form schemas, etc.)
190
+ const runtimeMeta = extractRuntimeMeta(components)
215
191
 
216
192
  // Generate source
217
- const source = generateEntrySource(componentNames, runtimeConfig, {
193
+ const source = generateEntrySource(componentNames, {
218
194
  cssPath,
219
195
  componentExtensions,
220
- runtimeExports,
196
+ foundationExports,
197
+ runtimeMeta,
221
198
  })
222
199
 
223
200
  // Write to file
@@ -227,15 +204,14 @@ export async function generateEntryPoint(srcDir, outputPath = null) {
227
204
 
228
205
  console.log(`Generated entry point: ${output}`)
229
206
  console.log(` - ${componentNames.length} components: ${componentNames.join(', ')}`)
230
- if (runtimeExports) {
231
- console.log(` - Runtime exports found: ${runtimeExports.path}`)
207
+ if (foundationExports) {
208
+ console.log(` - Foundation exports found: ${foundationExports.path}`)
232
209
  }
233
210
 
234
211
  return {
235
212
  outputPath: output,
236
213
  componentNames,
237
- runtimeConfig,
238
- runtimeExports,
214
+ foundationExports,
239
215
  }
240
216
  }
241
217
 
package/src/index.js CHANGED
@@ -9,9 +9,7 @@ export {
9
9
  loadComponentMeta,
10
10
  loadFoundationMeta,
11
11
  discoverComponents,
12
- extractRuntimeConfig,
13
12
  buildSchema,
14
- buildRuntimeConfig,
15
13
  getExposedComponents,
16
14
  } from './schema.js'
17
15
 
package/src/prerender.js CHANGED
@@ -108,9 +108,9 @@ export async function prerenderSite(siteDir, options = {}) {
108
108
  const uniweb = createUniweb(siteContent)
109
109
  uniweb.setFoundation(foundation)
110
110
 
111
- // Check for foundation config (runtime is the new name, config/site are legacy)
112
- if (foundation.runtime || foundation.config || foundation.site) {
113
- uniweb.setFoundationConfig(foundation.runtime || foundation.config || foundation.site)
111
+ // Set foundation config if provided
112
+ if (foundation.runtime) {
113
+ uniweb.setFoundationConfig(foundation.runtime)
114
114
  }
115
115
 
116
116
  // Pre-render each page
@@ -185,11 +185,10 @@ function DefaultLayout({ header, body, footer }) {
185
185
 
186
186
  /**
187
187
  * Layout component for SSG
188
- * Supports foundation-provided custom Layout via site.Layout
188
+ * Supports foundation-provided custom Layout via runtime.Layout
189
189
  */
190
190
  function Layout({ page, website, foundation }) {
191
- // Check if foundation provides a custom Layout (runtime is the new name, site is legacy)
192
- const RemoteLayout = foundation.runtime?.Layout || foundation.site?.Layout || null
191
+ const RemoteLayout = foundation.runtime?.Layout || null
193
192
 
194
193
  // Get block groups from page
195
194
  const headerBlocks = page.getHeaderBlocks()
package/src/schema.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Schema Discovery and Loading Utilities
3
3
  *
4
- * Discovers component meta files, loads them, and extracts
5
- * runtime-relevant configuration.
4
+ * Discovers component meta files and loads them for schema.json generation.
5
+ * Schema data is for editor-time only, not runtime.
6
6
  */
7
7
 
8
8
  import { readdir } from 'node:fs/promises'
@@ -13,9 +13,6 @@ import { pathToFileURL } from 'node:url'
13
13
  // Meta file name (standardized to meta.js)
14
14
  const META_FILE_NAME = 'meta.js'
15
15
 
16
- // Keys that should be extracted for runtime (embedded in foundation.js)
17
- const RUNTIME_KEYS = ['input', 'props']
18
-
19
16
  /**
20
17
  * Load a meta.js file via dynamic import
21
18
  */
@@ -95,22 +92,6 @@ export async function discoverComponents(srcDir) {
95
92
  return components
96
93
  }
97
94
 
98
- /**
99
- * Extract runtime-relevant config from meta
100
- * Only includes keys that are needed at render time
101
- */
102
- export function extractRuntimeConfig(meta) {
103
- if (!meta) return {}
104
-
105
- const config = {}
106
- for (const key of RUNTIME_KEYS) {
107
- if (meta[key] !== undefined) {
108
- config[key] = meta[key]
109
- }
110
- }
111
- return config
112
- }
113
-
114
95
  /**
115
96
  * Build complete schema for a foundation
116
97
  * Returns { _self: foundationMeta, ComponentName: componentMeta, ... }
@@ -125,27 +106,6 @@ export async function buildSchema(srcDir) {
125
106
  }
126
107
  }
127
108
 
128
- /**
129
- * Build runtime config (minimal, for embedding in foundation.js)
130
- */
131
- export async function buildRuntimeConfig(srcDir) {
132
- const foundationMeta = await loadFoundationMeta(srcDir)
133
- const components = await discoverComponents(srcDir)
134
-
135
- const componentConfigs = {}
136
- for (const [name, meta] of Object.entries(components)) {
137
- const config = extractRuntimeConfig(meta)
138
- if (Object.keys(config).length > 0) {
139
- componentConfigs[name] = config
140
- }
141
- }
142
-
143
- return {
144
- foundation: extractRuntimeConfig(foundationMeta),
145
- components: componentConfigs,
146
- }
147
- }
148
-
149
109
  /**
150
110
  * Get list of exposed component names
151
111
  */
@@ -79,9 +79,14 @@ function isMarkdownFile(filename) {
79
79
 
80
80
  /**
81
81
  * Parse numeric prefix from filename (e.g., "1-hero.md" → { prefix: "1", name: "hero" })
82
+ * Supports:
83
+ * - Simple: "1", "2", "3"
84
+ * - Decimal ordering: "1.5" (between 1 and 2), "2.5" (between 2 and 3)
85
+ * - Hierarchy via comma: "1,1" (child of 1), "1,2" (second child of 1)
86
+ * - Mixed: "1.5,1" (child of section 1.5)
82
87
  */
83
88
  function parseNumericPrefix(filename) {
84
- const match = filename.match(/^(\d+(?:\.\d+)*)-?(.*)$/)
89
+ const match = filename.match(/^(\d+(?:[.,]\d+)*)-?(.*)$/)
85
90
  if (match) {
86
91
  return { prefix: match[1], name: match[2] || match[1] }
87
92
  }
@@ -89,7 +94,9 @@ function parseNumericPrefix(filename) {
89
94
  }
90
95
 
91
96
  /**
92
- * Compare filenames for sorting by numeric prefix
97
+ * Compare filenames for sorting by numeric prefix.
98
+ * Both . and , are treated as separators for sorting purposes.
99
+ * This ensures correct ordering: 1, 1,1, 1.5, 2, 2,1, etc.
93
100
  */
94
101
  function compareFilenames(a, b) {
95
102
  const { prefix: prefixA } = parseNumericPrefix(parse(a).name)
@@ -99,8 +106,8 @@ function compareFilenames(a, b) {
99
106
  if (!prefixA) return 1
100
107
  if (!prefixB) return -1
101
108
 
102
- const partsA = prefixA.split('.').map(Number)
103
- const partsB = prefixB.split('.').map(Number)
109
+ const partsA = prefixA.split(/[.,]/).map(Number)
110
+ const partsB = prefixB.split(/[.,]/).map(Number)
104
111
 
105
112
  for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
106
113
  const numA = partsA[i] ?? 0
@@ -155,7 +162,11 @@ async function processMarkdownFile(filePath, id, siteRoot) {
155
162
  }
156
163
 
157
164
  /**
158
- * Build section hierarchy from flat list
165
+ * Build section hierarchy from flat list.
166
+ * Hierarchy is determined by comma separators:
167
+ * - "1", "1.5", "2" → all top-level (dots are for ordering)
168
+ * - "1,1", "1,2" → children of section "1"
169
+ * - "1.5,1" → child of section "1.5"
159
170
  */
160
171
  function buildSectionHierarchy(sections) {
161
172
  const sectionMap = new Map()
@@ -166,15 +177,15 @@ function buildSectionHierarchy(sections) {
166
177
  sectionMap.set(section.id, section)
167
178
  }
168
179
 
169
- // Second pass: build hierarchy
180
+ // Second pass: build hierarchy (comma = hierarchy)
170
181
  for (const section of sections) {
171
- if (!section.id.includes('.')) {
182
+ if (!section.id.includes(',')) {
172
183
  topLevel.push(section)
173
184
  continue
174
185
  }
175
186
 
176
- const parts = section.id.split('.')
177
- const parentId = parts.slice(0, -1).join('.')
187
+ const parts = section.id.split(',')
188
+ const parentId = parts.slice(0, -1).join(',')
178
189
  const parent = sectionMap.get(parentId)
179
190
 
180
191
  if (parent) {
@@ -127,9 +127,9 @@ export function foundationDevPlugin(options = {}) {
127
127
  server.ws.send({ type: 'full-reload' })
128
128
  }
129
129
 
130
- // Also regenerate if runtime.js changes
131
- if (file.endsWith('/runtime.js') || file.endsWith('/runtime.jsx')) {
132
- console.log('Runtime exports changed, regenerating entry...')
130
+ // Also regenerate if exports.js changes
131
+ if (file.endsWith('/exports.js') || file.endsWith('/exports.jsx')) {
132
+ console.log('Foundation exports changed, regenerating entry...')
133
133
  const entryPath = join(resolvedSrcDir, entryFileName)
134
134
  await generateEntryPoint(resolvedSrcDir, entryPath)
135
135
  server.ws.send({ type: 'full-reload' })