@uniweb/build 0.1.28 → 0.1.30

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.28",
3
+ "version": "0.1.30",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "optionalDependencies": {
53
53
  "@uniweb/content-reader": "1.0.4",
54
- "@uniweb/runtime": "0.2.15"
54
+ "@uniweb/runtime": "0.2.17"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -21,8 +21,9 @@
21
21
  */
22
22
 
23
23
  import { existsSync, readFileSync } from 'node:fs'
24
- import { resolve, dirname } from 'node:path'
24
+ import { resolve, dirname, join } from 'node:path'
25
25
  import yaml from 'js-yaml'
26
+ import { generateEntryPoint } from '../generate-entry.js'
26
27
 
27
28
  /**
28
29
  * Detect foundation type from the foundation config value
@@ -177,8 +178,52 @@ export async function defineSiteConfig(options = {}) {
177
178
  foundationDevPlugin = modules[3].foundationDevPlugin
178
179
  }
179
180
 
181
+ // Plugin to ensure foundation entry file exists (for bundled mode with local foundation)
182
+ const ensureFoundationEntryPlugin = !isRuntimeMode && foundationInfo.type === 'local' ? {
183
+ name: 'uniweb:ensure-foundation-entry',
184
+ async config() {
185
+ const srcDir = join(foundationInfo.path, 'src')
186
+ const entryPath = join(srcDir, '_entry.generated.js')
187
+
188
+ // Always regenerate on dev start to ensure it's current
189
+ // This handles new components being added
190
+ if (existsSync(srcDir)) {
191
+ console.log('[site] Ensuring foundation entry is up to date...')
192
+ try {
193
+ await generateEntryPoint(srcDir, entryPath)
194
+ } catch (err) {
195
+ console.warn('[site] Failed to generate foundation entry:', err.message)
196
+ }
197
+ }
198
+ },
199
+
200
+ configureServer(server) {
201
+ // Watch foundation src for meta.js changes to regenerate entry
202
+ const srcDir = join(foundationInfo.path, 'src')
203
+ const entryPath = join(srcDir, '_entry.generated.js')
204
+
205
+ server.watcher.add(join(srcDir, '**', 'meta.js'))
206
+
207
+ server.watcher.on('all', async (event, path) => {
208
+ // Regenerate entry when meta.js files change (new/deleted components)
209
+ if (path.includes(srcDir) && path.endsWith('meta.js')) {
210
+ console.log(`[site] Foundation meta.js changed, regenerating entry...`)
211
+ try {
212
+ await generateEntryPoint(srcDir, entryPath)
213
+ server.ws.send({ type: 'full-reload' })
214
+ } catch (err) {
215
+ console.warn('[site] Failed to regenerate foundation entry:', err.message)
216
+ }
217
+ }
218
+ })
219
+ }
220
+ } : null
221
+
180
222
  // Build the plugins array
181
223
  const plugins = [
224
+ // Ensure foundation entry exists first (bundled mode only)
225
+ ensureFoundationEntryPlugin,
226
+
182
227
  // Standard plugins
183
228
  tailwind && tailwindcss(),
184
229
  react(),
@@ -37,6 +37,79 @@ import { collectSiteContent } from './content-collector.js'
37
37
  import { processAssets, rewriteSiteContentPaths } from './asset-processor.js'
38
38
  import { processAdvancedAssets } from './advanced-processors.js'
39
39
  import { processCollections, writeCollectionFiles } from './collection-processor.js'
40
+ import { executeFetch, mergeDataIntoContent } from './data-fetcher.js'
41
+
42
+ /**
43
+ * Execute all fetches for site content (used in dev mode)
44
+ * Populates section.cascadedData with fetched data
45
+ *
46
+ * @param {Object} siteContent - The collected site content
47
+ * @param {string} siteDir - Path to site directory
48
+ */
49
+ async function executeDevFetches(siteContent, siteDir) {
50
+ const fetchOptions = { siteRoot: siteDir, publicDir: 'public' }
51
+
52
+ // Site-level fetch
53
+ let siteCascadedData = {}
54
+ const siteFetch = siteContent.config?.fetch
55
+ if (siteFetch) {
56
+ const result = await executeFetch(siteFetch, fetchOptions)
57
+ if (result.data && !result.error) {
58
+ siteCascadedData[siteFetch.schema] = result.data
59
+ }
60
+ }
61
+
62
+ // Process each page
63
+ for (const page of siteContent.pages || []) {
64
+ let pageCascadedData = { ...siteCascadedData }
65
+
66
+ // Page-level fetch
67
+ const pageFetch = page.fetch
68
+ if (pageFetch) {
69
+ const result = await executeFetch(pageFetch, fetchOptions)
70
+ if (result.data && !result.error) {
71
+ pageCascadedData[pageFetch.schema] = result.data
72
+ }
73
+ }
74
+
75
+ // Process sections
76
+ await processDevSectionFetches(page.sections, pageCascadedData, fetchOptions)
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Process fetches for sections recursively
82
+ *
83
+ * @param {Array} sections - Sections to process
84
+ * @param {Object} cascadedData - Data from parent levels
85
+ * @param {Object} fetchOptions - Options for executeFetch
86
+ */
87
+ async function processDevSectionFetches(sections, cascadedData, fetchOptions) {
88
+ if (!sections || !Array.isArray(sections)) return
89
+
90
+ for (const section of sections) {
91
+ // Execute section-level fetch
92
+ const sectionFetch = section.fetch
93
+ if (sectionFetch) {
94
+ const result = await executeFetch(sectionFetch, fetchOptions)
95
+ if (result.data && !result.error) {
96
+ // Merge fetched data into cascadedData for this section
97
+ cascadedData = {
98
+ ...cascadedData,
99
+ [sectionFetch.schema]: result.data
100
+ }
101
+ }
102
+ }
103
+
104
+ // Attach cascaded data to section
105
+ section.cascadedData = { ...cascadedData }
106
+
107
+ // Process subsections recursively
108
+ if (section.subsections && section.subsections.length > 0) {
109
+ await processDevSectionFetches(section.subsections, cascadedData, fetchOptions)
110
+ }
111
+ }
112
+ }
40
113
  import { generateSearchIndex, isSearchEnabled, getSearchIndexFilename } from '../search/index.js'
41
114
  import { mergeTranslations } from '../i18n/merge.js'
42
115
 
@@ -274,6 +347,7 @@ export function siteContentPlugin(options = {}) {
274
347
  let server = null
275
348
  let localeTranslations = {} // Cache: { locale: translations }
276
349
  let localesDir = 'locales' // Default, updated from site config
350
+ let collectionsConfig = null // Cached for watcher setup
277
351
 
278
352
  /**
279
353
  * Load translations for a specific locale
@@ -332,10 +406,28 @@ export function siteContentPlugin(options = {}) {
332
406
  return {
333
407
  name: 'uniweb:site-content',
334
408
 
335
- configResolved(config) {
409
+ async configResolved(config) {
336
410
  resolvedSitePath = resolve(config.root, sitePath)
337
411
  resolvedOutDir = resolve(config.root, config.build.outDir)
338
412
  isProduction = config.command === 'build'
413
+
414
+ // In dev mode, process collections early so JSON files exist before server starts
415
+ // This runs before configureServer, ensuring data is available immediately
416
+ if (!isProduction) {
417
+ try {
418
+ // Do an early content collection to get the collections config
419
+ const earlyContent = await collectSiteContent(resolvedSitePath)
420
+ collectionsConfig = earlyContent.config?.collections
421
+
422
+ if (collectionsConfig) {
423
+ console.log('[site-content] Processing content collections...')
424
+ const collections = await processCollections(resolvedSitePath, collectionsConfig)
425
+ await writeCollectionFiles(resolvedSitePath, collections)
426
+ }
427
+ } catch (err) {
428
+ console.warn('[site-content] Early collection processing failed:', err.message)
429
+ }
430
+ }
339
431
  },
340
432
 
341
433
  async buildStart() {
@@ -345,13 +437,20 @@ export function siteContentPlugin(options = {}) {
345
437
  console.log(`[site-content] Collected ${siteContent.pages?.length || 0} pages`)
346
438
 
347
439
  // Process content collections if defined in site.yml
348
- // This generates JSON files in public/data/ BEFORE the Vite build
349
- if (siteContent.config?.collections) {
440
+ // In dev mode, this was already done in configResolved (before server starts)
441
+ // In production, do it here
442
+ if (isProduction && siteContent.config?.collections) {
350
443
  console.log('[site-content] Processing content collections...')
351
444
  const collections = await processCollections(resolvedSitePath, siteContent.config.collections)
352
445
  await writeCollectionFiles(resolvedSitePath, collections)
353
446
  }
354
447
 
448
+ // Execute data fetches in dev mode
449
+ // In production, prerender handles this
450
+ if (!isProduction) {
451
+ await executeDevFetches(siteContent, resolvedSitePath)
452
+ }
453
+
355
454
  // Update localesDir from site config
356
455
  if (siteContent.config?.i18n?.localesDir) {
357
456
  localesDir = siteContent.config.i18n.localesDir
@@ -382,6 +481,8 @@ export function siteContentPlugin(options = {}) {
382
481
  console.log('[site-content] Content changed, rebuilding...')
383
482
  try {
384
483
  siteContent = await collectSiteContent(resolvedSitePath)
484
+ // Execute fetches for the updated content
485
+ await executeDevFetches(siteContent, resolvedSitePath)
385
486
  console.log(`[site-content] Rebuilt ${siteContent.pages?.length || 0} pages`)
386
487
 
387
488
  // Send full reload to client
@@ -399,9 +500,11 @@ export function siteContentPlugin(options = {}) {
399
500
  collectionRebuildTimeout = setTimeout(async () => {
400
501
  console.log('[site-content] Collection content changed, regenerating JSON...')
401
502
  try {
402
- if (siteContent?.config?.collections) {
403
- const collections = await processCollections(resolvedSitePath, siteContent.config.collections)
404
- await writeCollectionFiles(resolvedSitePath, collections)
503
+ // Use collectionsConfig (cached from configResolved) or siteContent
504
+ const collections = collectionsConfig || siteContent?.config?.collections
505
+ if (collections) {
506
+ const processed = await processCollections(resolvedSitePath, collections)
507
+ await writeCollectionFiles(resolvedSitePath, processed)
405
508
  }
406
509
  // Send full reload to client
407
510
  server.ws.send({ type: 'full-reload' })
@@ -437,9 +540,10 @@ export function siteContentPlugin(options = {}) {
437
540
  }
438
541
 
439
542
  // Watch content/ folder for collection changes
440
- if (siteContent?.config?.collections) {
543
+ // Use collectionsConfig cached from configResolved (siteContent may be null here)
544
+ if (collectionsConfig) {
441
545
  const contentPaths = new Set()
442
- for (const config of Object.values(siteContent.config.collections)) {
546
+ for (const config of Object.values(collectionsConfig)) {
443
547
  const collectionPath = typeof config === 'string' ? config : config.path
444
548
  if (collectionPath) {
445
549
  contentPaths.add(resolve(resolvedSitePath, collectionPath))