@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.
@@ -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,7 @@
1
+ /**
2
+ * Vite plugins for @uniweb/runtime
3
+ */
4
+
5
+ export { siteContentPlugin } from './site-content-plugin.js'
6
+ export { collectSiteContent } from './content-collector.js'
7
+ export { foundationPlugin } from './foundation-plugin.js'
@@ -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