@uniweb/build 0.1.13 → 0.1.15

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,7 +59,7 @@
59
59
  "@tailwindcss/vite": "^4.0.0",
60
60
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
61
61
  "vite-plugin-svgr": "^4.0.0",
62
- "@uniweb/core": "0.1.6"
62
+ "@uniweb/core": "0.1.7"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "vite": {
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import { resolve } from 'node:path'
27
+ import { foundationPlugin } from '../vite-foundation-plugin.js'
27
28
 
28
29
  /**
29
30
  * Default externals for foundations
@@ -45,7 +46,7 @@ const DEFAULT_EXTERNALS = [
45
46
  * Create a complete Vite configuration for a Uniweb foundation
46
47
  *
47
48
  * @param {Object} [options={}] - Configuration options
48
- * @param {string} [options.entry] - Entry point path (default: 'src/entry-runtime.js')
49
+ * @param {string} [options.entry] - Entry point path (default: 'src/_entry.generated.js')
49
50
  * @param {string} [options.fileName] - Output file name (default: 'foundation')
50
51
  * @param {string[]} [options.externals] - Additional packages to externalize
51
52
  * @param {boolean} [options.includeDefaultExternals] - Include default externals (default: true)
@@ -57,7 +58,7 @@ const DEFAULT_EXTERNALS = [
57
58
  */
58
59
  export async function defineFoundationConfig(options = {}) {
59
60
  const {
60
- entry = 'src/entry-runtime.js',
61
+ entry = 'src/_entry.generated.js',
61
62
  fileName = 'foundation',
62
63
  externals: additionalExternals = [],
63
64
  includeDefaultExternals = true,
@@ -101,7 +102,9 @@ export async function defineFoundationConfig(options = {}) {
101
102
  }
102
103
 
103
104
  // Build the plugins array
105
+ // foundationPlugin handles entry generation and schema building
104
106
  const plugins = [
107
+ foundationPlugin({ srcDir: 'src' }),
105
108
  tailwind && tailwindcss(),
106
109
  react(),
107
110
  svgr(),
@@ -15,15 +15,15 @@ import {
15
15
  } from './schema.js'
16
16
 
17
17
  /**
18
- * Detect site configuration file (for custom Layout, etc.)
19
- * Looks for: src/site.js, src/site.jsx, src/site/index.js, src/site/index.jsx
18
+ * Detect runtime configuration file (for custom Layout, props, etc.)
19
+ * Looks for: src/runtime.js, src/runtime.jsx, src/runtime/index.js, src/runtime/index.jsx
20
20
  */
21
- function detectSiteConfig(srcDir) {
21
+ function detectRuntimeExports(srcDir) {
22
22
  const candidates = [
23
- { path: 'site.js', ext: 'js' },
24
- { path: 'site.jsx', ext: 'jsx' },
25
- { path: 'site/index.js', ext: 'js' },
26
- { path: 'site/index.jsx', ext: 'jsx' },
23
+ { path: 'runtime.js', ext: 'js' },
24
+ { path: 'runtime.jsx', ext: 'jsx' },
25
+ { path: 'runtime/index.js', ext: 'js' },
26
+ { path: 'runtime/index.jsx', ext: 'jsx' },
27
27
  ]
28
28
 
29
29
  for (const { path, ext } of candidates) {
@@ -34,23 +34,36 @@ function detectSiteConfig(srcDir) {
34
34
  return null
35
35
  }
36
36
 
37
+ /**
38
+ * Detect CSS file
39
+ * Looks for: src/styles.css, src/index.css
40
+ */
41
+ function detectCssFile(srcDir) {
42
+ const candidates = ['styles.css', 'index.css']
43
+ for (const file of candidates) {
44
+ if (existsSync(join(srcDir, file))) {
45
+ return `./${file}`
46
+ }
47
+ }
48
+ return null
49
+ }
50
+
37
51
  /**
38
52
  * Generate the entry point source code
39
53
  */
40
54
  function generateEntrySource(componentNames, runtimeConfig, options = {}) {
41
- const { includeCss = true, cssPath = './index.css', componentExtensions = {}, siteConfig = null } = options
55
+ const { cssPath = null, componentExtensions = {}, runtimeExports = null } = options
42
56
 
43
57
  const imports = []
44
- const exports = []
45
58
 
46
59
  // CSS import
47
- if (includeCss) {
60
+ if (cssPath) {
48
61
  imports.push(`import '${cssPath}'`)
49
62
  }
50
63
 
51
- // Site config import (for custom Layout, etc.)
52
- if (siteConfig) {
53
- imports.push(`import { site } from '${siteConfig.path}'`)
64
+ // Runtime exports import (for custom Layout, props, etc.)
65
+ if (runtimeExports) {
66
+ imports.push(`import runtime from '${runtimeExports.path}'`)
54
67
  }
55
68
 
56
69
  // Component imports (use detected extension or default to .js)
@@ -130,10 +143,10 @@ export function getSchema(name) {
130
143
  ? `\n// Named exports for direct imports\nexport { ${componentNames.join(', ')} }`
131
144
  : ''
132
145
 
133
- // Site config export (for custom Layout, etc.)
134
- const siteExport = siteConfig
135
- ? `\n// Site configuration (Layout, etc.)\nexport { site }`
136
- : `\n// No site configuration provided\nexport const site = null`
146
+ // Runtime exports (Layout, props, etc.)
147
+ const runtimeExport = runtimeExports
148
+ ? `\n// Runtime exports (Layout, props, etc.)\nexport { runtime }`
149
+ : `\n// No runtime exports provided\nexport const runtime = null`
137
150
 
138
151
  return `// Auto-generated foundation entry point
139
152
  // DO NOT EDIT - This file is regenerated during build
@@ -144,7 +157,7 @@ ${componentsObj}
144
157
  ${runtimeConfigBlock}
145
158
  ${exportFunctions}
146
159
  ${namedExports}
147
- ${siteExport}
160
+ ${runtimeExport}
148
161
  `
149
162
  }
150
163
 
@@ -194,17 +207,17 @@ export async function generateEntryPoint(srcDir, outputPath = null) {
194
207
  }
195
208
  }
196
209
 
197
- // Check if CSS exists
198
- const cssExists = existsSync(join(srcDir, 'index.css'))
210
+ // Check for CSS file
211
+ const cssPath = detectCssFile(srcDir)
199
212
 
200
- // Check for site config (custom Layout, etc.)
201
- const siteConfig = detectSiteConfig(srcDir)
213
+ // Check for runtime exports (custom Layout, props, etc.)
214
+ const runtimeExports = detectRuntimeExports(srcDir)
202
215
 
203
216
  // Generate source
204
217
  const source = generateEntrySource(componentNames, runtimeConfig, {
205
- includeCss: cssExists,
218
+ cssPath,
206
219
  componentExtensions,
207
- siteConfig,
220
+ runtimeExports,
208
221
  })
209
222
 
210
223
  // Write to file
@@ -214,15 +227,15 @@ export async function generateEntryPoint(srcDir, outputPath = null) {
214
227
 
215
228
  console.log(`Generated entry point: ${output}`)
216
229
  console.log(` - ${componentNames.length} components: ${componentNames.join(', ')}`)
217
- if (siteConfig) {
218
- console.log(` - Site config found: ${siteConfig.path}`)
230
+ if (runtimeExports) {
231
+ console.log(` - Runtime exports found: ${runtimeExports.path}`)
219
232
  }
220
233
 
221
234
  return {
222
235
  outputPath: output,
223
236
  componentNames,
224
237
  runtimeConfig,
225
- siteConfig,
238
+ runtimeExports,
226
239
  }
227
240
  }
228
241
 
package/src/prerender.js CHANGED
@@ -108,8 +108,9 @@ export async function prerenderSite(siteDir, options = {}) {
108
108
  const uniweb = createUniweb(siteContent)
109
109
  uniweb.setFoundation(foundation)
110
110
 
111
- if (foundation.config || foundation.site) {
112
- uniweb.setFoundationConfig(foundation.config || foundation.site)
111
+ // Check for foundation config (runtime is the new name, config/site are legacy)
112
+ if (foundation.runtime || foundation.config || foundation.site) {
113
+ uniweb.setFoundationConfig(foundation.runtime || foundation.config || foundation.site)
113
114
  }
114
115
 
115
116
  // Pre-render each page
@@ -187,8 +188,8 @@ function DefaultLayout({ header, body, footer }) {
187
188
  * Supports foundation-provided custom Layout via site.Layout
188
189
  */
189
190
  function Layout({ page, website, foundation }) {
190
- // Check if foundation provides a custom Layout
191
- const RemoteLayout = foundation.site?.Layout || null
191
+ // Check if foundation provides a custom Layout (runtime is the new name, site is legacy)
192
+ const RemoteLayout = foundation.runtime?.Layout || foundation.site?.Layout || null
192
193
 
193
194
  // Get block groups from page
194
195
  const headerBlocks = page.getHeaderBlocks()
package/src/schema.js CHANGED
@@ -5,169 +5,24 @@
5
5
  * runtime-relevant configuration.
6
6
  */
7
7
 
8
- import { readdir, readFile } from 'node:fs/promises'
8
+ import { readdir } from 'node:fs/promises'
9
9
  import { existsSync } from 'node:fs'
10
10
  import { join, basename } from 'node:path'
11
11
  import { pathToFileURL } from 'node:url'
12
12
 
13
- // Meta file names in order of preference
14
- const META_FILE_NAMES = ['meta.js', 'config.js', 'config.yml', 'meta.yml']
13
+ // Meta file name (standardized to meta.js)
14
+ const META_FILE_NAME = 'meta.js'
15
15
 
16
16
  // Keys that should be extracted for runtime (embedded in foundation.js)
17
17
  const RUNTIME_KEYS = ['input', 'props']
18
18
 
19
19
  /**
20
- * Simple YAML parser for backwards compatibility
21
- * Supports basic key-value, nested objects, and arrays
22
- */
23
- function parseYaml(content) {
24
- const lines = content.split('\n')
25
- return parseYamlLines(lines, 0).value
26
- }
27
-
28
- function getIndent(line) {
29
- const match = line.match(/^(\s*)/)
30
- return match ? match[1].length : 0
31
- }
32
-
33
- function parseYamlValue(value) {
34
- const trimmed = value.trim()
35
- if (!trimmed) return null
36
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
37
- (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
38
- return trimmed.slice(1, -1)
39
- }
40
- if (trimmed === 'true') return true
41
- if (trimmed === 'false') return false
42
- if (!isNaN(Number(trimmed)) && trimmed !== '') return Number(trimmed)
43
- return trimmed
44
- }
45
-
46
- function parseYamlLines(lines, startIndex, baseIndent = 0) {
47
- const result = {}
48
- let i = startIndex
49
-
50
- while (i < lines.length) {
51
- const line = lines[i]
52
- const trimmed = line.trim()
53
-
54
- if (!trimmed || trimmed.startsWith('#')) {
55
- i++
56
- continue
57
- }
58
-
59
- const indent = getIndent(line)
60
- if (indent < baseIndent && i > startIndex) break
61
- if (trimmed.startsWith('- ')) {
62
- i++
63
- continue
64
- }
65
-
66
- const colonIndex = trimmed.indexOf(':')
67
- if (colonIndex === -1) {
68
- i++
69
- continue
70
- }
71
-
72
- const key = trimmed.slice(0, colonIndex).trim()
73
- const valueAfterColon = trimmed.slice(colonIndex + 1).trim()
74
-
75
- const nextLine = lines[i + 1]
76
- const nextTrimmed = nextLine?.trim()
77
- const nextIndent = nextLine ? getIndent(nextLine) : 0
78
-
79
- if (nextTrimmed?.startsWith('- ') && nextIndent > indent) {
80
- const arrayResult = parseYamlArray(lines, i + 1, nextIndent)
81
- result[key] = arrayResult.value
82
- i = arrayResult.endIndex
83
- } else if (!valueAfterColon && nextIndent > indent) {
84
- const nestedResult = parseYamlLines(lines, i + 1, nextIndent)
85
- result[key] = nestedResult.value
86
- i = nestedResult.endIndex
87
- } else {
88
- result[key] = parseYamlValue(valueAfterColon)
89
- i++
90
- }
91
- }
92
-
93
- return { value: result, endIndex: i }
94
- }
95
-
96
- function parseYamlArray(lines, startIndex, baseIndent) {
97
- const result = []
98
- let i = startIndex
99
-
100
- while (i < lines.length) {
101
- const line = lines[i]
102
- const trimmed = line.trim()
103
-
104
- if (!trimmed || trimmed.startsWith('#')) {
105
- i++
106
- continue
107
- }
108
-
109
- const indent = getIndent(line)
110
- if (indent < baseIndent) break
111
-
112
- if (trimmed.startsWith('- ')) {
113
- const afterDash = trimmed.slice(2)
114
- const colonIndex = afterDash.indexOf(':')
115
-
116
- if (colonIndex !== -1) {
117
- const key = afterDash.slice(0, colonIndex).trim()
118
- const value = afterDash.slice(colonIndex + 1).trim()
119
- const obj = { [key]: parseYamlValue(value) }
120
- const itemIndent = indent + 2
121
- i++
122
-
123
- while (i < lines.length) {
124
- const propLine = lines[i]
125
- const propTrimmed = propLine?.trim()
126
-
127
- if (!propTrimmed || propTrimmed.startsWith('#')) {
128
- i++
129
- continue
130
- }
131
-
132
- const propIndent = getIndent(propLine)
133
- if (propIndent < itemIndent || propTrimmed.startsWith('- ')) break
134
-
135
- const propColonIndex = propTrimmed.indexOf(':')
136
- if (propColonIndex !== -1) {
137
- const propKey = propTrimmed.slice(0, propColonIndex).trim()
138
- const propValue = propTrimmed.slice(propColonIndex + 1).trim()
139
- obj[propKey] = parseYamlValue(propValue)
140
- }
141
- i++
142
- }
143
-
144
- result.push(obj)
145
- } else {
146
- result.push(parseYamlValue(afterDash))
147
- i++
148
- }
149
- } else {
150
- break
151
- }
152
- }
153
-
154
- return { value: result, endIndex: i }
155
- }
156
-
157
- /**
158
- * Load a meta file (JS or YAML)
20
+ * Load a meta.js file via dynamic import
159
21
  */
160
22
  async function loadMetaFile(filePath) {
161
- if (filePath.endsWith('.js')) {
162
- // Dynamic import for JS files
163
- const fileUrl = pathToFileURL(filePath).href
164
- const module = await import(fileUrl)
165
- return module.default
166
- } else {
167
- // Parse YAML
168
- const content = await readFile(filePath, 'utf-8')
169
- return parseYaml(content)
170
- }
23
+ const fileUrl = pathToFileURL(filePath).href
24
+ const module = await import(fileUrl)
25
+ return module.default
171
26
  }
172
27
 
173
28
  /**
@@ -175,37 +30,33 @@ async function loadMetaFile(filePath) {
175
30
  * Returns null if no meta file found
176
31
  */
177
32
  export async function loadComponentMeta(componentDir) {
178
- for (const fileName of META_FILE_NAMES) {
179
- const filePath = join(componentDir, fileName)
180
- if (existsSync(filePath)) {
181
- try {
182
- const meta = await loadMetaFile(filePath)
183
- return { meta, fileName, filePath }
184
- } catch (error) {
185
- console.warn(`Warning: Failed to load ${filePath}:`, error.message)
186
- return null
187
- }
188
- }
33
+ const filePath = join(componentDir, META_FILE_NAME)
34
+ if (!existsSync(filePath)) {
35
+ return null
36
+ }
37
+ try {
38
+ const meta = await loadMetaFile(filePath)
39
+ return { meta, fileName: META_FILE_NAME, filePath }
40
+ } catch (error) {
41
+ console.warn(`Warning: Failed to load ${filePath}:`, error.message)
42
+ return null
189
43
  }
190
- return null
191
44
  }
192
45
 
193
46
  /**
194
47
  * Load foundation-level meta file
195
48
  */
196
49
  export async function loadFoundationMeta(srcDir) {
197
- for (const fileName of META_FILE_NAMES) {
198
- const filePath = join(srcDir, fileName)
199
- if (existsSync(filePath)) {
200
- try {
201
- return await loadMetaFile(filePath)
202
- } catch (error) {
203
- console.warn(`Warning: Failed to load foundation meta ${filePath}:`, error.message)
204
- return {}
205
- }
206
- }
50
+ const filePath = join(srcDir, META_FILE_NAME)
51
+ if (!existsSync(filePath)) {
52
+ return {}
53
+ }
54
+ try {
55
+ return await loadMetaFile(filePath)
56
+ } catch (error) {
57
+ console.warn(`Warning: Failed to load foundation meta ${filePath}:`, error.message)
58
+ return {}
207
59
  }
208
- return {}
209
60
  }
210
61
 
211
62
  /**
@@ -92,7 +92,7 @@ function detectFoundationType(foundation, siteRoot) {
92
92
  * @param {string} siteRoot - Path to site directory
93
93
  * @returns {Object}
94
94
  */
95
- function readSiteConfig(siteRoot) {
95
+ export function readSiteConfig(siteRoot) {
96
96
  const configPath = resolve(siteRoot, 'site.yml')
97
97
  if (!existsSync(configPath)) {
98
98
  return {}
@@ -381,6 +381,71 @@ async function processPage(pagePath, pageName, siteRoot) {
381
381
  }
382
382
  }
383
383
 
384
+ /**
385
+ * Recursively collect pages from a directory
386
+ *
387
+ * @param {string} dirPath - Directory to scan
388
+ * @param {string} routePrefix - Route prefix for nested pages
389
+ * @param {string} siteRoot - Site root directory for asset resolution
390
+ * @returns {Promise<Object>} { pages, assetCollection, header, footer, left, right }
391
+ */
392
+ async function collectPagesRecursive(dirPath, routePrefix, siteRoot) {
393
+ const entries = await readdir(dirPath)
394
+ const pages = []
395
+ let assetCollection = {
396
+ assets: {},
397
+ hasExplicitPoster: new Set(),
398
+ hasExplicitPreview: new Set()
399
+ }
400
+ let header = null
401
+ let footer = null
402
+ let left = null
403
+ let right = null
404
+
405
+ for (const entry of entries) {
406
+ const entryPath = join(dirPath, entry)
407
+ const stats = await stat(entryPath)
408
+
409
+ if (!stats.isDirectory()) continue
410
+
411
+ // Build the page name/route
412
+ const pageName = routePrefix ? `${routePrefix}/${entry}` : entry
413
+
414
+ // Process this directory as a page
415
+ const result = await processPage(entryPath, pageName, siteRoot)
416
+ if (result) {
417
+ const { page, assetCollection: pageAssets } = result
418
+ assetCollection = mergeAssetCollections(assetCollection, pageAssets)
419
+
420
+ // Handle special pages (layout areas) - only at root level
421
+ if (!routePrefix) {
422
+ if (entry === '@header' || page.route === '/@header') {
423
+ header = page
424
+ } else if (entry === '@footer' || page.route === '/@footer') {
425
+ footer = page
426
+ } else if (entry === '@left' || page.route === '/@left') {
427
+ left = page
428
+ } else if (entry === '@right' || page.route === '/@right') {
429
+ right = page
430
+ } else {
431
+ pages.push(page)
432
+ }
433
+ } else {
434
+ pages.push(page)
435
+ }
436
+ }
437
+
438
+ // Recursively process subdirectories (but not special @ directories)
439
+ if (!entry.startsWith('@')) {
440
+ const subResult = await collectPagesRecursive(entryPath, pageName, siteRoot)
441
+ pages.push(...subResult.pages)
442
+ assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
443
+ }
444
+ }
445
+
446
+ return { pages, assetCollection, header, footer, left, right }
447
+ }
448
+
384
449
  /**
385
450
  * Collect all site content
386
451
  *
@@ -404,51 +469,16 @@ export async function collectSiteContent(sitePath) {
404
469
  }
405
470
  }
406
471
 
407
- // Get page directories
408
- const entries = await readdir(pagesPath)
409
- const pages = []
410
- let siteAssetCollection = {
411
- assets: {},
412
- hasExplicitPoster: new Set(),
413
- hasExplicitPreview: new Set()
414
- }
415
- let header = null
416
- let footer = null
417
- let left = null
418
- let right = null
419
-
420
- for (const entry of entries) {
421
- const entryPath = join(pagesPath, entry)
422
- const stats = await stat(entryPath)
423
-
424
- if (!stats.isDirectory()) continue
425
-
426
- const result = await processPage(entryPath, entry, sitePath)
427
- if (!result) continue
428
-
429
- const { page, assetCollection } = result
430
- siteAssetCollection = mergeAssetCollections(siteAssetCollection, assetCollection)
431
-
432
- // Handle special pages (layout areas)
433
- if (entry === '@header' || page.route === '/@header') {
434
- header = page
435
- } else if (entry === '@footer' || page.route === '/@footer') {
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
441
- } else {
442
- pages.push(page)
443
- }
444
- }
472
+ // Recursively collect all pages
473
+ const { pages, assetCollection, header, footer, left, right } =
474
+ await collectPagesRecursive(pagesPath, '', sitePath)
445
475
 
446
476
  // Sort pages by order
447
477
  pages.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
448
478
 
449
479
  // Log asset summary
450
- const assetCount = Object.keys(siteAssetCollection.assets).length
451
- const explicitCount = siteAssetCollection.hasExplicitPoster.size + siteAssetCollection.hasExplicitPreview.size
480
+ const assetCount = Object.keys(assetCollection.assets).length
481
+ const explicitCount = assetCollection.hasExplicitPoster.size + assetCollection.hasExplicitPreview.size
452
482
  if (assetCount > 0) {
453
483
  console.log(`[content-collector] Found ${assetCount} asset references${explicitCount > 0 ? ` (${explicitCount} with explicit poster/preview)` : ''}`)
454
484
  }
@@ -461,9 +491,9 @@ export async function collectSiteContent(sitePath) {
461
491
  footer,
462
492
  left,
463
493
  right,
464
- assets: siteAssetCollection.assets,
465
- hasExplicitPoster: siteAssetCollection.hasExplicitPoster,
466
- hasExplicitPreview: siteAssetCollection.hasExplicitPreview
494
+ assets: assetCollection.assets,
495
+ hasExplicitPoster: assetCollection.hasExplicitPoster,
496
+ hasExplicitPreview: assetCollection.hasExplicitPreview
467
497
  }
468
498
  }
469
499
 
package/src/site/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  export { siteContentPlugin } from './plugin.js'
10
- export { defineSiteConfig, default } from './config.js'
10
+ export { defineSiteConfig, readSiteConfig, default } from './config.js'
11
11
  export { collectSiteContent } from './content-collector.js'
12
12
  export {
13
13
  resolveAssetPath,
@@ -7,10 +7,9 @@
7
7
  * - Processing preview images for presets
8
8
  */
9
9
 
10
- import { writeFile, readFile, readdir, mkdir } from 'node:fs/promises'
11
- import { existsSync } from 'node:fs'
12
- import { join, resolve, relative } from 'node:path'
13
- import { buildSchema, discoverComponents } from './schema.js'
10
+ import { writeFile, mkdir } from 'node:fs/promises'
11
+ import { join, resolve } from 'node:path'
12
+ import { buildSchema } from './schema.js'
14
13
  import { generateEntryPoint } from './generate-entry.js'
15
14
  import { processAllPreviews } from './images.js'
16
15
 
@@ -52,20 +51,22 @@ export function foundationBuildPlugin(options = {}) {
52
51
  return {
53
52
  name: 'uniweb-foundation-build',
54
53
 
54
+ // Generate entry before config resolution (entry must exist for Vite to resolve it)
55
+ async config(config) {
56
+ if (!generateEntry) return
57
+
58
+ const root = config.root || process.cwd()
59
+ const srcPath = resolve(root, srcDir)
60
+ const entryPath = join(srcPath, entryFileName)
61
+ await generateEntryPoint(srcPath, entryPath)
62
+ },
63
+
55
64
  async configResolved(config) {
56
65
  resolvedSrcDir = resolve(config.root, srcDir)
57
66
  resolvedOutDir = config.build.outDir
58
67
  isProduction = config.mode === 'production'
59
68
  },
60
69
 
61
- async buildStart() {
62
- if (!generateEntry) return
63
-
64
- // Generate entry point before build starts
65
- const entryPath = join(resolvedSrcDir, entryFileName)
66
- await generateEntryPoint(resolvedSrcDir, entryPath)
67
- },
68
-
69
70
  async writeBundle() {
70
71
  // After bundle is written, generate schema.json in meta folder
71
72
  const outDir = resolve(resolvedOutDir)
@@ -103,26 +104,36 @@ export function foundationDevPlugin(options = {}) {
103
104
  return {
104
105
  name: 'uniweb-foundation-dev',
105
106
 
106
- configResolved(config) {
107
- resolvedSrcDir = resolve(config.root, srcDir)
107
+ // Generate entry before config resolution
108
+ async config(config) {
109
+ const root = config.root || process.cwd()
110
+ const srcPath = resolve(root, srcDir)
111
+ const entryPath = join(srcPath, entryFileName)
112
+ await generateEntryPoint(srcPath, entryPath)
108
113
  },
109
114
 
110
- async buildStart() {
111
- // Generate entry point at dev server start
112
- const entryPath = join(resolvedSrcDir, entryFileName)
113
- await generateEntryPoint(resolvedSrcDir, entryPath)
115
+ configResolved(config) {
116
+ resolvedSrcDir = resolve(config.root, srcDir)
114
117
  },
115
118
 
116
119
  async handleHotUpdate({ file, server }) {
117
- // Regenerate entry when meta files change
118
- if (file.includes('/components/') && file.match(/meta\.(js|yml)$|config\.(js|yml)$/)) {
119
- console.log('Meta file changed, regenerating entry...')
120
+ // Regenerate entry when meta.js files change
121
+ if (file.includes('/components/') && file.endsWith('/meta.js')) {
122
+ console.log('Component meta.js changed, regenerating entry...')
120
123
  const entryPath = join(resolvedSrcDir, entryFileName)
121
124
  await generateEntryPoint(resolvedSrcDir, entryPath)
122
125
 
123
126
  // Trigger full reload since entry changed
124
127
  server.ws.send({ type: 'full-reload' })
125
128
  }
129
+
130
+ // Also regenerate if runtime.js changes
131
+ if (file.endsWith('/runtime.js') || file.endsWith('/runtime.jsx')) {
132
+ console.log('Runtime exports changed, regenerating entry...')
133
+ const entryPath = join(resolvedSrcDir, entryFileName)
134
+ await generateEntryPoint(resolvedSrcDir, entryPath)
135
+ server.ws.send({ type: 'full-reload' })
136
+ }
126
137
  },
127
138
  }
128
139
  }
@@ -137,15 +148,16 @@ export function foundationPlugin(options = {}) {
137
148
  return {
138
149
  name: 'uniweb-foundation',
139
150
 
151
+ async config(config) {
152
+ // Only need to call once - devPlugin.config generates the entry
153
+ await devPlugin.config?.(config)
154
+ },
155
+
140
156
  configResolved(config) {
141
157
  buildPlugin.configResolved?.(config)
142
158
  devPlugin.configResolved?.(config)
143
159
  },
144
160
 
145
- async buildStart() {
146
- await devPlugin.buildStart?.()
147
- },
148
-
149
161
  async writeBundle(...args) {
150
162
  await buildPlugin.writeBundle?.(...args)
151
163
  },