@uniweb/build 0.1.4 → 0.1.6
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 +11 -4
- package/src/docs.js +217 -0
- package/src/generate-entry.js +40 -1
- package/src/i18n/extract.js +259 -0
- package/src/i18n/hash.js +30 -0
- package/src/i18n/index.js +301 -0
- package/src/i18n/merge.js +192 -0
- package/src/i18n/sync.js +174 -0
- package/src/index.js +6 -0
- package/src/prerender.js +112 -22
- package/src/site/content-collector.js +150 -24
package/src/prerender.js
CHANGED
|
@@ -9,25 +9,44 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
|
9
9
|
import { existsSync } from 'node:fs'
|
|
10
10
|
import { join, dirname } from 'node:path'
|
|
11
11
|
import { pathToFileURL } from 'node:url'
|
|
12
|
+
import { createRequire } from 'node:module'
|
|
12
13
|
|
|
13
14
|
// Lazily loaded dependencies (ESM with React)
|
|
14
15
|
let React, renderToString, createUniweb
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* Load dependencies dynamically
|
|
18
|
-
*
|
|
18
|
+
* Load dependencies dynamically from the site's context
|
|
19
|
+
* This ensures we use the same React instance as the foundation
|
|
20
|
+
*
|
|
21
|
+
* @param {string} siteDir - Path to the site directory
|
|
19
22
|
*/
|
|
20
|
-
async function loadDependencies() {
|
|
23
|
+
async function loadDependencies(siteDir) {
|
|
21
24
|
if (React) return // Already loaded
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Create a require function that resolves from the site's perspective
|
|
27
|
+
// This ensures we get the same React instance that the foundation uses
|
|
28
|
+
const siteRequire = createRequire(join(siteDir, 'package.json'))
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Try to load React from site's node_modules
|
|
32
|
+
const reactMod = siteRequire('react')
|
|
33
|
+
const serverMod = siteRequire('react-dom/server')
|
|
34
|
+
|
|
35
|
+
React = reactMod.default || reactMod
|
|
36
|
+
renderToString = serverMod.renderToString
|
|
37
|
+
} catch {
|
|
38
|
+
// Fallback to dynamic import if require fails
|
|
39
|
+
const [reactMod, serverMod] = await Promise.all([
|
|
40
|
+
import('react'),
|
|
41
|
+
import('react-dom/server')
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
React = reactMod.default || reactMod
|
|
45
|
+
renderToString = serverMod.renderToString
|
|
46
|
+
}
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
// @uniweb/core can be imported normally
|
|
49
|
+
const coreMod = await import('@uniweb/core')
|
|
31
50
|
createUniweb = coreMod.createUniweb
|
|
32
51
|
}
|
|
33
52
|
|
|
@@ -53,9 +72,9 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
53
72
|
throw new Error(`Site must be built first. No dist directory found at: ${distDir}`)
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
// Load dependencies
|
|
75
|
+
// Load dependencies from site's context (ensures same React instance as foundation)
|
|
57
76
|
onProgress('Loading dependencies...')
|
|
58
|
-
await loadDependencies()
|
|
77
|
+
await loadDependencies(siteDir)
|
|
59
78
|
|
|
60
79
|
// Load site content
|
|
61
80
|
onProgress('Loading site content...')
|
|
@@ -140,22 +159,93 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
/**
|
|
143
|
-
*
|
|
144
|
-
|
|
162
|
+
* Render an array of blocks
|
|
163
|
+
*/
|
|
164
|
+
function BlocksRenderer({ blocks, foundation }) {
|
|
165
|
+
if (!blocks || blocks.length === 0) return null
|
|
166
|
+
|
|
167
|
+
return blocks.map((block, index) =>
|
|
168
|
+
React.createElement(BlockRenderer, {
|
|
169
|
+
key: block.id || index,
|
|
170
|
+
block,
|
|
171
|
+
foundation
|
|
172
|
+
})
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Default layout - renders header, body, footer in sequence
|
|
178
|
+
*/
|
|
179
|
+
function DefaultLayout({ header, body, footer }) {
|
|
180
|
+
return React.createElement(React.Fragment, null, header, body, footer)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Layout component for SSG
|
|
185
|
+
* Supports foundation-provided custom Layout via site.Layout
|
|
186
|
+
*/
|
|
187
|
+
function Layout({ page, website, foundation }) {
|
|
188
|
+
// Check if foundation provides a custom Layout
|
|
189
|
+
const RemoteLayout = foundation.site?.Layout || null
|
|
190
|
+
|
|
191
|
+
// Get block groups from page
|
|
192
|
+
const headerBlocks = page.getHeaderBlocks()
|
|
193
|
+
const bodyBlocks = page.getBodyBlocks()
|
|
194
|
+
const footerBlocks = page.getFooterBlocks()
|
|
195
|
+
const leftBlocks = page.getLeftBlocks()
|
|
196
|
+
const rightBlocks = page.getRightBlocks()
|
|
197
|
+
|
|
198
|
+
// Pre-render each area
|
|
199
|
+
const headerElement = headerBlocks
|
|
200
|
+
? React.createElement(BlocksRenderer, { blocks: headerBlocks, foundation })
|
|
201
|
+
: null
|
|
202
|
+
const bodyElement = bodyBlocks
|
|
203
|
+
? React.createElement(BlocksRenderer, { blocks: bodyBlocks, foundation })
|
|
204
|
+
: null
|
|
205
|
+
const footerElement = footerBlocks
|
|
206
|
+
? React.createElement(BlocksRenderer, { blocks: footerBlocks, foundation })
|
|
207
|
+
: null
|
|
208
|
+
const leftElement = leftBlocks
|
|
209
|
+
? React.createElement(BlocksRenderer, { blocks: leftBlocks, foundation })
|
|
210
|
+
: null
|
|
211
|
+
const rightElement = rightBlocks
|
|
212
|
+
? React.createElement(BlocksRenderer, { blocks: rightBlocks, foundation })
|
|
213
|
+
: null
|
|
214
|
+
|
|
215
|
+
// Use foundation's custom Layout if provided
|
|
216
|
+
if (RemoteLayout) {
|
|
217
|
+
return React.createElement(RemoteLayout, {
|
|
218
|
+
page,
|
|
219
|
+
website,
|
|
220
|
+
header: headerElement,
|
|
221
|
+
body: bodyElement,
|
|
222
|
+
footer: footerElement,
|
|
223
|
+
left: leftElement,
|
|
224
|
+
right: rightElement,
|
|
225
|
+
leftPanel: leftElement,
|
|
226
|
+
rightPanel: rightElement
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Default layout
|
|
231
|
+
return React.createElement(DefaultLayout, {
|
|
232
|
+
header: headerElement,
|
|
233
|
+
body: bodyElement,
|
|
234
|
+
footer: footerElement
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Page renderer for SSG
|
|
240
|
+
* Uses Layout component for proper orchestration of layout areas
|
|
145
241
|
*/
|
|
146
242
|
function PageRenderer({ page, foundation }) {
|
|
147
|
-
const
|
|
243
|
+
const website = globalThis.uniweb?.activeWebsite
|
|
148
244
|
|
|
149
245
|
return React.createElement(
|
|
150
246
|
'main',
|
|
151
247
|
null,
|
|
152
|
-
|
|
153
|
-
React.createElement(BlockRenderer, {
|
|
154
|
-
key: block.id || index,
|
|
155
|
-
block,
|
|
156
|
-
foundation
|
|
157
|
-
})
|
|
158
|
-
)
|
|
248
|
+
React.createElement(Layout, { page, website, foundation })
|
|
159
249
|
)
|
|
160
250
|
}
|
|
161
251
|
|
|
@@ -188,6 +188,91 @@ function buildSectionHierarchy(sections) {
|
|
|
188
188
|
return topLevel
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Process explicit sections array from page.yml
|
|
193
|
+
* Supports nested structure for subsections:
|
|
194
|
+
* sections:
|
|
195
|
+
* - hero
|
|
196
|
+
* - features:
|
|
197
|
+
* - logocloud
|
|
198
|
+
* - stats
|
|
199
|
+
* - pricing
|
|
200
|
+
*
|
|
201
|
+
* @param {Array} sectionsConfig - Sections array from page.yml
|
|
202
|
+
* @param {string} pagePath - Path to page directory
|
|
203
|
+
* @param {string} siteRoot - Site root for asset resolution
|
|
204
|
+
* @param {string} parentId - Parent section ID for building hierarchy
|
|
205
|
+
* @returns {Object} { sections, assetCollection, lastModified }
|
|
206
|
+
*/
|
|
207
|
+
async function processExplicitSections(sectionsConfig, pagePath, siteRoot, parentId = '') {
|
|
208
|
+
const sections = []
|
|
209
|
+
let assetCollection = {
|
|
210
|
+
assets: {},
|
|
211
|
+
hasExplicitPoster: new Set(),
|
|
212
|
+
hasExplicitPreview: new Set()
|
|
213
|
+
}
|
|
214
|
+
let lastModified = null
|
|
215
|
+
|
|
216
|
+
let index = 1
|
|
217
|
+
for (const item of sectionsConfig) {
|
|
218
|
+
let sectionName
|
|
219
|
+
let subsections = null
|
|
220
|
+
|
|
221
|
+
if (typeof item === 'string') {
|
|
222
|
+
// Simple section: "hero"
|
|
223
|
+
sectionName = item
|
|
224
|
+
} else if (typeof item === 'object' && item !== null) {
|
|
225
|
+
// Section with subsections: { features: [logocloud, stats] }
|
|
226
|
+
const keys = Object.keys(item)
|
|
227
|
+
if (keys.length === 1) {
|
|
228
|
+
sectionName = keys[0]
|
|
229
|
+
subsections = item[sectionName]
|
|
230
|
+
} else {
|
|
231
|
+
console.warn(`[content-collector] Invalid section entry:`, item)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Build section ID
|
|
239
|
+
const id = parentId ? `${parentId}.${index}` : String(index)
|
|
240
|
+
|
|
241
|
+
// Look for the markdown file
|
|
242
|
+
const filePath = join(pagePath, `${sectionName}.md`)
|
|
243
|
+
if (!existsSync(filePath)) {
|
|
244
|
+
console.warn(`[content-collector] Section file not found: ${sectionName}.md`)
|
|
245
|
+
index++
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Process the section
|
|
250
|
+
const { section, assetCollection: sectionAssets } = await processMarkdownFile(filePath, id, siteRoot)
|
|
251
|
+
assetCollection = mergeAssetCollections(assetCollection, sectionAssets)
|
|
252
|
+
|
|
253
|
+
// Track last modified
|
|
254
|
+
const fileStat = await stat(filePath)
|
|
255
|
+
if (!lastModified || fileStat.mtime > lastModified) {
|
|
256
|
+
lastModified = fileStat.mtime
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Process subsections recursively
|
|
260
|
+
if (Array.isArray(subsections) && subsections.length > 0) {
|
|
261
|
+
const subResult = await processExplicitSections(subsections, pagePath, siteRoot, id)
|
|
262
|
+
section.subsections = subResult.sections
|
|
263
|
+
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
264
|
+
if (subResult.lastModified && (!lastModified || subResult.lastModified > lastModified)) {
|
|
265
|
+
lastModified = subResult.lastModified
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
sections.push(section)
|
|
270
|
+
index++
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { sections, assetCollection, lastModified }
|
|
274
|
+
}
|
|
275
|
+
|
|
191
276
|
/**
|
|
192
277
|
* Process a page directory
|
|
193
278
|
*
|
|
@@ -199,14 +284,11 @@ function buildSectionHierarchy(sections) {
|
|
|
199
284
|
async function processPage(pagePath, pageName, siteRoot) {
|
|
200
285
|
const pageConfig = await readYamlFile(join(pagePath, 'page.yml'))
|
|
201
286
|
|
|
202
|
-
|
|
287
|
+
// Note: We no longer skip hidden pages here - they still exist as valid pages,
|
|
288
|
+
// they're just filtered from navigation. This allows direct linking to hidden pages.
|
|
289
|
+
// if (pageConfig.hidden) return null
|
|
203
290
|
|
|
204
|
-
|
|
205
|
-
const files = await readdir(pagePath)
|
|
206
|
-
const mdFiles = files.filter(isMarkdownFile).sort(compareFilenames)
|
|
207
|
-
|
|
208
|
-
// Process sections and collect assets
|
|
209
|
-
const sections = []
|
|
291
|
+
let hierarchicalSections = []
|
|
210
292
|
let pageAssetCollection = {
|
|
211
293
|
assets: {},
|
|
212
294
|
hasExplicitPoster: new Set(),
|
|
@@ -214,24 +296,45 @@ async function processPage(pagePath, pageName, siteRoot) {
|
|
|
214
296
|
}
|
|
215
297
|
let lastModified = null
|
|
216
298
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const { prefix } = parseNumericPrefix(name)
|
|
220
|
-
const id = prefix || name
|
|
299
|
+
// Check for explicit sections configuration
|
|
300
|
+
const { sections: sectionsConfig } = pageConfig
|
|
221
301
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
302
|
+
if (sectionsConfig === undefined || sectionsConfig === '*') {
|
|
303
|
+
// Default behavior: discover all .md files, sort by numeric prefix
|
|
304
|
+
const files = await readdir(pagePath)
|
|
305
|
+
const mdFiles = files.filter(isMarkdownFile).sort(compareFilenames)
|
|
225
306
|
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
307
|
+
const sections = []
|
|
308
|
+
for (const file of mdFiles) {
|
|
309
|
+
const { name } = parse(file)
|
|
310
|
+
const { prefix } = parseNumericPrefix(name)
|
|
311
|
+
const id = prefix || name
|
|
312
|
+
|
|
313
|
+
const { section, assetCollection } = await processMarkdownFile(join(pagePath, file), id, siteRoot)
|
|
314
|
+
sections.push(section)
|
|
315
|
+
pageAssetCollection = mergeAssetCollections(pageAssetCollection, assetCollection)
|
|
316
|
+
|
|
317
|
+
// Track last modified time for sitemap
|
|
318
|
+
const fileStat = await stat(join(pagePath, file))
|
|
319
|
+
if (!lastModified || fileStat.mtime > lastModified) {
|
|
320
|
+
lastModified = fileStat.mtime
|
|
321
|
+
}
|
|
230
322
|
}
|
|
231
|
-
}
|
|
232
323
|
|
|
233
|
-
|
|
234
|
-
|
|
324
|
+
// Build hierarchy from dot notation
|
|
325
|
+
hierarchicalSections = buildSectionHierarchy(sections)
|
|
326
|
+
|
|
327
|
+
} else if (Array.isArray(sectionsConfig) && sectionsConfig.length > 0) {
|
|
328
|
+
// Explicit sections array
|
|
329
|
+
const result = await processExplicitSections(sectionsConfig, pagePath, siteRoot)
|
|
330
|
+
hierarchicalSections = result.sections
|
|
331
|
+
pageAssetCollection = result.assetCollection
|
|
332
|
+
lastModified = result.lastModified
|
|
333
|
+
|
|
334
|
+
} else {
|
|
335
|
+
// Empty sections (null, empty array, or invalid) = pure route with no content
|
|
336
|
+
// hierarchicalSections stays empty, lastModified stays null
|
|
337
|
+
}
|
|
235
338
|
|
|
236
339
|
// Determine route
|
|
237
340
|
let route = '/' + pageName
|
|
@@ -241,16 +344,31 @@ async function processPage(pagePath, pageName, siteRoot) {
|
|
|
241
344
|
route = '/' + pageName
|
|
242
345
|
}
|
|
243
346
|
|
|
244
|
-
// Extract
|
|
245
|
-
const { seo = {}, ...restConfig } = pageConfig
|
|
347
|
+
// Extract configuration
|
|
348
|
+
const { seo = {}, layout = {}, ...restConfig } = pageConfig
|
|
246
349
|
|
|
247
350
|
return {
|
|
248
351
|
page: {
|
|
249
352
|
route,
|
|
250
353
|
title: pageConfig.title || pageName,
|
|
251
354
|
description: pageConfig.description || '',
|
|
355
|
+
label: pageConfig.label || null, // Short label for navigation (defaults to title)
|
|
252
356
|
order: pageConfig.order,
|
|
253
357
|
lastModified: lastModified?.toISOString(),
|
|
358
|
+
|
|
359
|
+
// Navigation options
|
|
360
|
+
hidden: pageConfig.hidden || false, // Hide from all navigation
|
|
361
|
+
hideInHeader: pageConfig.hideInHeader || false, // Hide from header nav
|
|
362
|
+
hideInFooter: pageConfig.hideInFooter || false, // Hide from footer nav
|
|
363
|
+
|
|
364
|
+
// Layout options (per-page overrides)
|
|
365
|
+
layout: {
|
|
366
|
+
header: layout.header !== false, // Show header (default true)
|
|
367
|
+
footer: layout.footer !== false, // Show footer (default true)
|
|
368
|
+
leftPanel: layout.leftPanel !== false, // Show left panel (default true)
|
|
369
|
+
rightPanel: layout.rightPanel !== false // Show right panel (default true)
|
|
370
|
+
},
|
|
371
|
+
|
|
254
372
|
seo: {
|
|
255
373
|
noindex: seo.noindex || false,
|
|
256
374
|
image: seo.image || null,
|
|
@@ -296,6 +414,8 @@ export async function collectSiteContent(sitePath) {
|
|
|
296
414
|
}
|
|
297
415
|
let header = null
|
|
298
416
|
let footer = null
|
|
417
|
+
let left = null
|
|
418
|
+
let right = null
|
|
299
419
|
|
|
300
420
|
for (const entry of entries) {
|
|
301
421
|
const entryPath = join(pagesPath, entry)
|
|
@@ -309,11 +429,15 @@ export async function collectSiteContent(sitePath) {
|
|
|
309
429
|
const { page, assetCollection } = result
|
|
310
430
|
siteAssetCollection = mergeAssetCollections(siteAssetCollection, assetCollection)
|
|
311
431
|
|
|
312
|
-
// Handle special pages
|
|
432
|
+
// Handle special pages (layout areas)
|
|
313
433
|
if (entry === '@header' || page.route === '/@header') {
|
|
314
434
|
header = page
|
|
315
435
|
} else if (entry === '@footer' || page.route === '/@footer') {
|
|
316
436
|
footer = page
|
|
437
|
+
} else if (entry === '@left' || page.route === '/@left') {
|
|
438
|
+
left = page
|
|
439
|
+
} else if (entry === '@right' || page.route === '/@right') {
|
|
440
|
+
right = page
|
|
317
441
|
} else {
|
|
318
442
|
pages.push(page)
|
|
319
443
|
}
|
|
@@ -335,6 +459,8 @@ export async function collectSiteContent(sitePath) {
|
|
|
335
459
|
pages,
|
|
336
460
|
header,
|
|
337
461
|
footer,
|
|
462
|
+
left,
|
|
463
|
+
right,
|
|
338
464
|
assets: siteAssetCollection.assets,
|
|
339
465
|
hasExplicitPoster: siteAssetCollection.hasExplicitPoster,
|
|
340
466
|
hasExplicitPreview: siteAssetCollection.hasExplicitPreview
|