@uniweb/build 0.6.21 → 0.7.0
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 +3 -3
- package/src/generate-entry.js +62 -12
- package/src/prerender.js +15 -15
- package/src/runtime-schema.js +53 -0
- package/src/schema.js +94 -3
- package/src/site/asset-processor.js +4 -18
- package/src/site/content-collector.js +123 -85
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
53
|
"@uniweb/content-reader": "1.1.2",
|
|
54
|
-
"@uniweb/runtime": "0.5.
|
|
54
|
+
"@uniweb/runtime": "0.5.23",
|
|
55
55
|
"@uniweb/schemas": "0.2.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
@@ -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.
|
|
64
|
+
"@uniweb/core": "0.5.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependenciesMeta": {
|
|
67
67
|
"vite": {
|
package/src/generate-entry.js
CHANGED
|
@@ -22,20 +22,22 @@
|
|
|
22
22
|
import { writeFile, mkdir } from 'node:fs/promises'
|
|
23
23
|
import { existsSync } from 'node:fs'
|
|
24
24
|
import { join, dirname } from 'node:path'
|
|
25
|
-
import { discoverComponents } from './schema.js'
|
|
26
|
-
import { extractAllRuntimeSchemas } from './runtime-schema.js'
|
|
25
|
+
import { discoverComponents, discoverLayoutsInPath } from './schema.js'
|
|
26
|
+
import { extractAllRuntimeSchemas, extractAllLayoutRuntimeSchemas } from './runtime-schema.js'
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Detect foundation config/exports file (for
|
|
29
|
+
* Detect foundation config/exports file (for props, vars, etc.)
|
|
30
30
|
*
|
|
31
31
|
* Looks for (in order of preference):
|
|
32
32
|
* 1. foundation.js - New consolidated format
|
|
33
33
|
* 2. exports.js - Legacy format (for backward compatibility)
|
|
34
34
|
*
|
|
35
35
|
* The file should export:
|
|
36
|
-
* - Layout (optional) - Custom page layout component
|
|
37
36
|
* - props (optional) - Foundation-wide props
|
|
38
37
|
* - vars (optional) - CSS custom properties (also read by schema builder)
|
|
38
|
+
* - defaultLayout (optional) - Default layout name
|
|
39
|
+
*
|
|
40
|
+
* Note: Layout components are now discovered from src/layouts/
|
|
39
41
|
*/
|
|
40
42
|
function detectFoundationExports(srcDir) {
|
|
41
43
|
// Prefer foundation.js (new consolidated format)
|
|
@@ -92,9 +94,12 @@ function generateEntrySource(components, options = {}) {
|
|
|
92
94
|
cssPath = null,
|
|
93
95
|
foundationExports = null,
|
|
94
96
|
meta = {},
|
|
97
|
+
layouts = {},
|
|
98
|
+
layoutMeta = {},
|
|
95
99
|
} = options
|
|
96
100
|
|
|
97
101
|
const componentNames = Object.keys(components).sort()
|
|
102
|
+
const layoutNames = Object.keys(layouts).sort()
|
|
98
103
|
|
|
99
104
|
const lines = [
|
|
100
105
|
'// Auto-generated foundation entry point',
|
|
@@ -107,13 +112,10 @@ function generateEntrySource(components, options = {}) {
|
|
|
107
112
|
lines.push(`import '${cssPath}'`)
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
// Foundation capabilities import (for
|
|
111
|
-
//
|
|
112
|
-
// This ensures both `export const layouts = {...}` (named) and
|
|
113
|
-
// `export default { layouts: {...} }` (default) work correctly.
|
|
115
|
+
// Foundation capabilities import (for props, vars, etc.)
|
|
116
|
+
// Note: Layout/layouts no longer merged from foundation.js — layouts come from src/layouts/ discovery
|
|
114
117
|
if (foundationExports) {
|
|
115
118
|
lines.push(`import * as _foundationModule from '${foundationExports.path}'`)
|
|
116
|
-
lines.push(`const capabilities = { ..._foundationModule.default, ...(_foundationModule.Layout && { Layout: _foundationModule.Layout }), ...(_foundationModule.layouts && { layouts: _foundationModule.layouts }) }`)
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
// Component imports
|
|
@@ -122,6 +124,12 @@ function generateEntrySource(components, options = {}) {
|
|
|
122
124
|
lines.push(`import ${name} from './${path}/${entryFile}'`)
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
// Layout imports
|
|
128
|
+
for (const name of layoutNames) {
|
|
129
|
+
const { path, entryFile = `index.js` } = layouts[name]
|
|
130
|
+
lines.push(`import ${name} from './${path}/${entryFile}'`)
|
|
131
|
+
}
|
|
132
|
+
|
|
125
133
|
lines.push('')
|
|
126
134
|
|
|
127
135
|
// Export components object
|
|
@@ -133,9 +141,17 @@ function generateEntrySource(components, options = {}) {
|
|
|
133
141
|
lines.push('export const components = {}')
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
// Foundation capabilities (
|
|
144
|
+
// Foundation capabilities (props, vars, etc. + discovered layouts)
|
|
137
145
|
lines.push('')
|
|
138
|
-
if (foundationExports) {
|
|
146
|
+
if (foundationExports || layoutNames.length > 0) {
|
|
147
|
+
const capParts = []
|
|
148
|
+
if (foundationExports) {
|
|
149
|
+
capParts.push('..._foundationModule.default')
|
|
150
|
+
}
|
|
151
|
+
if (layoutNames.length > 0) {
|
|
152
|
+
capParts.push(`layouts: { ${layoutNames.join(', ')} }`)
|
|
153
|
+
}
|
|
154
|
+
lines.push(`const capabilities = { ${capParts.join(', ')} }`)
|
|
139
155
|
lines.push('export { capabilities }')
|
|
140
156
|
} else {
|
|
141
157
|
lines.push('export const capabilities = null')
|
|
@@ -151,6 +167,16 @@ function generateEntrySource(components, options = {}) {
|
|
|
151
167
|
lines.push('export const meta = {}')
|
|
152
168
|
}
|
|
153
169
|
|
|
170
|
+
// Per-layout runtime metadata (areas, transitions, defaults)
|
|
171
|
+
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
|
+
}
|
|
179
|
+
|
|
154
180
|
lines.push('')
|
|
155
181
|
|
|
156
182
|
return lines.join('\n')
|
|
@@ -205,6 +231,10 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
205
231
|
console.warn('Warning: No section types found')
|
|
206
232
|
}
|
|
207
233
|
|
|
234
|
+
// Discover layouts from src/layouts/
|
|
235
|
+
const layouts = await discoverLayoutsInPath(srcDir)
|
|
236
|
+
const layoutNames = Object.keys(layouts).sort()
|
|
237
|
+
|
|
208
238
|
// Detect entry files for each component
|
|
209
239
|
// Bare files discovered in sections/ already have entryFile set — skip detection for those
|
|
210
240
|
for (const name of componentNames) {
|
|
@@ -216,20 +246,35 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
216
246
|
}
|
|
217
247
|
}
|
|
218
248
|
|
|
249
|
+
// Detect entry files for each layout (same logic as components)
|
|
250
|
+
for (const name of layoutNames) {
|
|
251
|
+
const layout = layouts[name]
|
|
252
|
+
if (!layout.entryFile) {
|
|
253
|
+
const entry = detectComponentEntry(srcDir, layout.path, layout.name)
|
|
254
|
+
layout.ext = entry.ext
|
|
255
|
+
layout.entryFile = entry.file
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
219
259
|
// Check for CSS file
|
|
220
260
|
const cssPath = detectCssFile(srcDir)
|
|
221
261
|
|
|
222
|
-
// Check for foundation exports (
|
|
262
|
+
// Check for foundation exports (props, vars, etc.)
|
|
223
263
|
const foundationExports = detectFoundationExports(srcDir)
|
|
224
264
|
|
|
225
265
|
// Extract per-component runtime metadata from meta.js files
|
|
226
266
|
const meta = extractAllRuntimeSchemas(components)
|
|
227
267
|
|
|
268
|
+
// Extract per-layout runtime metadata from meta.js files
|
|
269
|
+
const layoutMeta = extractAllLayoutRuntimeSchemas(layouts)
|
|
270
|
+
|
|
228
271
|
// Generate source
|
|
229
272
|
const source = generateEntrySource(components, {
|
|
230
273
|
cssPath,
|
|
231
274
|
foundationExports,
|
|
232
275
|
meta,
|
|
276
|
+
layouts,
|
|
277
|
+
layoutMeta,
|
|
233
278
|
})
|
|
234
279
|
|
|
235
280
|
// Write to file
|
|
@@ -239,6 +284,9 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
239
284
|
|
|
240
285
|
console.log(`Generated entry point: ${output}`)
|
|
241
286
|
console.log(` - ${componentNames.length} components: ${componentNames.join(', ')}`)
|
|
287
|
+
if (layoutNames.length > 0) {
|
|
288
|
+
console.log(` - ${layoutNames.length} layouts: ${layoutNames.join(', ')}`)
|
|
289
|
+
}
|
|
242
290
|
if (foundationExports) {
|
|
243
291
|
console.log(` - Foundation exports found: ${foundationExports.path}`)
|
|
244
292
|
}
|
|
@@ -246,8 +294,10 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
246
294
|
return {
|
|
247
295
|
outputPath: output,
|
|
248
296
|
componentNames,
|
|
297
|
+
layoutNames,
|
|
249
298
|
foundationExports,
|
|
250
299
|
meta,
|
|
300
|
+
layoutMeta,
|
|
251
301
|
}
|
|
252
302
|
}
|
|
253
303
|
|
package/src/prerender.js
CHANGED
|
@@ -569,33 +569,33 @@ function renderBlocks(blocks) {
|
|
|
569
569
|
* Render page layout for SSR
|
|
570
570
|
*/
|
|
571
571
|
function renderLayout(page, website) {
|
|
572
|
-
const
|
|
572
|
+
const layoutName = page.getLayoutName()
|
|
573
|
+
const RemoteLayout = website.getRemoteLayout(layoutName)
|
|
574
|
+
const layoutMeta = website.getLayoutMeta(layoutName)
|
|
573
575
|
|
|
574
|
-
const headerBlocks = page.getHeaderBlocks()
|
|
575
576
|
const bodyBlocks = page.getBodyBlocks()
|
|
576
|
-
const
|
|
577
|
-
const leftBlocks = page.getLeftBlocks()
|
|
578
|
-
const rightBlocks = page.getRightBlocks()
|
|
577
|
+
const areas = page.getLayoutAreas()
|
|
579
578
|
|
|
580
|
-
const headerElement = headerBlocks ? renderBlocks(headerBlocks) : null
|
|
581
579
|
const bodyElement = bodyBlocks ? renderBlocks(bodyBlocks) : null
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
580
|
+
const areaElements = {}
|
|
581
|
+
for (const [name, blocks] of Object.entries(areas)) {
|
|
582
|
+
areaElements[name] = renderBlocks(blocks)
|
|
583
|
+
}
|
|
585
584
|
|
|
586
585
|
if (RemoteLayout) {
|
|
586
|
+
const params = { ...(layoutMeta?.defaults || {}), ...(page.getLayoutParams() || {}) }
|
|
587
|
+
|
|
587
588
|
return React.createElement(RemoteLayout, {
|
|
588
|
-
page, website,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
leftPanel: leftElement, rightPanel: rightElement
|
|
589
|
+
page, website, params,
|
|
590
|
+
body: bodyElement,
|
|
591
|
+
...areaElements,
|
|
592
592
|
})
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
return React.createElement(React.Fragment, null,
|
|
596
|
-
|
|
596
|
+
areaElements.header && React.createElement('header', null, areaElements.header),
|
|
597
597
|
bodyElement && React.createElement('main', null, bodyElement),
|
|
598
|
-
|
|
598
|
+
areaElements.footer && React.createElement('footer', null, areaElements.footer)
|
|
599
599
|
)
|
|
600
600
|
}
|
|
601
601
|
|
package/src/runtime-schema.js
CHANGED
|
@@ -288,3 +288,56 @@ export function extractAllRuntimeSchemas(componentsMeta) {
|
|
|
288
288
|
|
|
289
289
|
return schemas
|
|
290
290
|
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Extract lean runtime schema for a layout from its full meta.js
|
|
294
|
+
*
|
|
295
|
+
* Layout runtime metadata:
|
|
296
|
+
* - areas: Array of area names this layout supports
|
|
297
|
+
* - transitions: View transition name mapping (stored but not acted on yet)
|
|
298
|
+
* - defaults: Param default values
|
|
299
|
+
*
|
|
300
|
+
* @param {Object} fullMeta - The full meta.js default export for a layout
|
|
301
|
+
* @returns {Object|null} - Lean layout runtime schema or null if empty
|
|
302
|
+
*/
|
|
303
|
+
export function extractLayoutRuntimeSchema(fullMeta) {
|
|
304
|
+
if (!fullMeta || typeof fullMeta !== 'object') {
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const runtime = {}
|
|
309
|
+
|
|
310
|
+
if (fullMeta.areas && Array.isArray(fullMeta.areas)) {
|
|
311
|
+
runtime.areas = fullMeta.areas
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (fullMeta.transitions && typeof fullMeta.transitions === 'object') {
|
|
315
|
+
runtime.transitions = fullMeta.transitions
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const defaults = extractParamDefaults(fullMeta.params)
|
|
319
|
+
if (defaults) {
|
|
320
|
+
runtime.defaults = defaults
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return Object.keys(runtime).length > 0 ? runtime : null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Extract runtime schemas for all layouts
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} layoutsMeta - Map of layoutName -> meta.js content
|
|
330
|
+
* @returns {Object} - Map of layoutName -> layout runtime schema (excludes null entries)
|
|
331
|
+
*/
|
|
332
|
+
export function extractAllLayoutRuntimeSchemas(layoutsMeta) {
|
|
333
|
+
const schemas = {}
|
|
334
|
+
|
|
335
|
+
for (const [name, meta] of Object.entries(layoutsMeta)) {
|
|
336
|
+
const schema = extractLayoutRuntimeSchema(meta)
|
|
337
|
+
if (schema) {
|
|
338
|
+
schemas[name] = schema
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return schemas
|
|
343
|
+
}
|
package/src/schema.js
CHANGED
|
@@ -32,6 +32,9 @@ const COMPONENT_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts'])
|
|
|
32
32
|
// The primary sections path where relaxed discovery applies
|
|
33
33
|
const SECTIONS_PATH = 'sections'
|
|
34
34
|
|
|
35
|
+
// The layouts path where layout components are discovered
|
|
36
|
+
const LAYOUTS_PATH = 'layouts'
|
|
37
|
+
|
|
35
38
|
/**
|
|
36
39
|
* Load a meta.js file via dynamic import
|
|
37
40
|
*/
|
|
@@ -107,11 +110,10 @@ export async function loadFoundationConfig(srcDir) {
|
|
|
107
110
|
try {
|
|
108
111
|
const module = await import(pathToFileURL(filePath).href)
|
|
109
112
|
// Support both default export and named exports
|
|
113
|
+
// Note: Layout/layouts no longer read from foundation.js — layouts come from src/layouts/ discovery
|
|
110
114
|
return {
|
|
111
115
|
...module.default,
|
|
112
116
|
vars: module.vars || module.default?.vars,
|
|
113
|
-
Layout: module.Layout || module.default?.Layout,
|
|
114
|
-
layouts: module.layouts || module.default?.layouts,
|
|
115
117
|
defaultLayout: module.default?.defaultLayout,
|
|
116
118
|
}
|
|
117
119
|
} catch (error) {
|
|
@@ -254,6 +256,87 @@ async function discoverSectionsInPath(srcDir, sectionsRelPath) {
|
|
|
254
256
|
return components
|
|
255
257
|
}
|
|
256
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Discover layout components in src/layouts/ with relaxed rules
|
|
261
|
+
*
|
|
262
|
+
* Same discovery pattern as sections: root-level files and directories
|
|
263
|
+
* are addressable by default. No recursion — layouts are flat.
|
|
264
|
+
*
|
|
265
|
+
* @param {string} srcDir - Source directory (e.g., 'src')
|
|
266
|
+
* @param {string} layoutsRelPath - Relative path to layouts dir (e.g., 'layouts')
|
|
267
|
+
* @returns {Object} Map of layoutName -> { name, path, ...meta }
|
|
268
|
+
*/
|
|
269
|
+
export async function discoverLayoutsInPath(srcDir, layoutsRelPath = LAYOUTS_PATH) {
|
|
270
|
+
const fullPath = join(srcDir, layoutsRelPath)
|
|
271
|
+
|
|
272
|
+
if (!existsSync(fullPath)) {
|
|
273
|
+
return {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const entries = await readdir(fullPath, { withFileTypes: true })
|
|
277
|
+
const layouts = {}
|
|
278
|
+
|
|
279
|
+
// Collect names from both files and directories to detect collisions
|
|
280
|
+
const fileNames = new Set()
|
|
281
|
+
const dirNames = new Set()
|
|
282
|
+
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
const ext = extname(entry.name)
|
|
285
|
+
if (entry.isFile() && COMPONENT_EXTENSIONS.has(ext)) {
|
|
286
|
+
const name = basename(entry.name, ext)
|
|
287
|
+
if (isComponentFileName(name)) {
|
|
288
|
+
fileNames.add(name)
|
|
289
|
+
}
|
|
290
|
+
} else if (entry.isDirectory()) {
|
|
291
|
+
dirNames.add(entry.name)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check for name collisions (e.g., DocsLayout.jsx AND DocsLayout/)
|
|
296
|
+
for (const name of fileNames) {
|
|
297
|
+
if (dirNames.has(name)) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Name collision in ${layoutsRelPath}/: both "${name}.jsx" (or similar) and "${name}/" exist. ` +
|
|
300
|
+
`Use one or the other, not both.`
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Discover bare files at root
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
if (!entry.isFile()) continue
|
|
308
|
+
const ext = extname(entry.name)
|
|
309
|
+
if (!COMPONENT_EXTENSIONS.has(ext)) continue
|
|
310
|
+
const name = basename(entry.name, ext)
|
|
311
|
+
if (!isComponentFileName(name)) continue
|
|
312
|
+
|
|
313
|
+
const meta = createImplicitMeta(name)
|
|
314
|
+
layouts[name] = {
|
|
315
|
+
...buildComponentEntry(name, layoutsRelPath, meta),
|
|
316
|
+
entryFile: entry.name,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Discover directories at root (no recursion for layouts)
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
if (!entry.isDirectory()) continue
|
|
323
|
+
if (!isComponentFileName(entry.name)) continue
|
|
324
|
+
|
|
325
|
+
const dirPath = join(fullPath, entry.name)
|
|
326
|
+
const relativePath = join(layoutsRelPath, entry.name)
|
|
327
|
+
const result = await loadComponentMeta(dirPath)
|
|
328
|
+
|
|
329
|
+
if (result && result.meta) {
|
|
330
|
+
if (result.meta.exposed === false) continue
|
|
331
|
+
layouts[entry.name] = buildComponentEntry(entry.name, relativePath, result.meta)
|
|
332
|
+
} else if (hasEntryFile(dirPath, entry.name)) {
|
|
333
|
+
layouts[entry.name] = buildComponentEntry(entry.name, relativePath, createImplicitMeta(entry.name))
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return layouts
|
|
338
|
+
}
|
|
339
|
+
|
|
257
340
|
/**
|
|
258
341
|
* Recursively discover nested section types that have meta.js
|
|
259
342
|
*
|
|
@@ -378,12 +461,20 @@ export async function buildSchema(srcDir, componentPaths) {
|
|
|
378
461
|
// Discover components
|
|
379
462
|
const components = await discoverComponents(srcDir, componentPaths)
|
|
380
463
|
|
|
464
|
+
// Discover layouts from src/layouts/
|
|
465
|
+
const layouts = await discoverLayoutsInPath(srcDir)
|
|
466
|
+
|
|
381
467
|
return {
|
|
382
|
-
// Merge identity and config - identity fields take precedence
|
|
383
468
|
_self: {
|
|
384
469
|
...foundationConfig,
|
|
385
470
|
...identity,
|
|
471
|
+
// foundation.js overrides package.json for editor-facing identity
|
|
472
|
+
...(foundationConfig.name && { name: foundationConfig.name }),
|
|
473
|
+
...(foundationConfig.description && { description: foundationConfig.description }),
|
|
474
|
+
...(foundationConfig.defaultLayout && { defaultLayout: foundationConfig.defaultLayout }),
|
|
386
475
|
},
|
|
476
|
+
// Layout metadata (full, for editor)
|
|
477
|
+
...(Object.keys(layouts).length > 0 && { _layouts: layouts }),
|
|
387
478
|
...components,
|
|
388
479
|
}
|
|
389
480
|
}
|
|
@@ -312,29 +312,15 @@ export function rewriteSiteContentPaths(siteContent, pathMapping) {
|
|
|
312
312
|
result.pages.forEach(processPage)
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
// Process
|
|
315
|
+
// Process layout area sets
|
|
316
316
|
if (result.layouts) {
|
|
317
|
-
for (const [name,
|
|
318
|
-
for (const
|
|
319
|
-
if (
|
|
317
|
+
for (const [name, areas] of Object.entries(result.layouts)) {
|
|
318
|
+
for (const areaPage of Object.values(areas)) {
|
|
319
|
+
if (areaPage) processPage(areaPage)
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
// Process flat header/footer/left/right (backward compat)
|
|
325
|
-
if (result.header) {
|
|
326
|
-
processPage(result.header)
|
|
327
|
-
}
|
|
328
|
-
if (result.footer) {
|
|
329
|
-
processPage(result.footer)
|
|
330
|
-
}
|
|
331
|
-
if (result.left) {
|
|
332
|
-
processPage(result.left)
|
|
333
|
-
}
|
|
334
|
-
if (result.right) {
|
|
335
|
-
processPage(result.right)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
324
|
// Remove the assets manifest from output (no longer needed at runtime)
|
|
339
325
|
delete result.assets
|
|
340
326
|
|
|
@@ -535,12 +535,7 @@ async function processFileAsPage(filePath, fileName, siteRoot, parentRoute) {
|
|
|
535
535
|
hidden: false,
|
|
536
536
|
hideInHeader: false,
|
|
537
537
|
hideInFooter: false,
|
|
538
|
-
layout: {
|
|
539
|
-
header: true,
|
|
540
|
-
footer: true,
|
|
541
|
-
leftPanel: true,
|
|
542
|
-
rightPanel: true
|
|
543
|
-
},
|
|
538
|
+
layout: {},
|
|
544
539
|
seo: {
|
|
545
540
|
noindex: false,
|
|
546
541
|
image: null,
|
|
@@ -971,10 +966,8 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
971
966
|
// Layout options (named layout + per-page overrides)
|
|
972
967
|
layout: {
|
|
973
968
|
...(resolvedLayoutName ? { name: resolvedLayoutName } : {}),
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
leftPanel: layoutObj.leftPanel !== false, // Show left panel (default true)
|
|
977
|
-
rightPanel: layoutObj.rightPanel !== false // Show right panel (default true)
|
|
969
|
+
...(layoutObj.hide ? { hide: layoutObj.hide } : {}),
|
|
970
|
+
...(layoutObj.params ? { params: layoutObj.params } : {}),
|
|
978
971
|
},
|
|
979
972
|
|
|
980
973
|
seo: {
|
|
@@ -1315,10 +1308,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1315
1308
|
hideInFooter: dirConfig.hideInFooter || false,
|
|
1316
1309
|
layout: {
|
|
1317
1310
|
...(effectiveLayout ? { name: effectiveLayout } : {}),
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
leftPanel: containerLayoutObj.leftPanel !== false,
|
|
1321
|
-
rightPanel: containerLayoutObj.rightPanel !== false
|
|
1311
|
+
...(containerLayoutObj.hide ? { hide: containerLayoutObj.hide } : {}),
|
|
1312
|
+
...(containerLayoutObj.params ? { params: containerLayoutObj.params } : {}),
|
|
1322
1313
|
},
|
|
1323
1314
|
seo: {
|
|
1324
1315
|
noindex: dirConfig.seo?.noindex || false,
|
|
@@ -1404,10 +1395,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1404
1395
|
hideInFooter: dirConfig.hideInFooter || false,
|
|
1405
1396
|
layout: {
|
|
1406
1397
|
...(effectiveLayout ? { name: effectiveLayout } : {}),
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
leftPanel: containerLayoutObj.leftPanel !== false,
|
|
1410
|
-
rightPanel: containerLayoutObj.rightPanel !== false
|
|
1398
|
+
...(containerLayoutObj.hide ? { hide: containerLayoutObj.hide } : {}),
|
|
1399
|
+
...(containerLayoutObj.params ? { params: containerLayoutObj.params } : {}),
|
|
1411
1400
|
},
|
|
1412
1401
|
seo: {
|
|
1413
1402
|
noindex: dirConfig.seo?.noindex || false,
|
|
@@ -1517,55 +1506,62 @@ async function loadFoundationVars(foundationPath) {
|
|
|
1517
1506
|
}
|
|
1518
1507
|
|
|
1519
1508
|
/**
|
|
1520
|
-
* Collect
|
|
1509
|
+
* Collect areas from a single directory (general named areas, not hardcoded).
|
|
1521
1510
|
*
|
|
1522
|
-
* Supports two forms per
|
|
1511
|
+
* Supports two forms per area:
|
|
1523
1512
|
* - Folder: dir/header/ (directory with .md files, like a page)
|
|
1524
1513
|
* - File shorthand: dir/header.md (single markdown file)
|
|
1525
1514
|
* Folder takes priority when both exist.
|
|
1526
1515
|
*
|
|
1527
|
-
* @param {string} dir - Directory to scan for
|
|
1516
|
+
* @param {string} dir - Directory to scan for area files
|
|
1528
1517
|
* @param {string} siteRoot - Path to site root
|
|
1529
|
-
* @param {string} routePrefix - Route prefix for
|
|
1530
|
-
* @returns {Promise<Object>}
|
|
1518
|
+
* @param {string} routePrefix - Route prefix for area pages (e.g., '/layout' or '/layout/marketing')
|
|
1519
|
+
* @returns {Promise<Object>} Map of areaName -> page data
|
|
1531
1520
|
*/
|
|
1532
|
-
async function
|
|
1533
|
-
const result = {
|
|
1521
|
+
async function collectAreasFromDir(dir, siteRoot, routePrefix = '/layout') {
|
|
1522
|
+
const result = {}
|
|
1534
1523
|
|
|
1535
1524
|
if (!existsSync(dir)) return result
|
|
1536
1525
|
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1526
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
1527
|
+
|
|
1528
|
+
// Track which area names we've already processed (folder form takes priority)
|
|
1529
|
+
const processed = new Set()
|
|
1530
|
+
|
|
1531
|
+
// First pass: directories (folder form, higher priority)
|
|
1532
|
+
for (const entry of entries) {
|
|
1533
|
+
if (!entry.isDirectory()) continue
|
|
1534
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
1535
|
+
|
|
1536
|
+
const areaName = entry.name
|
|
1537
|
+
const entryPath = join(dir, areaName)
|
|
1538
|
+
const pageResult = await processPage(entryPath, areaName, siteRoot, {
|
|
1539
|
+
isIndex: false,
|
|
1540
|
+
parentRoute: routePrefix
|
|
1541
|
+
})
|
|
1542
|
+
if (pageResult) {
|
|
1543
|
+
result[areaName] = pageResult.page
|
|
1544
|
+
processed.add(areaName)
|
|
1555
1545
|
}
|
|
1546
|
+
}
|
|
1556
1547
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
if (
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1548
|
+
// Second pass: markdown file shorthand (only if not already processed as folder)
|
|
1549
|
+
for (const entry of entries) {
|
|
1550
|
+
if (!entry.isFile()) continue
|
|
1551
|
+
if (!entry.name.endsWith('.md')) continue
|
|
1552
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
1553
|
+
|
|
1554
|
+
const areaName = entry.name.replace('.md', '')
|
|
1555
|
+
if (processed.has(areaName)) continue
|
|
1556
|
+
|
|
1557
|
+
const filePath = join(dir, entry.name)
|
|
1558
|
+
const { section } = await processMarkdownFile(filePath, '1', siteRoot, areaName)
|
|
1559
|
+
result[areaName] = {
|
|
1560
|
+
route: `${routePrefix}/${areaName}`,
|
|
1561
|
+
title: areaName.charAt(0).toUpperCase() + areaName.slice(1),
|
|
1562
|
+
description: '',
|
|
1563
|
+
layout: {},
|
|
1564
|
+
sections: [section]
|
|
1569
1565
|
}
|
|
1570
1566
|
}
|
|
1571
1567
|
|
|
@@ -1573,47 +1569,94 @@ async function collectPanelsFromDir(dir, siteRoot, routePrefix = '/layout') {
|
|
|
1573
1569
|
}
|
|
1574
1570
|
|
|
1575
1571
|
/**
|
|
1576
|
-
*
|
|
1572
|
+
* Check if a directory looks like a named layout (contains area-like .md files or area subdirs)
|
|
1573
|
+
* vs an area in folder form (contains section content processed by processPage).
|
|
1574
|
+
*
|
|
1575
|
+
* Heuristic: if a directory contains .md files at the top level AND no page.yml,
|
|
1576
|
+
* it's a named layout (those .md files are its area definitions).
|
|
1577
|
+
* If it has page.yml or looks like a page directory, it's an area folder.
|
|
1578
|
+
*
|
|
1579
|
+
* @param {string} dirPath - Path to the directory
|
|
1580
|
+
* @returns {Promise<boolean>} True if this looks like a named layout directory
|
|
1581
|
+
*/
|
|
1582
|
+
async function isNamedLayoutDir(dirPath) {
|
|
1583
|
+
// If it has page.yml, it's an area folder (processPage will handle it)
|
|
1584
|
+
if (existsSync(join(dirPath, 'page.yml'))) return false
|
|
1585
|
+
|
|
1586
|
+
const entries = await readdir(dirPath)
|
|
1587
|
+
// If directory contains .md files but no page.yml, it's a named layout
|
|
1588
|
+
return entries.some(e => e.endsWith('.md') && !e.startsWith('_') && !e.startsWith('.'))
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Collect layout areas from the layout/ directory, including named layout subdirectories.
|
|
1577
1593
|
*
|
|
1578
|
-
* Root-level files
|
|
1579
|
-
* Subdirectories
|
|
1580
|
-
* each
|
|
1594
|
+
* Root-level .md files and area directories form the "default" layout's areas.
|
|
1595
|
+
* Subdirectories that themselves contain .md files (without page.yml) are named layouts,
|
|
1596
|
+
* each with its own set of areas.
|
|
1581
1597
|
*
|
|
1582
1598
|
* @param {string} layoutDir - Path to layout directory
|
|
1583
1599
|
* @param {string} siteRoot - Path to site root
|
|
1584
|
-
* @returns {Promise<Object>} { layouts
|
|
1600
|
+
* @returns {Promise<Object>} { layouts }
|
|
1585
1601
|
*/
|
|
1586
1602
|
async function collectLayouts(layoutDir, siteRoot) {
|
|
1587
1603
|
if (!existsSync(layoutDir)) {
|
|
1588
|
-
return { layouts: null
|
|
1604
|
+
return { layouts: null }
|
|
1589
1605
|
}
|
|
1590
1606
|
|
|
1591
|
-
// Collect root-level panels (= "default" layout, backward compat)
|
|
1592
|
-
const defaultPanels = await collectPanelsFromDir(layoutDir, siteRoot, '/layout')
|
|
1593
|
-
|
|
1594
|
-
// Scan for named layout subdirectories
|
|
1595
1607
|
const entries = await readdir(layoutDir, { withFileTypes: true })
|
|
1596
|
-
|
|
1597
|
-
|
|
1608
|
+
|
|
1609
|
+
// Separate root-level entries into:
|
|
1610
|
+
// 1. Area .md files (for default layout)
|
|
1611
|
+
// 2. Area directories (for default layout) vs named layout directories
|
|
1612
|
+
const defaultAreaFiles = []
|
|
1613
|
+
const defaultAreaDirs = []
|
|
1614
|
+
const namedLayoutDirs = []
|
|
1598
1615
|
|
|
1599
1616
|
for (const entry of entries) {
|
|
1600
|
-
if (!entry.isDirectory()) continue
|
|
1601
|
-
if (knownPanels.has(entry.name)) continue // Skip panel folders (belong to default)
|
|
1602
1617
|
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
1603
1618
|
|
|
1604
|
-
|
|
1619
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
1620
|
+
defaultAreaFiles.push(entry)
|
|
1621
|
+
} else if (entry.isDirectory()) {
|
|
1622
|
+
const dirPath = join(layoutDir, entry.name)
|
|
1623
|
+
if (await isNamedLayoutDir(dirPath)) {
|
|
1624
|
+
namedLayoutDirs.push(entry)
|
|
1625
|
+
} else {
|
|
1626
|
+
defaultAreaDirs.push(entry)
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Collect default layout areas
|
|
1632
|
+
const defaultAreas = await collectAreasFromDir(layoutDir, siteRoot, '/layout')
|
|
1633
|
+
// Remove any named layout directories that got collected as areas
|
|
1634
|
+
for (const dir of namedLayoutDirs) {
|
|
1635
|
+
delete defaultAreas[dir.name]
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// Collect named layout areas
|
|
1639
|
+
const namedLayouts = {}
|
|
1640
|
+
for (const entry of namedLayoutDirs) {
|
|
1605
1641
|
const subdir = join(layoutDir, entry.name)
|
|
1606
|
-
namedLayouts[entry.name] = await
|
|
1642
|
+
namedLayouts[entry.name] = await collectAreasFromDir(subdir, siteRoot, `/layout/${entry.name}`)
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const hasDefaultAreas = Object.keys(defaultAreas).length > 0
|
|
1646
|
+
const hasNamedLayouts = Object.keys(namedLayouts).length > 0
|
|
1647
|
+
|
|
1648
|
+
if (!hasDefaultAreas && !hasNamedLayouts) {
|
|
1649
|
+
return { layouts: null }
|
|
1607
1650
|
}
|
|
1608
1651
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1652
|
+
// Always use the layouts object format (general areas)
|
|
1653
|
+
const layouts = {}
|
|
1654
|
+
if (hasDefaultAreas) {
|
|
1655
|
+
layouts.default = defaultAreas
|
|
1612
1656
|
}
|
|
1657
|
+
Object.assign(layouts, namedLayouts)
|
|
1613
1658
|
|
|
1614
|
-
|
|
1615
|
-
const layouts = { default: defaultPanels, ...namedLayouts }
|
|
1616
|
-
return { layouts, ...defaultPanels }
|
|
1659
|
+
return { layouts }
|
|
1617
1660
|
}
|
|
1618
1661
|
|
|
1619
1662
|
/**
|
|
@@ -1673,8 +1716,8 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1673
1716
|
// Determine root content mode from folder.yml/page.yml presence in pages directory
|
|
1674
1717
|
const { mode: rootContentMode } = await readFolderConfig(pagesPath, 'sections')
|
|
1675
1718
|
|
|
1676
|
-
// Collect layout
|
|
1677
|
-
const { layouts
|
|
1719
|
+
// Collect layout areas from layout/ directory (including named layout subdirectories)
|
|
1720
|
+
const { layouts } = await collectLayouts(layoutPath, sitePath)
|
|
1678
1721
|
|
|
1679
1722
|
// Site-level layout name (from site.yml layout: field)
|
|
1680
1723
|
const siteLayoutName = typeof siteConfig.layout === 'string' ? siteConfig.layout
|
|
@@ -1761,13 +1804,8 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
1761
1804
|
css: themeCSS
|
|
1762
1805
|
},
|
|
1763
1806
|
pages,
|
|
1764
|
-
//
|
|
1807
|
+
// Layout area sets: { default: { header: page, footer: page, ... }, marketing: { ... } }
|
|
1765
1808
|
layouts,
|
|
1766
|
-
// Flat panel fields (always present for backward compat)
|
|
1767
|
-
header,
|
|
1768
|
-
footer,
|
|
1769
|
-
left,
|
|
1770
|
-
right,
|
|
1771
1809
|
notFound,
|
|
1772
1810
|
// Versioned scopes: route → { versions, latestId }
|
|
1773
1811
|
versionedScopes: versionedScopesObj,
|