@uniweb/build 0.1.23 → 0.1.25
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 +2 -2
- package/src/foundation/config.js +5 -1
- package/src/generate-entry.js +49 -57
- package/src/images.js +7 -14
- package/src/prerender.js +61 -6
- package/src/runtime-schema.js +125 -0
- package/src/schema.js +50 -11
- package/src/site/plugin.js +138 -6
- package/src/vite-foundation-plugin.js +12 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
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.
|
|
62
|
+
"@uniweb/core": "0.1.11"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"vite": {
|
package/src/foundation/config.js
CHANGED
|
@@ -48,6 +48,9 @@ const DEFAULT_EXTERNALS = [
|
|
|
48
48
|
* @param {Object} [options={}] - Configuration options
|
|
49
49
|
* @param {string} [options.entry] - Entry point path (default: 'src/_entry.generated.js')
|
|
50
50
|
* @param {string} [options.fileName] - Output file name (default: 'foundation')
|
|
51
|
+
* @param {string[]} [options.components] - Paths to search for components (relative to src/).
|
|
52
|
+
* Default: ['components']
|
|
53
|
+
* Example: ['components', 'components/sections']
|
|
51
54
|
* @param {string[]} [options.externals] - Additional packages to externalize
|
|
52
55
|
* @param {boolean} [options.includeDefaultExternals] - Include default externals (default: true)
|
|
53
56
|
* @param {Array} [options.plugins] - Additional Vite plugins
|
|
@@ -60,6 +63,7 @@ export async function defineFoundationConfig(options = {}) {
|
|
|
60
63
|
const {
|
|
61
64
|
entry = 'src/_entry.generated.js',
|
|
62
65
|
fileName = 'foundation',
|
|
66
|
+
components: componentPaths,
|
|
63
67
|
externals: additionalExternals = [],
|
|
64
68
|
includeDefaultExternals = true,
|
|
65
69
|
plugins: extraPlugins = [],
|
|
@@ -104,7 +108,7 @@ export async function defineFoundationConfig(options = {}) {
|
|
|
104
108
|
// Build the plugins array
|
|
105
109
|
// foundationPlugin handles entry generation and schema building
|
|
106
110
|
const plugins = [
|
|
107
|
-
foundationPlugin({ srcDir: 'src' }),
|
|
111
|
+
foundationPlugin({ srcDir: 'src', components: componentPaths }),
|
|
108
112
|
tailwind && tailwindcss(),
|
|
109
113
|
react(),
|
|
110
114
|
svgr(),
|
package/src/generate-entry.js
CHANGED
|
@@ -6,28 +6,24 @@
|
|
|
6
6
|
* Exports:
|
|
7
7
|
* - `components` - Object map of component name -> React component
|
|
8
8
|
* - `capabilities` - Custom Layout and props from src/exports.js (if present)
|
|
9
|
-
* - `meta` -
|
|
9
|
+
* - `meta` - Per-component runtime metadata extracted from meta.js files
|
|
10
10
|
*
|
|
11
|
-
* The `meta` export contains properties
|
|
12
|
-
*
|
|
13
|
-
* - `
|
|
11
|
+
* The `meta` export contains only properties needed at runtime:
|
|
12
|
+
* - `background` - Engine-level background image handling
|
|
13
|
+
* - `data` - CMS entity binding ({ type, limit })
|
|
14
|
+
* - `defaults` - Param default values
|
|
15
|
+
* - `context` - Static capabilities for cross-block coordination
|
|
16
|
+
* - `initialState` - Initial values for mutable block state
|
|
14
17
|
*
|
|
15
18
|
* Full component metadata lives in schema.json (for the visual editor).
|
|
16
|
-
*
|
|
19
|
+
* Foundation identity (name, description) comes from package.json in the editor schema.
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
22
|
import { writeFile, mkdir } from 'node:fs/promises'
|
|
20
23
|
import { existsSync } from 'node:fs'
|
|
21
24
|
import { join, dirname } from 'node:path'
|
|
22
25
|
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']
|
|
26
|
+
import { extractAllRuntimeSchemas } from './runtime-schema.js'
|
|
31
27
|
|
|
32
28
|
/**
|
|
33
29
|
* Detect foundation exports file (for custom Layout, props, etc.)
|
|
@@ -63,32 +59,21 @@ function detectCssFile(srcDir) {
|
|
|
63
59
|
return null
|
|
64
60
|
}
|
|
65
61
|
|
|
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
62
|
|
|
87
63
|
/**
|
|
88
64
|
* Generate the entry point source code
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} components - Map of componentName -> { name, path, ext, ...meta }
|
|
67
|
+
* @param {Object} options - Generation options
|
|
89
68
|
*/
|
|
90
|
-
function generateEntrySource(
|
|
91
|
-
const {
|
|
69
|
+
function generateEntrySource(components, options = {}) {
|
|
70
|
+
const {
|
|
71
|
+
cssPath = null,
|
|
72
|
+
foundationExports = null,
|
|
73
|
+
meta = {},
|
|
74
|
+
} = options
|
|
75
|
+
|
|
76
|
+
const componentNames = Object.keys(components).sort()
|
|
92
77
|
|
|
93
78
|
const lines = [
|
|
94
79
|
'// Auto-generated foundation entry point',
|
|
@@ -106,10 +91,10 @@ function generateEntrySource(componentNames, options = {}) {
|
|
|
106
91
|
lines.push(`import capabilities from '${foundationExports.path}'`)
|
|
107
92
|
}
|
|
108
93
|
|
|
109
|
-
// Component imports (use
|
|
94
|
+
// Component imports (use component's path and detected extension)
|
|
110
95
|
for (const name of componentNames) {
|
|
111
|
-
const ext =
|
|
112
|
-
lines.push(`import ${name} from '
|
|
96
|
+
const { path, ext = 'js' } = components[name]
|
|
97
|
+
lines.push(`import ${name} from './${path}/index.${ext}'`)
|
|
113
98
|
}
|
|
114
99
|
|
|
115
100
|
lines.push('')
|
|
@@ -131,14 +116,11 @@ function generateEntrySource(componentNames, options = {}) {
|
|
|
131
116
|
lines.push('export const capabilities = null')
|
|
132
117
|
}
|
|
133
118
|
|
|
134
|
-
//
|
|
119
|
+
// Per-component metadata (defaults, context, initialState, background, data)
|
|
135
120
|
lines.push('')
|
|
136
|
-
if (Object.keys(
|
|
137
|
-
const metaJson = JSON.stringify(
|
|
138
|
-
|
|
139
|
-
.map((line, i) => (i === 0 ? line : line))
|
|
140
|
-
.join('\n')
|
|
141
|
-
lines.push(`// Runtime metadata (form schemas, etc.)`)
|
|
121
|
+
if (Object.keys(meta).length > 0) {
|
|
122
|
+
const metaJson = JSON.stringify(meta, null, 2)
|
|
123
|
+
lines.push(`// Per-component runtime metadata (from meta.js)`)
|
|
142
124
|
lines.push(`export const meta = ${metaJson}`)
|
|
143
125
|
} else {
|
|
144
126
|
lines.push('export const meta = {}')
|
|
@@ -151,9 +133,12 @@ function generateEntrySource(componentNames, options = {}) {
|
|
|
151
133
|
|
|
152
134
|
/**
|
|
153
135
|
* Detect the index file extension for a component
|
|
136
|
+
*
|
|
137
|
+
* @param {string} srcDir - Source directory
|
|
138
|
+
* @param {string} componentPath - Relative path to component (e.g., 'components/Hero')
|
|
154
139
|
*/
|
|
155
|
-
function detectComponentExtension(srcDir,
|
|
156
|
-
const basePath = join(srcDir,
|
|
140
|
+
function detectComponentExtension(srcDir, componentPath) {
|
|
141
|
+
const basePath = join(srcDir, componentPath)
|
|
157
142
|
for (const ext of ['jsx', 'tsx', 'js', 'ts']) {
|
|
158
143
|
if (existsSync(join(basePath, `index.${ext}`))) {
|
|
159
144
|
return ext
|
|
@@ -164,20 +149,27 @@ function detectComponentExtension(srcDir, componentName) {
|
|
|
164
149
|
|
|
165
150
|
/**
|
|
166
151
|
* Generate the foundation entry point file
|
|
152
|
+
*
|
|
153
|
+
* @param {string} srcDir - Source directory
|
|
154
|
+
* @param {string} [outputPath] - Output file path (default: srcDir/_entry.generated.js)
|
|
155
|
+
* @param {Object} [options] - Options
|
|
156
|
+
* @param {string[]} [options.componentPaths] - Paths to search for components (relative to srcDir)
|
|
167
157
|
*/
|
|
168
|
-
export async function generateEntryPoint(srcDir, outputPath = null) {
|
|
158
|
+
export async function generateEntryPoint(srcDir, outputPath = null, options = {}) {
|
|
159
|
+
const { componentPaths } = options
|
|
160
|
+
|
|
169
161
|
// Discover components (includes meta from meta.js files)
|
|
170
|
-
const components = await discoverComponents(srcDir)
|
|
162
|
+
const components = await discoverComponents(srcDir, componentPaths)
|
|
171
163
|
const componentNames = Object.keys(components).sort()
|
|
172
164
|
|
|
173
165
|
if (componentNames.length === 0) {
|
|
174
166
|
console.warn('Warning: No exposed components found')
|
|
175
167
|
}
|
|
176
168
|
|
|
177
|
-
// Detect extensions for each component
|
|
178
|
-
const componentExtensions = {}
|
|
169
|
+
// Detect extensions for each component and add to component info
|
|
179
170
|
for (const name of componentNames) {
|
|
180
|
-
|
|
171
|
+
const component = components[name]
|
|
172
|
+
component.ext = detectComponentExtension(srcDir, component.path)
|
|
181
173
|
}
|
|
182
174
|
|
|
183
175
|
// Check for CSS file
|
|
@@ -186,15 +178,14 @@ export async function generateEntryPoint(srcDir, outputPath = null) {
|
|
|
186
178
|
// Check for foundation exports (custom Layout, props, etc.)
|
|
187
179
|
const foundationExports = detectFoundationExports(srcDir)
|
|
188
180
|
|
|
189
|
-
// Extract
|
|
190
|
-
const
|
|
181
|
+
// Extract per-component runtime metadata from meta.js files
|
|
182
|
+
const meta = extractAllRuntimeSchemas(components)
|
|
191
183
|
|
|
192
184
|
// Generate source
|
|
193
|
-
const source = generateEntrySource(
|
|
185
|
+
const source = generateEntrySource(components, {
|
|
194
186
|
cssPath,
|
|
195
|
-
componentExtensions,
|
|
196
187
|
foundationExports,
|
|
197
|
-
|
|
188
|
+
meta,
|
|
198
189
|
})
|
|
199
190
|
|
|
200
191
|
// Write to file
|
|
@@ -212,6 +203,7 @@ export async function generateEntryPoint(srcDir, outputPath = null) {
|
|
|
212
203
|
outputPath: output,
|
|
213
204
|
componentNames,
|
|
214
205
|
foundationExports,
|
|
206
|
+
meta,
|
|
215
207
|
}
|
|
216
208
|
}
|
|
217
209
|
|
package/src/images.js
CHANGED
|
@@ -132,28 +132,21 @@ export async function processComponentPreviews(componentDir, componentName, outp
|
|
|
132
132
|
*
|
|
133
133
|
* @param {string} srcDir - Source directory (e.g., src/)
|
|
134
134
|
* @param {string} outputDir - Output directory (e.g., dist/)
|
|
135
|
-
* @param {Object} schema - Schema object
|
|
135
|
+
* @param {Object} schema - Schema object with components (each has `path` property)
|
|
136
136
|
* @param {boolean} isProduction - Whether to convert to webp
|
|
137
137
|
* @returns {Object} Updated schema with image references
|
|
138
138
|
*/
|
|
139
139
|
export async function processAllPreviews(srcDir, outputDir, schema, isProduction = true) {
|
|
140
|
-
const componentsDir = join(srcDir, 'components')
|
|
141
|
-
|
|
142
|
-
if (!existsSync(componentsDir)) {
|
|
143
|
-
return schema
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const entries = await readdir(componentsDir, { withFileTypes: true })
|
|
147
140
|
let totalImages = 0
|
|
148
141
|
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
// Iterate through components in schema (skip _self)
|
|
143
|
+
for (const [componentName, componentMeta] of Object.entries(schema)) {
|
|
144
|
+
if (componentName === '_self') continue
|
|
145
|
+
if (!componentMeta.path) continue
|
|
151
146
|
|
|
152
|
-
const
|
|
153
|
-
const componentDir = join(componentsDir, componentName)
|
|
147
|
+
const componentDir = join(srcDir, componentMeta.path)
|
|
154
148
|
|
|
155
|
-
|
|
156
|
-
if (!schema[componentName]) continue
|
|
149
|
+
if (!existsSync(componentDir)) continue
|
|
157
150
|
|
|
158
151
|
// Process preview images
|
|
159
152
|
const previews = await processComponentPreviews(
|
package/src/prerender.js
CHANGED
|
@@ -263,6 +263,52 @@ function PageRenderer({ page, foundation }) {
|
|
|
263
263
|
)
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Guarantee content structure exists (mirrors runtime/prepare-props.js)
|
|
268
|
+
* Returns a content object with all standard paths guaranteed to exist
|
|
269
|
+
*/
|
|
270
|
+
function guaranteeContentStructure(parsedContent) {
|
|
271
|
+
const content = parsedContent || {}
|
|
272
|
+
|
|
273
|
+
// Spread content first, then override with guaranteed structure
|
|
274
|
+
// This ensures main.body.paragraphs etc. are always arrays
|
|
275
|
+
return {
|
|
276
|
+
// Preserve any additional fields from parser
|
|
277
|
+
...content,
|
|
278
|
+
// Main content section (override with guarantees)
|
|
279
|
+
main: {
|
|
280
|
+
header: {
|
|
281
|
+
title: content.main?.header?.title || '',
|
|
282
|
+
pretitle: content.main?.header?.pretitle || '',
|
|
283
|
+
subtitle: content.main?.header?.subtitle || '',
|
|
284
|
+
},
|
|
285
|
+
body: {
|
|
286
|
+
paragraphs: content.main?.body?.paragraphs || [],
|
|
287
|
+
links: content.main?.body?.links || [],
|
|
288
|
+
imgs: content.main?.body?.imgs || [],
|
|
289
|
+
lists: content.main?.body?.lists || [],
|
|
290
|
+
icons: content.main?.body?.icons || [],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
// Content items (H3 sections)
|
|
294
|
+
items: content.items || [],
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Apply param defaults from runtime schema
|
|
300
|
+
*/
|
|
301
|
+
function applyDefaults(params, defaults) {
|
|
302
|
+
if (!defaults || Object.keys(defaults).length === 0) {
|
|
303
|
+
return params || {}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
...defaults,
|
|
308
|
+
...(params || {}),
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
266
312
|
/**
|
|
267
313
|
* Block renderer - maps block to foundation component
|
|
268
314
|
*/
|
|
@@ -290,13 +336,24 @@ function BlockRenderer({ block, foundation }) {
|
|
|
290
336
|
)
|
|
291
337
|
}
|
|
292
338
|
|
|
293
|
-
//
|
|
294
|
-
|
|
339
|
+
// Get runtime schema for defaults (from foundation.runtimeSchema)
|
|
340
|
+
const runtimeSchema = foundation.runtimeSchema || {}
|
|
341
|
+
const schema = runtimeSchema[componentName] || null
|
|
342
|
+
const defaults = schema?.defaults || {}
|
|
343
|
+
|
|
344
|
+
// Build content and params with runtime guarantees (same as runtime's BlockRenderer)
|
|
345
|
+
let content, params
|
|
295
346
|
if (block.parsedContent?.raw) {
|
|
347
|
+
// Simple PoC format - content was passed directly
|
|
296
348
|
content = block.parsedContent.raw
|
|
349
|
+
params = block.properties
|
|
297
350
|
} else {
|
|
351
|
+
// Apply param defaults from meta.js
|
|
352
|
+
params = applyDefaults(block.properties, defaults)
|
|
353
|
+
|
|
354
|
+
// Guarantee content structure + merge with properties for backward compat
|
|
298
355
|
content = {
|
|
299
|
-
...block.parsedContent,
|
|
356
|
+
...guaranteeContentStructure(block.parsedContent),
|
|
300
357
|
...block.properties,
|
|
301
358
|
_prosemirror: block.parsedContent
|
|
302
359
|
}
|
|
@@ -313,10 +370,8 @@ function BlockRenderer({ block, foundation }) {
|
|
|
313
370
|
// Component props
|
|
314
371
|
const componentProps = {
|
|
315
372
|
content,
|
|
316
|
-
params
|
|
373
|
+
params,
|
|
317
374
|
block,
|
|
318
|
-
page: globalThis.uniweb?.activeWebsite?.activePage,
|
|
319
|
-
website: globalThis.uniweb?.activeWebsite,
|
|
320
375
|
input: block.input
|
|
321
376
|
}
|
|
322
377
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Schema Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts lean runtime-relevant metadata from full meta.js files.
|
|
5
|
+
* The runtime schema is optimized for size and contains only what's
|
|
6
|
+
* needed at render time:
|
|
7
|
+
*
|
|
8
|
+
* - background: boolean for engine-level background handling
|
|
9
|
+
* - data: { type, limit } for CMS entity binding
|
|
10
|
+
* - defaults: param default values
|
|
11
|
+
* - context: static capabilities for cross-block coordination
|
|
12
|
+
* - initialState: initial values for mutable block state
|
|
13
|
+
*
|
|
14
|
+
* Full metadata (titles, descriptions, hints, etc.) stays in schema.json
|
|
15
|
+
* for the visual editor.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse data string into structured object
|
|
20
|
+
* 'events' -> { type: 'events', limit: null }
|
|
21
|
+
* 'events:6' -> { type: 'events', limit: 6 }
|
|
22
|
+
*
|
|
23
|
+
* @param {string} dataString
|
|
24
|
+
* @returns {{ type: string, limit: number|null }}
|
|
25
|
+
*/
|
|
26
|
+
function parseDataString(dataString) {
|
|
27
|
+
if (!dataString || typeof dataString !== 'string') {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const [type, limitStr] = dataString.split(':')
|
|
32
|
+
return {
|
|
33
|
+
type: type.trim(),
|
|
34
|
+
limit: limitStr ? parseInt(limitStr, 10) : null,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract param defaults from params object
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} params - The params object from meta.js
|
|
42
|
+
* @returns {Object|null} - Object of { paramName: defaultValue } or null if empty
|
|
43
|
+
*/
|
|
44
|
+
function extractParamDefaults(params) {
|
|
45
|
+
if (!params || typeof params !== 'object') {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaults = {}
|
|
50
|
+
|
|
51
|
+
for (const [key, param] of Object.entries(params)) {
|
|
52
|
+
if (param && typeof param === 'object' && param.default !== undefined) {
|
|
53
|
+
defaults[key] = param.default
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Object.keys(defaults).length > 0 ? defaults : null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract lean runtime schema from a full meta.js object
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} fullMeta - The full meta.js default export
|
|
64
|
+
* @returns {Object|null} - Lean runtime schema or null if empty
|
|
65
|
+
*/
|
|
66
|
+
export function extractRuntimeSchema(fullMeta) {
|
|
67
|
+
if (!fullMeta || typeof fullMeta !== 'object') {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const runtime = {}
|
|
72
|
+
|
|
73
|
+
// Background handling (boolean or 'auto'/'manual')
|
|
74
|
+
if (fullMeta.background) {
|
|
75
|
+
runtime.background = fullMeta.background
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Data binding (CMS entities)
|
|
79
|
+
if (fullMeta.data) {
|
|
80
|
+
const parsed = parseDataString(fullMeta.data)
|
|
81
|
+
if (parsed) {
|
|
82
|
+
runtime.data = parsed
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Param defaults - support both v2 'params' and v1 'properties'
|
|
87
|
+
const paramsObj = fullMeta.params || fullMeta.properties
|
|
88
|
+
const defaults = extractParamDefaults(paramsObj)
|
|
89
|
+
if (defaults) {
|
|
90
|
+
runtime.defaults = defaults
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Context - static capabilities for cross-block coordination
|
|
94
|
+
// e.g., { allowTranslucentTop: true } for Hero components
|
|
95
|
+
if (fullMeta.context && typeof fullMeta.context === 'object') {
|
|
96
|
+
runtime.context = fullMeta.context
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Initial state - default values for mutable block state
|
|
100
|
+
// e.g., { expanded: false } for accordion-like components
|
|
101
|
+
if (fullMeta.initialState && typeof fullMeta.initialState === 'object') {
|
|
102
|
+
runtime.initialState = fullMeta.initialState
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Object.keys(runtime).length > 0 ? runtime : null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract runtime schemas for all components
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} componentsMeta - Map of componentName -> meta.js content
|
|
112
|
+
* @returns {Object} - Map of componentName -> runtime schema (excludes null entries)
|
|
113
|
+
*/
|
|
114
|
+
export function extractAllRuntimeSchemas(componentsMeta) {
|
|
115
|
+
const schemas = {}
|
|
116
|
+
|
|
117
|
+
for (const [name, meta] of Object.entries(componentsMeta)) {
|
|
118
|
+
const schema = extractRuntimeSchema(meta)
|
|
119
|
+
if (schema) {
|
|
120
|
+
schemas[name] = schema
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return schemas
|
|
125
|
+
}
|
package/src/schema.js
CHANGED
|
@@ -13,6 +13,9 @@ 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
|
+
// Default component paths (relative to srcDir)
|
|
17
|
+
const DEFAULT_COMPONENT_PATHS = ['components']
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* Load a meta.js file via dynamic import
|
|
18
21
|
*/
|
|
@@ -57,23 +60,25 @@ export async function loadFoundationMeta(srcDir) {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
|
-
* Discover
|
|
61
|
-
*
|
|
63
|
+
* Discover components in a single path
|
|
64
|
+
* @param {string} srcDir - Source directory (e.g., 'src')
|
|
65
|
+
* @param {string} relativePath - Path relative to srcDir (e.g., 'components' or 'components/sections')
|
|
66
|
+
* @returns {Object} Map of componentName -> { name, path, ...meta }
|
|
62
67
|
*/
|
|
63
|
-
|
|
64
|
-
const
|
|
68
|
+
async function discoverComponentsInPath(srcDir, relativePath) {
|
|
69
|
+
const fullPath = join(srcDir, relativePath)
|
|
65
70
|
|
|
66
|
-
if (!existsSync(
|
|
71
|
+
if (!existsSync(fullPath)) {
|
|
67
72
|
return {}
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
const entries = await readdir(
|
|
75
|
+
const entries = await readdir(fullPath, { withFileTypes: true })
|
|
71
76
|
const components = {}
|
|
72
77
|
|
|
73
78
|
for (const entry of entries) {
|
|
74
79
|
if (!entry.isDirectory()) continue
|
|
75
80
|
|
|
76
|
-
const componentDir = join(
|
|
81
|
+
const componentDir = join(fullPath, entry.name)
|
|
77
82
|
const result = await loadComponentMeta(componentDir)
|
|
78
83
|
|
|
79
84
|
if (result && result.meta) {
|
|
@@ -84,6 +89,7 @@ export async function discoverComponents(srcDir) {
|
|
|
84
89
|
|
|
85
90
|
components[entry.name] = {
|
|
86
91
|
name: entry.name,
|
|
92
|
+
path: join(relativePath, entry.name), // e.g., 'components/Hero' or 'components/sections/Hero'
|
|
87
93
|
...result.meta,
|
|
88
94
|
}
|
|
89
95
|
}
|
|
@@ -92,13 +98,43 @@ export async function discoverComponents(srcDir) {
|
|
|
92
98
|
return components
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Discover all exposed components in a foundation
|
|
103
|
+
*
|
|
104
|
+
* @param {string} srcDir - Source directory (e.g., 'src')
|
|
105
|
+
* @param {string[]} [componentPaths] - Paths to search for components (relative to srcDir)
|
|
106
|
+
* Default: ['components']
|
|
107
|
+
* @returns {Object} Map of componentName -> { name, path, ...meta }
|
|
108
|
+
*/
|
|
109
|
+
export async function discoverComponents(srcDir, componentPaths = DEFAULT_COMPONENT_PATHS) {
|
|
110
|
+
const components = {}
|
|
111
|
+
|
|
112
|
+
for (const relativePath of componentPaths) {
|
|
113
|
+
const found = await discoverComponentsInPath(srcDir, relativePath)
|
|
114
|
+
|
|
115
|
+
for (const [name, meta] of Object.entries(found)) {
|
|
116
|
+
if (components[name]) {
|
|
117
|
+
// Component already found in an earlier path - skip (first wins)
|
|
118
|
+
console.warn(`Warning: Component "${name}" found in multiple paths. Using ${components[name].path}, ignoring ${meta.path}`)
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
components[name] = meta
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return components
|
|
126
|
+
}
|
|
127
|
+
|
|
95
128
|
/**
|
|
96
129
|
* Build complete schema for a foundation
|
|
97
130
|
* Returns { _self: foundationMeta, ComponentName: componentMeta, ... }
|
|
131
|
+
*
|
|
132
|
+
* @param {string} srcDir - Source directory
|
|
133
|
+
* @param {string[]} [componentPaths] - Paths to search for components
|
|
98
134
|
*/
|
|
99
|
-
export async function buildSchema(srcDir) {
|
|
135
|
+
export async function buildSchema(srcDir, componentPaths) {
|
|
100
136
|
const foundationMeta = await loadFoundationMeta(srcDir)
|
|
101
|
-
const components = await discoverComponents(srcDir)
|
|
137
|
+
const components = await discoverComponents(srcDir, componentPaths)
|
|
102
138
|
|
|
103
139
|
return {
|
|
104
140
|
_self: foundationMeta,
|
|
@@ -108,8 +144,11 @@ export async function buildSchema(srcDir) {
|
|
|
108
144
|
|
|
109
145
|
/**
|
|
110
146
|
* Get list of exposed component names
|
|
147
|
+
*
|
|
148
|
+
* @param {string} srcDir - Source directory
|
|
149
|
+
* @param {string[]} [componentPaths] - Paths to search for components
|
|
111
150
|
*/
|
|
112
|
-
export async function getExposedComponents(srcDir) {
|
|
113
|
-
const components = await discoverComponents(srcDir)
|
|
151
|
+
export async function getExposedComponents(srcDir, componentPaths) {
|
|
152
|
+
const components = await discoverComponents(srcDir, componentPaths)
|
|
114
153
|
return Object.keys(components)
|
|
115
154
|
}
|
package/src/site/plugin.js
CHANGED
|
@@ -30,12 +30,14 @@
|
|
|
30
30
|
* })
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { resolve } from 'node:path'
|
|
34
|
-
import { watch } from 'node:fs'
|
|
33
|
+
import { resolve, join } from 'node:path'
|
|
34
|
+
import { watch, existsSync } from 'node:fs'
|
|
35
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
35
36
|
import { collectSiteContent } from './content-collector.js'
|
|
36
37
|
import { processAssets, rewriteSiteContentPaths } from './asset-processor.js'
|
|
37
38
|
import { processAdvancedAssets } from './advanced-processors.js'
|
|
38
39
|
import { generateSearchIndex, isSearchEnabled, getSearchIndexFilename } from '../search/index.js'
|
|
40
|
+
import { mergeTranslations } from '../i18n/merge.js'
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Generate sitemap.xml content
|
|
@@ -269,6 +271,62 @@ export function siteContentPlugin(options = {}) {
|
|
|
269
271
|
let isProduction = false
|
|
270
272
|
let watcher = null
|
|
271
273
|
let server = null
|
|
274
|
+
let localeTranslations = {} // Cache: { locale: translations }
|
|
275
|
+
let localesDir = 'locales' // Default, updated from site config
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Load translations for a specific locale
|
|
279
|
+
*/
|
|
280
|
+
async function loadLocaleTranslations(locale) {
|
|
281
|
+
if (localeTranslations[locale]) {
|
|
282
|
+
return localeTranslations[locale]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const localePath = join(resolvedSitePath, localesDir, `${locale}.json`)
|
|
286
|
+
if (!existsSync(localePath)) {
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const content = await readFile(localePath, 'utf-8')
|
|
292
|
+
const translations = JSON.parse(content)
|
|
293
|
+
localeTranslations[locale] = translations
|
|
294
|
+
return translations
|
|
295
|
+
} catch {
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get available locales from locales directory
|
|
302
|
+
*/
|
|
303
|
+
async function getAvailableLocales() {
|
|
304
|
+
const localesPath = join(resolvedSitePath, localesDir)
|
|
305
|
+
if (!existsSync(localesPath)) {
|
|
306
|
+
return []
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const files = await readdir(localesPath)
|
|
311
|
+
return files
|
|
312
|
+
.filter(f => f.endsWith('.json') && f !== 'manifest.json' && !f.startsWith('_'))
|
|
313
|
+
.map(f => f.replace('.json', ''))
|
|
314
|
+
} catch {
|
|
315
|
+
return []
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get translated content for a locale
|
|
321
|
+
*/
|
|
322
|
+
async function getTranslatedContent(locale) {
|
|
323
|
+
if (!siteContent) return null
|
|
324
|
+
|
|
325
|
+
const translations = await loadLocaleTranslations(locale)
|
|
326
|
+
if (!translations) return null
|
|
327
|
+
|
|
328
|
+
return mergeTranslations(siteContent, translations)
|
|
329
|
+
}
|
|
272
330
|
|
|
273
331
|
return {
|
|
274
332
|
name: 'uniweb:site-content',
|
|
@@ -284,6 +342,14 @@ export function siteContentPlugin(options = {}) {
|
|
|
284
342
|
try {
|
|
285
343
|
siteContent = await collectSiteContent(resolvedSitePath)
|
|
286
344
|
console.log(`[site-content] Collected ${siteContent.pages?.length || 0} pages`)
|
|
345
|
+
|
|
346
|
+
// Update localesDir from site config
|
|
347
|
+
if (siteContent.config?.i18n?.localesDir) {
|
|
348
|
+
localesDir = siteContent.config.i18n.localesDir
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Clear translation cache on rebuild
|
|
352
|
+
localeTranslations = {}
|
|
287
353
|
} catch (err) {
|
|
288
354
|
console.error('[site-content] Failed to collect content:', err.message)
|
|
289
355
|
siteContent = { config: {}, theme: {}, pages: [] }
|
|
@@ -346,14 +412,59 @@ export function siteContentPlugin(options = {}) {
|
|
|
346
412
|
watcher = { close: () => watchers.forEach(w => w.close()) }
|
|
347
413
|
}
|
|
348
414
|
|
|
415
|
+
// Watch locales directory for translation changes
|
|
416
|
+
const localesPath = resolve(resolvedSitePath, localesDir)
|
|
417
|
+
if (existsSync(localesPath)) {
|
|
418
|
+
try {
|
|
419
|
+
const localeWatcher = watch(localesPath, { recursive: false }, () => {
|
|
420
|
+
console.log('[site-content] Translation files changed, clearing cache...')
|
|
421
|
+
localeTranslations = {}
|
|
422
|
+
server.ws.send({ type: 'full-reload' })
|
|
423
|
+
})
|
|
424
|
+
if (watcher) {
|
|
425
|
+
const originalClose = watcher.close
|
|
426
|
+
watcher.close = () => {
|
|
427
|
+
originalClose()
|
|
428
|
+
localeWatcher.close()
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
console.log(`[site-content] Watching ${localesPath} for translation changes`)
|
|
432
|
+
} catch (err) {
|
|
433
|
+
// locales dir may not exist, that's ok
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
349
437
|
// Serve content and SEO files
|
|
350
|
-
devServer.middlewares.use((req, res, next) => {
|
|
438
|
+
devServer.middlewares.use(async (req, res, next) => {
|
|
439
|
+
// Handle default locale site-content request
|
|
351
440
|
if (req.url === `/${filename}`) {
|
|
352
441
|
res.setHeader('Content-Type', 'application/json')
|
|
353
442
|
res.end(JSON.stringify(siteContent, null, 2))
|
|
354
443
|
return
|
|
355
444
|
}
|
|
356
445
|
|
|
446
|
+
// Handle locale-prefixed site-content request (e.g., /es/site-content.json)
|
|
447
|
+
const localeContentMatch = req.url.match(/^\/([a-z]{2})\/site-content\.json$/)
|
|
448
|
+
if (localeContentMatch) {
|
|
449
|
+
const locale = localeContentMatch[1]
|
|
450
|
+
const translatedContent = await getTranslatedContent(locale)
|
|
451
|
+
|
|
452
|
+
if (translatedContent) {
|
|
453
|
+
// Add activeLocale to the content so runtime knows which locale is active
|
|
454
|
+
const contentWithLocale = {
|
|
455
|
+
...translatedContent,
|
|
456
|
+
config: {
|
|
457
|
+
...translatedContent.config,
|
|
458
|
+
activeLocale: locale
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
res.setHeader('Content-Type', 'application/json')
|
|
462
|
+
res.end(JSON.stringify(contentWithLocale, null, 2))
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
// If no translations, fall through to serve default content
|
|
466
|
+
}
|
|
467
|
+
|
|
357
468
|
// Serve sitemap.xml in dev mode
|
|
358
469
|
if (req.url === '/sitemap.xml' && seoEnabled && siteContent?.pages) {
|
|
359
470
|
res.setHeader('Content-Type', 'application/xml')
|
|
@@ -394,14 +505,35 @@ export function siteContentPlugin(options = {}) {
|
|
|
394
505
|
})
|
|
395
506
|
},
|
|
396
507
|
|
|
397
|
-
transformIndexHtml(html) {
|
|
508
|
+
async transformIndexHtml(html, ctx) {
|
|
398
509
|
if (!siteContent) return html
|
|
399
510
|
|
|
511
|
+
// Detect locale from URL (e.g., /es/about → 'es')
|
|
512
|
+
let contentToInject = siteContent
|
|
513
|
+
let activeLocale = null
|
|
514
|
+
|
|
515
|
+
if (ctx?.originalUrl) {
|
|
516
|
+
const localeMatch = ctx.originalUrl.match(/^\/([a-z]{2})(\/|$)/)
|
|
517
|
+
if (localeMatch) {
|
|
518
|
+
activeLocale = localeMatch[1]
|
|
519
|
+
const translatedContent = await getTranslatedContent(activeLocale)
|
|
520
|
+
if (translatedContent) {
|
|
521
|
+
contentToInject = {
|
|
522
|
+
...translatedContent,
|
|
523
|
+
config: {
|
|
524
|
+
...translatedContent.config,
|
|
525
|
+
activeLocale
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
400
532
|
let headInjection = ''
|
|
401
533
|
|
|
402
534
|
// Inject SEO meta tags
|
|
403
535
|
if (seoEnabled) {
|
|
404
|
-
const metaTags = generateMetaTags(
|
|
536
|
+
const metaTags = generateMetaTags(contentToInject, seoOptions)
|
|
405
537
|
if (metaTags) {
|
|
406
538
|
headInjection += ` ${metaTags}\n`
|
|
407
539
|
}
|
|
@@ -409,7 +541,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
409
541
|
|
|
410
542
|
// Inject content as JSON script tag
|
|
411
543
|
if (inject) {
|
|
412
|
-
headInjection += ` <script type="application/json" id="${variableName}">${JSON.stringify(
|
|
544
|
+
headInjection += ` <script type="application/json" id="${variableName}">${JSON.stringify(contentToInject)}</script>\n`
|
|
413
545
|
}
|
|
414
546
|
|
|
415
547
|
if (!headInjection) return html
|
|
@@ -16,8 +16,8 @@ import { processAllPreviews } from './images.js'
|
|
|
16
16
|
/**
|
|
17
17
|
* Build schema.json with preview image references
|
|
18
18
|
*/
|
|
19
|
-
async function buildSchemaWithPreviews(srcDir, outDir, isProduction) {
|
|
20
|
-
const schema = await buildSchema(srcDir)
|
|
19
|
+
async function buildSchemaWithPreviews(srcDir, outDir, isProduction, componentPaths) {
|
|
20
|
+
const schema = await buildSchema(srcDir, componentPaths)
|
|
21
21
|
|
|
22
22
|
// Process preview images
|
|
23
23
|
const { schema: schemaWithImages, totalImages } = await processAllPreviews(
|
|
@@ -42,6 +42,7 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
42
42
|
srcDir = 'src',
|
|
43
43
|
generateEntry = true,
|
|
44
44
|
entryFileName = '_entry.generated.js',
|
|
45
|
+
components: componentPaths,
|
|
45
46
|
} = options
|
|
46
47
|
|
|
47
48
|
let resolvedSrcDir
|
|
@@ -58,7 +59,7 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
58
59
|
const root = config.root || process.cwd()
|
|
59
60
|
const srcPath = resolve(root, srcDir)
|
|
60
61
|
const entryPath = join(srcPath, entryFileName)
|
|
61
|
-
await generateEntryPoint(srcPath, entryPath)
|
|
62
|
+
await generateEntryPoint(srcPath, entryPath, { componentPaths })
|
|
62
63
|
},
|
|
63
64
|
|
|
64
65
|
async configResolved(config) {
|
|
@@ -78,7 +79,8 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
78
79
|
const schema = await buildSchemaWithPreviews(
|
|
79
80
|
resolvedSrcDir,
|
|
80
81
|
outDir,
|
|
81
|
-
isProduction
|
|
82
|
+
isProduction,
|
|
83
|
+
componentPaths
|
|
82
84
|
)
|
|
83
85
|
|
|
84
86
|
const schemaPath = join(metaDir, 'schema.json')
|
|
@@ -97,6 +99,7 @@ export function foundationDevPlugin(options = {}) {
|
|
|
97
99
|
const {
|
|
98
100
|
srcDir = 'src',
|
|
99
101
|
entryFileName = '_entry.generated.js',
|
|
102
|
+
components: componentPaths,
|
|
100
103
|
} = options
|
|
101
104
|
|
|
102
105
|
let resolvedSrcDir
|
|
@@ -109,7 +112,7 @@ export function foundationDevPlugin(options = {}) {
|
|
|
109
112
|
const root = config.root || process.cwd()
|
|
110
113
|
const srcPath = resolve(root, srcDir)
|
|
111
114
|
const entryPath = join(srcPath, entryFileName)
|
|
112
|
-
await generateEntryPoint(srcPath, entryPath)
|
|
115
|
+
await generateEntryPoint(srcPath, entryPath, { componentPaths })
|
|
113
116
|
},
|
|
114
117
|
|
|
115
118
|
configResolved(config) {
|
|
@@ -118,10 +121,11 @@ export function foundationDevPlugin(options = {}) {
|
|
|
118
121
|
|
|
119
122
|
async handleHotUpdate({ file, server }) {
|
|
120
123
|
// Regenerate entry when meta.js files change
|
|
121
|
-
if
|
|
124
|
+
// Check if file is a meta.js in the src directory
|
|
125
|
+
if (file.startsWith(resolvedSrcDir) && file.endsWith('/meta.js')) {
|
|
122
126
|
console.log('Component meta.js changed, regenerating entry...')
|
|
123
127
|
const entryPath = join(resolvedSrcDir, entryFileName)
|
|
124
|
-
await generateEntryPoint(resolvedSrcDir, entryPath)
|
|
128
|
+
await generateEntryPoint(resolvedSrcDir, entryPath, { componentPaths })
|
|
125
129
|
|
|
126
130
|
// Trigger full reload since entry changed
|
|
127
131
|
server.ws.send({ type: 'full-reload' })
|
|
@@ -131,7 +135,7 @@ export function foundationDevPlugin(options = {}) {
|
|
|
131
135
|
if (file.endsWith('/exports.js') || file.endsWith('/exports.jsx')) {
|
|
132
136
|
console.log('Foundation exports changed, regenerating entry...')
|
|
133
137
|
const entryPath = join(resolvedSrcDir, entryFileName)
|
|
134
|
-
await generateEntryPoint(resolvedSrcDir, entryPath)
|
|
138
|
+
await generateEntryPoint(resolvedSrcDir, entryPath, { componentPaths })
|
|
135
139
|
server.ws.send({ type: 'full-reload' })
|
|
136
140
|
}
|
|
137
141
|
},
|