@uniweb/runtime 0.1.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/LICENSE +201 -0
- package/README.md +229 -0
- package/package.json +50 -0
- package/src/components/BlockRenderer.jsx +124 -0
- package/src/components/ErrorBoundary.jsx +51 -0
- package/src/components/Link.jsx +28 -0
- package/src/components/PageRenderer.jsx +57 -0
- package/src/components/SafeHtml.jsx +22 -0
- package/src/components/WebsiteRenderer.jsx +84 -0
- package/src/core/block.js +279 -0
- package/src/core/input.js +15 -0
- package/src/core/page.js +75 -0
- package/src/core/uniweb.js +103 -0
- package/src/core/website.js +157 -0
- package/src/index.js +245 -0
- package/src/vite/content-collector.js +269 -0
- package/src/vite/foundation-plugin.js +194 -0
- package/src/vite/index.js +7 -0
- package/src/vite/site-content-plugin.js +135 -0
|
@@ -0,0 +1,194 @@
|
|
|
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
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|