@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.
@@ -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,7 +0,0 @@
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'
@@ -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