@uniweb/runtime 0.1.1 → 0.2.1
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 +48 -171
- package/package.json +9 -21
- package/src/index.jsx +27 -44
- package/src/components/Link.jsx +0 -28
- package/src/components/SafeHtml.jsx +0 -22
- package/src/core/block.js +0 -311
- package/src/core/input.js +0 -15
- package/src/core/page.js +0 -75
- package/src/core/uniweb.js +0 -103
- package/src/core/website.js +0 -157
- package/src/vite/content-collector.js +0 -269
- package/src/vite/foundation-plugin.js +0 -194
- package/src/vite/index.js +0 -7
- package/src/vite/site-content-plugin.js +0 -135
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content Collector for Vite
|
|
3
|
-
*
|
|
4
|
-
* Collects site content from a pages/ directory structure:
|
|
5
|
-
* - site.yml: Site configuration
|
|
6
|
-
* - pages/: Directory of page folders
|
|
7
|
-
* - page.yml: Page metadata
|
|
8
|
-
* - *.md: Section content with YAML frontmatter
|
|
9
|
-
*
|
|
10
|
-
* Uses @uniweb/content-reader for markdown → ProseMirror conversion
|
|
11
|
-
* when available, otherwise uses a simplified parser.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
15
|
-
import { join, parse, relative } from 'node:path'
|
|
16
|
-
import { existsSync } from 'node:fs'
|
|
17
|
-
import yaml from 'js-yaml'
|
|
18
|
-
|
|
19
|
-
// Try to import content-reader, fall back to simplified parser
|
|
20
|
-
let markdownToProseMirror
|
|
21
|
-
try {
|
|
22
|
-
const contentReader = await import('@uniweb/content-reader')
|
|
23
|
-
markdownToProseMirror = contentReader.markdownToProseMirror
|
|
24
|
-
} catch {
|
|
25
|
-
// Simplified fallback - just wraps content as text
|
|
26
|
-
markdownToProseMirror = (markdown) => ({
|
|
27
|
-
type: 'doc',
|
|
28
|
-
content: [
|
|
29
|
-
{
|
|
30
|
-
type: 'paragraph',
|
|
31
|
-
content: [{ type: 'text', text: markdown.trim() }]
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Parse YAML string using js-yaml
|
|
39
|
-
*/
|
|
40
|
-
function parseYaml(yamlString) {
|
|
41
|
-
try {
|
|
42
|
-
return yaml.load(yamlString) || {}
|
|
43
|
-
} catch (err) {
|
|
44
|
-
console.warn('[content-collector] YAML parse error:', err.message)
|
|
45
|
-
return {}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Read and parse a YAML file
|
|
51
|
-
*/
|
|
52
|
-
async function readYamlFile(filePath) {
|
|
53
|
-
try {
|
|
54
|
-
const content = await readFile(filePath, 'utf8')
|
|
55
|
-
return parseYaml(content)
|
|
56
|
-
} catch (err) {
|
|
57
|
-
if (err.code === 'ENOENT') return {}
|
|
58
|
-
throw err
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Check if a file is a markdown file
|
|
64
|
-
*/
|
|
65
|
-
function isMarkdownFile(filename) {
|
|
66
|
-
return filename.endsWith('.md') && !filename.startsWith('_')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Parse numeric prefix from filename (e.g., "1-hero.md" → { prefix: "1", name: "hero" })
|
|
71
|
-
*/
|
|
72
|
-
function parseNumericPrefix(filename) {
|
|
73
|
-
const match = filename.match(/^(\d+(?:\.\d+)*)-?(.*)$/)
|
|
74
|
-
if (match) {
|
|
75
|
-
return { prefix: match[1], name: match[2] || match[1] }
|
|
76
|
-
}
|
|
77
|
-
return { prefix: null, name: filename }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Compare filenames for sorting by numeric prefix
|
|
82
|
-
*/
|
|
83
|
-
function compareFilenames(a, b) {
|
|
84
|
-
const { prefix: prefixA } = parseNumericPrefix(parse(a).name)
|
|
85
|
-
const { prefix: prefixB } = parseNumericPrefix(parse(b).name)
|
|
86
|
-
|
|
87
|
-
if (!prefixA && !prefixB) return a.localeCompare(b)
|
|
88
|
-
if (!prefixA) return 1
|
|
89
|
-
if (!prefixB) return -1
|
|
90
|
-
|
|
91
|
-
const partsA = prefixA.split('.').map(Number)
|
|
92
|
-
const partsB = prefixB.split('.').map(Number)
|
|
93
|
-
|
|
94
|
-
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
95
|
-
const numA = partsA[i] ?? 0
|
|
96
|
-
const numB = partsB[i] ?? 0
|
|
97
|
-
if (numA !== numB) return numA - numB
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return 0
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Process a markdown file into a section
|
|
105
|
-
*/
|
|
106
|
-
async function processMarkdownFile(filePath, id) {
|
|
107
|
-
const content = await readFile(filePath, 'utf8')
|
|
108
|
-
let frontMatter = {}
|
|
109
|
-
let markdown = content
|
|
110
|
-
|
|
111
|
-
// Extract frontmatter
|
|
112
|
-
if (content.trim().startsWith('---')) {
|
|
113
|
-
const parts = content.split('---\n')
|
|
114
|
-
if (parts.length >= 3) {
|
|
115
|
-
frontMatter = parseYaml(parts[1])
|
|
116
|
-
markdown = parts.slice(2).join('---\n')
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const { component, preset, input, props, ...params } = frontMatter
|
|
121
|
-
|
|
122
|
-
// Convert markdown to ProseMirror
|
|
123
|
-
const proseMirrorContent = markdownToProseMirror(markdown)
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
id,
|
|
127
|
-
component: component || 'Section',
|
|
128
|
-
preset,
|
|
129
|
-
input,
|
|
130
|
-
params: { ...params, ...props },
|
|
131
|
-
content: proseMirrorContent,
|
|
132
|
-
subsections: []
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Build section hierarchy from flat list
|
|
138
|
-
*/
|
|
139
|
-
function buildSectionHierarchy(sections) {
|
|
140
|
-
const sectionMap = new Map()
|
|
141
|
-
const topLevel = []
|
|
142
|
-
|
|
143
|
-
// First pass: create map
|
|
144
|
-
for (const section of sections) {
|
|
145
|
-
sectionMap.set(section.id, section)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Second pass: build hierarchy
|
|
149
|
-
for (const section of sections) {
|
|
150
|
-
if (!section.id.includes('.')) {
|
|
151
|
-
topLevel.push(section)
|
|
152
|
-
continue
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const parts = section.id.split('.')
|
|
156
|
-
const parentId = parts.slice(0, -1).join('.')
|
|
157
|
-
const parent = sectionMap.get(parentId)
|
|
158
|
-
|
|
159
|
-
if (parent) {
|
|
160
|
-
parent.subsections.push(section)
|
|
161
|
-
} else {
|
|
162
|
-
// Orphan subsection - add to top level
|
|
163
|
-
topLevel.push(section)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return topLevel
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Process a page directory
|
|
172
|
-
*/
|
|
173
|
-
async function processPage(pagePath, pageName, headerSections, footerSections) {
|
|
174
|
-
const pageConfig = await readYamlFile(join(pagePath, 'page.yml'))
|
|
175
|
-
|
|
176
|
-
if (pageConfig.hidden) return null
|
|
177
|
-
|
|
178
|
-
// Get markdown files
|
|
179
|
-
const files = await readdir(pagePath)
|
|
180
|
-
const mdFiles = files.filter(isMarkdownFile).sort(compareFilenames)
|
|
181
|
-
|
|
182
|
-
// Process sections
|
|
183
|
-
const sections = []
|
|
184
|
-
for (const file of mdFiles) {
|
|
185
|
-
const { name } = parse(file)
|
|
186
|
-
const { prefix } = parseNumericPrefix(name)
|
|
187
|
-
const id = prefix || name
|
|
188
|
-
|
|
189
|
-
const section = await processMarkdownFile(join(pagePath, file), id)
|
|
190
|
-
sections.push(section)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Build hierarchy
|
|
194
|
-
const hierarchicalSections = buildSectionHierarchy(sections)
|
|
195
|
-
|
|
196
|
-
// Determine route
|
|
197
|
-
let route = '/' + pageName
|
|
198
|
-
if (pageName === 'home' || pageName === 'index') {
|
|
199
|
-
route = '/'
|
|
200
|
-
} else if (pageName.startsWith('@')) {
|
|
201
|
-
route = '/' + pageName
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
route,
|
|
206
|
-
title: pageConfig.title || pageName,
|
|
207
|
-
description: pageConfig.description || '',
|
|
208
|
-
order: pageConfig.order,
|
|
209
|
-
sections: hierarchicalSections
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Collect all site content
|
|
215
|
-
*/
|
|
216
|
-
export async function collectSiteContent(sitePath) {
|
|
217
|
-
const pagesPath = join(sitePath, 'pages')
|
|
218
|
-
|
|
219
|
-
// Read site config
|
|
220
|
-
const siteConfig = await readYamlFile(join(sitePath, 'site.yml'))
|
|
221
|
-
const themeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
|
|
222
|
-
|
|
223
|
-
// Check if pages directory exists
|
|
224
|
-
if (!existsSync(pagesPath)) {
|
|
225
|
-
return {
|
|
226
|
-
config: siteConfig,
|
|
227
|
-
theme: themeConfig,
|
|
228
|
-
pages: []
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Get page directories
|
|
233
|
-
const entries = await readdir(pagesPath)
|
|
234
|
-
const pages = []
|
|
235
|
-
let header = null
|
|
236
|
-
let footer = null
|
|
237
|
-
|
|
238
|
-
for (const entry of entries) {
|
|
239
|
-
const entryPath = join(pagesPath, entry)
|
|
240
|
-
const stats = await stat(entryPath)
|
|
241
|
-
|
|
242
|
-
if (!stats.isDirectory()) continue
|
|
243
|
-
|
|
244
|
-
const page = await processPage(entryPath, entry)
|
|
245
|
-
if (!page) continue
|
|
246
|
-
|
|
247
|
-
// Handle special pages
|
|
248
|
-
if (entry === '@header' || page.route === '/@header') {
|
|
249
|
-
header = page
|
|
250
|
-
} else if (entry === '@footer' || page.route === '/@footer') {
|
|
251
|
-
footer = page
|
|
252
|
-
} else {
|
|
253
|
-
pages.push(page)
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Sort pages by order
|
|
258
|
-
pages.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
config: siteConfig,
|
|
262
|
-
theme: themeConfig,
|
|
263
|
-
pages,
|
|
264
|
-
header,
|
|
265
|
-
footer
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export default collectSiteContent
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vite Plugin: Foundation
|
|
3
|
-
*
|
|
4
|
-
* Builds and serves a foundation within the site's dev server.
|
|
5
|
-
* This enables a single dev server for both site and foundation development.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* ```js
|
|
9
|
-
* import { foundationPlugin } from '@uniweb/runtime/vite'
|
|
10
|
-
*
|
|
11
|
-
* export default defineConfig({
|
|
12
|
-
* plugins: [
|
|
13
|
-
* foundationPlugin({
|
|
14
|
-
* name: 'my-foundation',
|
|
15
|
-
* path: '../my-foundation', // Path to foundation package
|
|
16
|
-
* serve: '/foundation', // URL path to serve from
|
|
17
|
-
* })
|
|
18
|
-
* ]
|
|
19
|
-
* })
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { resolve, join, relative } from 'node:path'
|
|
24
|
-
import { watch } from 'node:fs'
|
|
25
|
-
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
26
|
-
import { existsSync } from 'node:fs'
|
|
27
|
-
import { build } from 'vite'
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Create the foundation plugin
|
|
31
|
-
*/
|
|
32
|
-
export function foundationPlugin(options = {}) {
|
|
33
|
-
const {
|
|
34
|
-
name = 'foundation',
|
|
35
|
-
path: foundationPath = '../foundation',
|
|
36
|
-
serve: servePath = '/foundation',
|
|
37
|
-
watch: shouldWatch = true,
|
|
38
|
-
buildOnStart = true
|
|
39
|
-
} = options
|
|
40
|
-
|
|
41
|
-
let resolvedFoundationPath = null
|
|
42
|
-
let resolvedDistPath = null
|
|
43
|
-
let server = null
|
|
44
|
-
let watcher = null
|
|
45
|
-
let isBuilding = false
|
|
46
|
-
let lastBuildTime = 0
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Build the foundation using Vite
|
|
50
|
-
*/
|
|
51
|
-
async function buildFoundation() {
|
|
52
|
-
if (isBuilding) return
|
|
53
|
-
isBuilding = true
|
|
54
|
-
|
|
55
|
-
const startTime = Date.now()
|
|
56
|
-
console.log(`[foundation] Building ${name}...`)
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
// Use Vite's native config loading by specifying configFile
|
|
60
|
-
const configPath = join(resolvedFoundationPath, 'vite.config.js')
|
|
61
|
-
|
|
62
|
-
// Build using Vite with the foundation's own config file
|
|
63
|
-
await build({
|
|
64
|
-
root: resolvedFoundationPath,
|
|
65
|
-
configFile: existsSync(configPath) ? configPath : false,
|
|
66
|
-
logLevel: 'warn',
|
|
67
|
-
build: {
|
|
68
|
-
outDir: 'dist',
|
|
69
|
-
emptyOutDir: true,
|
|
70
|
-
watch: null, // Don't use Vite's watch, we handle it ourselves
|
|
71
|
-
},
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
lastBuildTime = Date.now()
|
|
75
|
-
console.log(`[foundation] Built ${name} in ${lastBuildTime - startTime}ms`)
|
|
76
|
-
|
|
77
|
-
// Trigger HMR reload if server is running
|
|
78
|
-
if (server) {
|
|
79
|
-
server.ws.send({ type: 'full-reload' })
|
|
80
|
-
}
|
|
81
|
-
} catch (err) {
|
|
82
|
-
console.error(`[foundation] Build failed:`, err.message)
|
|
83
|
-
} finally {
|
|
84
|
-
isBuilding = false
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
name: 'uniweb:foundation',
|
|
90
|
-
// Run before other plugins to intercept foundation requests
|
|
91
|
-
enforce: 'pre',
|
|
92
|
-
|
|
93
|
-
configResolved(config) {
|
|
94
|
-
resolvedFoundationPath = resolve(config.root, foundationPath)
|
|
95
|
-
resolvedDistPath = join(resolvedFoundationPath, 'dist')
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
async buildStart() {
|
|
99
|
-
if (buildOnStart) {
|
|
100
|
-
await buildFoundation()
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
configureServer(devServer) {
|
|
105
|
-
server = devServer
|
|
106
|
-
|
|
107
|
-
// Serve foundation files via middleware
|
|
108
|
-
// For JS files, use Vite's transform pipeline to properly resolve imports
|
|
109
|
-
devServer.middlewares.use(async (req, res, next) => {
|
|
110
|
-
const urlPath = req.url.split('?')[0]
|
|
111
|
-
|
|
112
|
-
if (!urlPath.startsWith(servePath)) {
|
|
113
|
-
return next()
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const filePath = urlPath.slice(servePath.length) || '/foundation.js'
|
|
117
|
-
const fullPath = join(resolvedDistPath, filePath)
|
|
118
|
-
|
|
119
|
-
if (!existsSync(fullPath)) {
|
|
120
|
-
return next()
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
let content = await readFile(fullPath, 'utf-8')
|
|
125
|
-
let contentType = 'application/octet-stream'
|
|
126
|
-
|
|
127
|
-
if (filePath.endsWith('.js')) {
|
|
128
|
-
contentType = 'application/javascript'
|
|
129
|
-
|
|
130
|
-
// Use Vite's transform pipeline to resolve bare imports
|
|
131
|
-
// This properly handles React ESM/CJS interop
|
|
132
|
-
const result = await devServer.transformRequest(
|
|
133
|
-
`/@fs${fullPath}`,
|
|
134
|
-
{ html: false }
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
if (result) {
|
|
138
|
-
content = result.code
|
|
139
|
-
}
|
|
140
|
-
} else if (filePath.endsWith('.css')) {
|
|
141
|
-
contentType = 'text/css'
|
|
142
|
-
} else if (filePath.endsWith('.json')) {
|
|
143
|
-
contentType = 'application/json'
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
res.setHeader('Content-Type', contentType)
|
|
147
|
-
res.setHeader('Cache-Control', 'no-cache')
|
|
148
|
-
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
149
|
-
res.end(content)
|
|
150
|
-
} catch (err) {
|
|
151
|
-
next(err)
|
|
152
|
-
}
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
// Watch foundation source for changes
|
|
156
|
-
if (shouldWatch) {
|
|
157
|
-
const srcPath = join(resolvedFoundationPath, 'src')
|
|
158
|
-
|
|
159
|
-
// Debounce rebuilds
|
|
160
|
-
let rebuildTimeout = null
|
|
161
|
-
const scheduleRebuild = () => {
|
|
162
|
-
if (rebuildTimeout) clearTimeout(rebuildTimeout)
|
|
163
|
-
rebuildTimeout = setTimeout(() => {
|
|
164
|
-
buildFoundation()
|
|
165
|
-
}, 200)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
watcher = watch(srcPath, { recursive: true }, (eventType, filename) => {
|
|
170
|
-
// Ignore non-source files
|
|
171
|
-
if (filename && (filename.endsWith('.js') || filename.endsWith('.jsx') ||
|
|
172
|
-
filename.endsWith('.ts') || filename.endsWith('.tsx') ||
|
|
173
|
-
filename.endsWith('.css') || filename.endsWith('.svg'))) {
|
|
174
|
-
console.log(`[foundation] ${filename} changed`)
|
|
175
|
-
scheduleRebuild()
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
console.log(`[foundation] Watching ${srcPath}`)
|
|
179
|
-
} catch (err) {
|
|
180
|
-
console.warn(`[foundation] Could not watch source:`, err.message)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
|
|
185
|
-
closeBundle() {
|
|
186
|
-
if (watcher) {
|
|
187
|
-
watcher.close()
|
|
188
|
-
watcher = null
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export default foundationPlugin
|
package/src/vite/index.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vite Plugin: Site Content
|
|
3
|
-
*
|
|
4
|
-
* Collects site content from pages/ directory and injects it into HTML.
|
|
5
|
-
* Watches for changes in development mode.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* ```js
|
|
9
|
-
* import { siteContentPlugin } from '@uniweb/runtime/vite'
|
|
10
|
-
*
|
|
11
|
-
* export default defineConfig({
|
|
12
|
-
* plugins: [
|
|
13
|
-
* siteContentPlugin({
|
|
14
|
-
* sitePath: './site', // Path to site directory
|
|
15
|
-
* inject: true, // Inject into HTML
|
|
16
|
-
* })
|
|
17
|
-
* ]
|
|
18
|
-
* })
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { resolve } from 'node:path'
|
|
23
|
-
import { watch } from 'node:fs'
|
|
24
|
-
import { collectSiteContent } from './content-collector.js'
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Create the site content plugin
|
|
28
|
-
*/
|
|
29
|
-
export function siteContentPlugin(options = {}) {
|
|
30
|
-
const {
|
|
31
|
-
sitePath = './',
|
|
32
|
-
pagesDir = 'pages',
|
|
33
|
-
variableName = '__SITE_CONTENT__',
|
|
34
|
-
inject = true,
|
|
35
|
-
filename = 'site-content.json',
|
|
36
|
-
watch: shouldWatch = true
|
|
37
|
-
} = options
|
|
38
|
-
|
|
39
|
-
let siteContent = null
|
|
40
|
-
let resolvedSitePath = null
|
|
41
|
-
let watcher = null
|
|
42
|
-
let server = null
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
name: 'uniweb:site-content',
|
|
46
|
-
|
|
47
|
-
configResolved(config) {
|
|
48
|
-
resolvedSitePath = resolve(config.root, sitePath)
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
async buildStart() {
|
|
52
|
-
// Collect content at build start
|
|
53
|
-
try {
|
|
54
|
-
siteContent = await collectSiteContent(resolvedSitePath)
|
|
55
|
-
console.log(`[site-content] Collected ${siteContent.pages?.length || 0} pages`)
|
|
56
|
-
} catch (err) {
|
|
57
|
-
console.error('[site-content] Failed to collect content:', err.message)
|
|
58
|
-
siteContent = { config: {}, theme: {}, pages: [] }
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
configureServer(devServer) {
|
|
63
|
-
server = devServer
|
|
64
|
-
|
|
65
|
-
// Watch for content changes in dev mode
|
|
66
|
-
if (shouldWatch) {
|
|
67
|
-
const watchPath = resolve(resolvedSitePath, pagesDir)
|
|
68
|
-
|
|
69
|
-
// Debounce rebuilds
|
|
70
|
-
let rebuildTimeout = null
|
|
71
|
-
const scheduleRebuild = () => {
|
|
72
|
-
if (rebuildTimeout) clearTimeout(rebuildTimeout)
|
|
73
|
-
rebuildTimeout = setTimeout(async () => {
|
|
74
|
-
console.log('[site-content] Content changed, rebuilding...')
|
|
75
|
-
try {
|
|
76
|
-
siteContent = await collectSiteContent(resolvedSitePath)
|
|
77
|
-
console.log(`[site-content] Rebuilt ${siteContent.pages?.length || 0} pages`)
|
|
78
|
-
|
|
79
|
-
// Send full reload to client
|
|
80
|
-
server.ws.send({ type: 'full-reload' })
|
|
81
|
-
} catch (err) {
|
|
82
|
-
console.error('[site-content] Rebuild failed:', err.message)
|
|
83
|
-
}
|
|
84
|
-
}, 100)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
watcher = watch(watchPath, { recursive: true }, scheduleRebuild)
|
|
89
|
-
console.log(`[site-content] Watching ${watchPath}`)
|
|
90
|
-
} catch (err) {
|
|
91
|
-
console.warn('[site-content] Could not watch pages directory:', err.message)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Serve content as JSON endpoint
|
|
96
|
-
devServer.middlewares.use((req, res, next) => {
|
|
97
|
-
if (req.url === `/${filename}`) {
|
|
98
|
-
res.setHeader('Content-Type', 'application/json')
|
|
99
|
-
res.end(JSON.stringify(siteContent, null, 2))
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
next()
|
|
103
|
-
})
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
transformIndexHtml(html) {
|
|
107
|
-
if (!inject || !siteContent) return html
|
|
108
|
-
|
|
109
|
-
// Inject content as JSON script tag
|
|
110
|
-
const injection = `<script type="application/json" id="${variableName}">${JSON.stringify(siteContent)}</script>\n`
|
|
111
|
-
|
|
112
|
-
// Insert before </head>
|
|
113
|
-
return html.replace('</head>', injection + '</head>')
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
generateBundle() {
|
|
117
|
-
// Emit content as JSON file in production build
|
|
118
|
-
this.emitFile({
|
|
119
|
-
type: 'asset',
|
|
120
|
-
fileName: filename,
|
|
121
|
-
source: JSON.stringify(siteContent, null, 2)
|
|
122
|
-
})
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
closeBundle() {
|
|
126
|
-
// Clean up watcher
|
|
127
|
-
if (watcher) {
|
|
128
|
-
watcher.close()
|
|
129
|
-
watcher = null
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export default siteContentPlugin
|