@uniweb/build 0.4.10 → 0.6.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/README.md +3 -3
- package/package.json +4 -3
- package/src/docs.js +3 -3
- package/src/foundation/config.js +3 -3
- package/src/generate-entry.js +29 -10
- package/src/i18n/collections.js +479 -126
- package/src/i18n/index.js +2 -0
- package/src/prerender.js +59 -73
- package/src/runtime-schema.js +36 -7
- package/src/schema.js +196 -19
- package/src/site/collection-processor.js +73 -9
- package/src/site/data-fetcher.js +3 -1
- package/src/site/plugin.js +93 -15
- package/src/theme/css-generator.js +21 -1
- package/src/theme/processor.js +8 -0
- package/src/utils/infer-title.js +16 -0
- package/src/vite-foundation-plugin.js +17 -5
package/src/i18n/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
extractCollectionContent,
|
|
21
21
|
buildLocalizedCollections,
|
|
22
22
|
getCollectionLocales,
|
|
23
|
+
translateCollectionData,
|
|
23
24
|
COLLECTIONS_DIR
|
|
24
25
|
} from './collections.js'
|
|
25
26
|
import { generateSearchIndex, isSearchEnabled } from '../search/index.js'
|
|
@@ -69,6 +70,7 @@ export {
|
|
|
69
70
|
extractCollectionContent,
|
|
70
71
|
buildLocalizedCollections,
|
|
71
72
|
getCollectionLocales,
|
|
73
|
+
translateCollectionData,
|
|
72
74
|
COLLECTIONS_DIR,
|
|
73
75
|
|
|
74
76
|
// Locale resolution
|
package/src/prerender.js
CHANGED
|
@@ -26,19 +26,19 @@ let preparePropsSSR, getComponentMetaSSR
|
|
|
26
26
|
* @param {Object} siteContent - The site content from site-content.json
|
|
27
27
|
* @param {string} siteDir - Path to the site directory
|
|
28
28
|
* @param {function} onProgress - Progress callback
|
|
29
|
-
* @returns {Object} {
|
|
29
|
+
* @returns {Object} { pageFetchedData, fetchedData } - Fetched data for dynamic route expansion and DataStore pre-population
|
|
30
30
|
*/
|
|
31
31
|
async function executeAllFetches(siteContent, siteDir, onProgress) {
|
|
32
32
|
const fetchOptions = { siteRoot: siteDir, publicDir: 'public' }
|
|
33
|
+
const fetchedData = [] // Collected for DataStore pre-population
|
|
33
34
|
|
|
34
|
-
// 1. Site-level fetch
|
|
35
|
-
let siteCascadedData = {}
|
|
35
|
+
// 1. Site-level fetch
|
|
36
36
|
const siteFetch = siteContent.config?.fetch
|
|
37
37
|
if (siteFetch && siteFetch.prerender !== false) {
|
|
38
38
|
onProgress(` Fetching site data: ${siteFetch.path || siteFetch.url}`)
|
|
39
39
|
const result = await executeFetch(siteFetch, fetchOptions)
|
|
40
40
|
if (result.data && !result.error) {
|
|
41
|
-
|
|
41
|
+
fetchedData.push({ config: siteFetch, data: result.data })
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -46,14 +46,13 @@ async function executeAllFetches(siteContent, siteDir, onProgress) {
|
|
|
46
46
|
const pageFetchedData = new Map()
|
|
47
47
|
|
|
48
48
|
for (const page of siteContent.pages || []) {
|
|
49
|
-
// Page-level fetch
|
|
50
|
-
let pageCascadedData = { ...siteCascadedData }
|
|
49
|
+
// Page-level fetch
|
|
51
50
|
const pageFetch = page.fetch
|
|
52
51
|
if (pageFetch && pageFetch.prerender !== false) {
|
|
53
52
|
onProgress(` Fetching page data for ${page.route}: ${pageFetch.path || pageFetch.url}`)
|
|
54
53
|
const result = await executeFetch(pageFetch, fetchOptions)
|
|
55
54
|
if (result.data && !result.error) {
|
|
56
|
-
|
|
55
|
+
fetchedData.push({ config: pageFetch, data: result.data })
|
|
57
56
|
// Store for dynamic route expansion
|
|
58
57
|
pageFetchedData.set(page.route, {
|
|
59
58
|
schema: pageFetch.schema,
|
|
@@ -62,11 +61,11 @@ async function executeAllFetches(siteContent, siteDir, onProgress) {
|
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
// Process
|
|
66
|
-
await processSectionFetches(page.sections,
|
|
64
|
+
// Process section-level fetches (own fetch → parsedContent.data, not cascaded)
|
|
65
|
+
await processSectionFetches(page.sections, fetchOptions, onProgress)
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
return {
|
|
68
|
+
return { pageFetchedData, fetchedData }
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/**
|
|
@@ -142,12 +141,6 @@ function expandDynamicPages(pages, pageFetchedData, onProgress) {
|
|
|
142
141
|
allItems: items, // All items from parent
|
|
143
142
|
}
|
|
144
143
|
|
|
145
|
-
// Also inject into sections' cascadedData for components with inheritData
|
|
146
|
-
injectDynamicData(concretePage.sections, {
|
|
147
|
-
[singularSchema]: item, // Current item as singular
|
|
148
|
-
[schema]: items, // All items as plural
|
|
149
|
-
})
|
|
150
|
-
|
|
151
144
|
// Use item data for page metadata if available
|
|
152
145
|
if (item.title) concretePage.title = item.title
|
|
153
146
|
if (item.description || item.excerpt) concretePage.description = item.description || item.excerpt
|
|
@@ -159,38 +152,15 @@ function expandDynamicPages(pages, pageFetchedData, onProgress) {
|
|
|
159
152
|
return expandedPages
|
|
160
153
|
}
|
|
161
154
|
|
|
162
|
-
/**
|
|
163
|
-
* Inject dynamic route data into section cascadedData
|
|
164
|
-
* This ensures components with inheritData receive the current item
|
|
165
|
-
*
|
|
166
|
-
* @param {Array} sections - Sections to update
|
|
167
|
-
* @param {Object} data - Data to inject { article: {...}, articles: [...] }
|
|
168
|
-
*/
|
|
169
|
-
function injectDynamicData(sections, data) {
|
|
170
|
-
if (!sections || !Array.isArray(sections)) return
|
|
171
|
-
|
|
172
|
-
for (const section of sections) {
|
|
173
|
-
section.cascadedData = {
|
|
174
|
-
...(section.cascadedData || {}),
|
|
175
|
-
...data,
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Recurse into subsections
|
|
179
|
-
if (section.subsections && section.subsections.length > 0) {
|
|
180
|
-
injectDynamicData(section.subsections, data)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
155
|
/**
|
|
186
156
|
* Process fetch configs for sections (and subsections recursively)
|
|
157
|
+
* Section-level fetches merge data into parsedContent.data (not cascaded).
|
|
187
158
|
*
|
|
188
159
|
* @param {Array} sections - Array of section objects
|
|
189
|
-
* @param {Object} cascadedData - Data cascaded from site/page level
|
|
190
160
|
* @param {Object} fetchOptions - Options for executeFetch
|
|
191
161
|
* @param {function} onProgress - Progress callback
|
|
192
162
|
*/
|
|
193
|
-
async function processSectionFetches(sections,
|
|
163
|
+
async function processSectionFetches(sections, fetchOptions, onProgress) {
|
|
194
164
|
if (!sections || !Array.isArray(sections)) return
|
|
195
165
|
|
|
196
166
|
for (const section of sections) {
|
|
@@ -210,12 +180,9 @@ async function processSectionFetches(sections, cascadedData, fetchOptions, onPro
|
|
|
210
180
|
}
|
|
211
181
|
}
|
|
212
182
|
|
|
213
|
-
// Attach cascaded data for components with inheritData
|
|
214
|
-
section.cascadedData = cascadedData
|
|
215
|
-
|
|
216
183
|
// Process subsections recursively
|
|
217
184
|
if (section.subsections && section.subsections.length > 0) {
|
|
218
|
-
await processSectionFetches(section.subsections,
|
|
185
|
+
await processSectionFetches(section.subsections, fetchOptions, onProgress)
|
|
219
186
|
}
|
|
220
187
|
}
|
|
221
188
|
}
|
|
@@ -329,6 +296,13 @@ function getWrapperProps(block) {
|
|
|
329
296
|
style.position = 'relative'
|
|
330
297
|
}
|
|
331
298
|
|
|
299
|
+
// Apply context overrides as inline CSS custom properties (mirrors BlockRenderer.jsx)
|
|
300
|
+
if (block.contextOverrides) {
|
|
301
|
+
for (const [key, value] of Object.entries(block.contextOverrides)) {
|
|
302
|
+
style[`--${key}`] = value
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
332
306
|
const sectionId = block.stableId || block.id
|
|
333
307
|
return { id: `section-${sectionId}`, style, className, background }
|
|
334
308
|
}
|
|
@@ -372,18 +346,16 @@ function renderBackground(background) {
|
|
|
372
346
|
|
|
373
347
|
if (background.mode === 'gradient' && background.gradient) {
|
|
374
348
|
const g = background.gradient
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
const startPos = g.startPosition || 0
|
|
379
|
-
const endPos = g.endPosition || 100
|
|
349
|
+
// Raw CSS gradient string (e.g., "linear-gradient(to bottom, #000, #333)")
|
|
350
|
+
const bgValue = typeof g === 'string' ? g
|
|
351
|
+
: `linear-gradient(${g.angle || 0}deg, ${g.start || 'transparent'} ${g.startPosition || 0}%, ${g.end || 'transparent'} ${g.endPosition || 100}%)`
|
|
380
352
|
children.push(
|
|
381
353
|
React.createElement('div', {
|
|
382
354
|
key: 'bg-gradient',
|
|
383
355
|
className: 'background-gradient',
|
|
384
356
|
style: {
|
|
385
357
|
position: 'absolute', inset: '0',
|
|
386
|
-
background:
|
|
358
|
+
background: bgValue
|
|
387
359
|
},
|
|
388
360
|
'aria-hidden': 'true'
|
|
389
361
|
})
|
|
@@ -449,7 +421,8 @@ function renderBackground(background) {
|
|
|
449
421
|
|
|
450
422
|
/**
|
|
451
423
|
* Render a single block for SSR
|
|
452
|
-
* Mirrors BlockRenderer.jsx but without hooks (no runtime data fetching in SSR)
|
|
424
|
+
* Mirrors BlockRenderer.jsx but without hooks (no runtime data fetching in SSR).
|
|
425
|
+
* block.dataLoading is always false at prerender time — runtime fetches only happen client-side.
|
|
453
426
|
*/
|
|
454
427
|
function renderBlock(block) {
|
|
455
428
|
const Component = block.initComponent()
|
|
@@ -462,35 +435,37 @@ function renderBlock(block) {
|
|
|
462
435
|
}
|
|
463
436
|
|
|
464
437
|
// Build content and params with runtime guarantees
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const meta = getComponentMetaSSR(block.type)
|
|
472
|
-
const prepared = preparePropsSSR(block, meta)
|
|
473
|
-
params = prepared.params
|
|
474
|
-
content = {
|
|
475
|
-
...prepared.content,
|
|
476
|
-
...block.properties,
|
|
477
|
-
_prosemirror: block.parsedContent
|
|
478
|
-
}
|
|
438
|
+
const meta = getComponentMetaSSR(block.type)
|
|
439
|
+
const prepared = preparePropsSSR(block, meta)
|
|
440
|
+
let params = prepared.params
|
|
441
|
+
let content = {
|
|
442
|
+
...prepared.content,
|
|
443
|
+
...block.properties,
|
|
479
444
|
}
|
|
480
445
|
|
|
481
446
|
// Background handling (mirrors BlockRenderer.jsx)
|
|
482
447
|
const { background, ...wrapperProps } = getWrapperProps(block)
|
|
483
|
-
const meta = getComponentMetaSSR(block.type)
|
|
484
|
-
const hasBackground = background?.mode && meta?.background !== 'self'
|
|
485
448
|
|
|
486
|
-
|
|
487
|
-
|
|
449
|
+
// Merge Component.className (static classes declared on the component function)
|
|
450
|
+
// Order: context-{theme} + block.state.className + Component.className
|
|
451
|
+
const componentClassName = Component.className
|
|
452
|
+
if (componentClassName) {
|
|
453
|
+
wrapperProps.className = wrapperProps.className
|
|
454
|
+
? `${wrapperProps.className} ${componentClassName}`
|
|
455
|
+
: componentClassName
|
|
488
456
|
}
|
|
489
457
|
|
|
458
|
+
const hasBackground = background?.mode && meta?.background !== 'self'
|
|
459
|
+
|
|
460
|
+
block.hasBackground = hasBackground
|
|
461
|
+
|
|
462
|
+
// Use Component.as as the wrapper tag (default: 'section')
|
|
463
|
+
const wrapperTag = Component.as || 'section'
|
|
464
|
+
|
|
490
465
|
const componentProps = { content, params, block }
|
|
491
466
|
|
|
492
467
|
if (hasBackground) {
|
|
493
|
-
return React.createElement(
|
|
468
|
+
return React.createElement(wrapperTag, wrapperProps,
|
|
494
469
|
renderBackground(background),
|
|
495
470
|
React.createElement('div', { className: 'relative z-10' },
|
|
496
471
|
React.createElement(Component, componentProps)
|
|
@@ -498,7 +473,7 @@ function renderBlock(block) {
|
|
|
498
473
|
)
|
|
499
474
|
}
|
|
500
475
|
|
|
501
|
-
return React.createElement(
|
|
476
|
+
return React.createElement(wrapperTag, wrapperProps,
|
|
502
477
|
React.createElement(Component, componentProps)
|
|
503
478
|
)
|
|
504
479
|
}
|
|
@@ -678,7 +653,10 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
678
653
|
|
|
679
654
|
// Execute data fetches (site, page, section levels)
|
|
680
655
|
onProgress('Executing data fetches...')
|
|
681
|
-
const {
|
|
656
|
+
const { pageFetchedData, fetchedData } = await executeAllFetches(siteContent, siteDir, onProgress)
|
|
657
|
+
|
|
658
|
+
// Store fetchedData on siteContent for runtime DataStore pre-population
|
|
659
|
+
siteContent.fetchedData = fetchedData
|
|
682
660
|
|
|
683
661
|
// Expand dynamic pages (e.g., /blog/:slug → /blog/post-1, /blog/post-2)
|
|
684
662
|
if (siteContent.pages?.some(p => p.isDynamic)) {
|
|
@@ -693,6 +671,14 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
693
671
|
// Initialize the Uniweb runtime for this locale
|
|
694
672
|
onProgress('Initializing runtime...')
|
|
695
673
|
const uniweb = createUniweb(siteContent)
|
|
674
|
+
|
|
675
|
+
// Pre-populate DataStore so EntityStore can resolve data during prerender
|
|
676
|
+
if (fetchedData.length > 0 && uniweb.activeWebsite?.dataStore) {
|
|
677
|
+
for (const entry of fetchedData) {
|
|
678
|
+
uniweb.activeWebsite.dataStore.set(entry.config, entry.data)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
696
682
|
uniweb.setFoundation(foundation)
|
|
697
683
|
|
|
698
684
|
// Set base path from site config so components can access it during SSR
|
package/src/runtime-schema.js
CHANGED
|
@@ -197,15 +197,36 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// Data binding (CMS entities)
|
|
200
|
+
// Supports both old format (data: 'person:6') and new consolidated format
|
|
201
|
+
// (data: { entity: 'person:6', schemas: {...}, inherit: [...] })
|
|
200
202
|
if (fullMeta.data) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
if (typeof fullMeta.data === 'string') {
|
|
204
|
+
// Old format: data: 'person:6'
|
|
205
|
+
const parsed = parseDataString(fullMeta.data)
|
|
206
|
+
if (parsed) {
|
|
207
|
+
runtime.data = parsed
|
|
208
|
+
}
|
|
209
|
+
} else if (typeof fullMeta.data === 'object') {
|
|
210
|
+
// New format: data: { entity, schemas, inherit }
|
|
211
|
+
if (fullMeta.data.entity) {
|
|
212
|
+
const parsed = parseDataString(fullMeta.data.entity)
|
|
213
|
+
if (parsed) {
|
|
214
|
+
runtime.data = parsed
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (fullMeta.data.schemas) {
|
|
218
|
+
const schemas = extractSchemas(fullMeta.data.schemas)
|
|
219
|
+
if (schemas) {
|
|
220
|
+
runtime.schemas = schemas
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (fullMeta.data.inherit !== undefined) {
|
|
224
|
+
runtime.inheritData = fullMeta.data.inherit
|
|
225
|
+
}
|
|
204
226
|
}
|
|
205
227
|
}
|
|
206
228
|
|
|
207
|
-
|
|
208
|
-
const paramsObj = fullMeta.params || fullMeta.properties
|
|
229
|
+
const paramsObj = fullMeta.params
|
|
209
230
|
const defaults = extractParamDefaults(paramsObj)
|
|
210
231
|
if (defaults) {
|
|
211
232
|
runtime.defaults = defaults
|
|
@@ -225,7 +246,8 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
225
246
|
|
|
226
247
|
// Schemas - lean version for runtime validation/defaults
|
|
227
248
|
// Strips editor-only fields (label, hint, description)
|
|
228
|
-
|
|
249
|
+
// Top-level schemas supported for backwards compat (lower priority than data.schemas)
|
|
250
|
+
if (fullMeta.schemas && !runtime.schemas) {
|
|
229
251
|
const schemas = extractSchemas(fullMeta.schemas)
|
|
230
252
|
if (schemas) {
|
|
231
253
|
runtime.schemas = schemas
|
|
@@ -234,10 +256,17 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
234
256
|
|
|
235
257
|
// Data inheritance - component receives cascaded data from page/site level fetches
|
|
236
258
|
// Can be: true (inherit all), false (inherit none), or ['schema1', 'schema2'] (selective)
|
|
237
|
-
|
|
259
|
+
// Top-level inheritData supported for backwards compat (lower priority than data.inherit)
|
|
260
|
+
if (fullMeta.inheritData !== undefined && runtime.inheritData === undefined) {
|
|
238
261
|
runtime.inheritData = fullMeta.inheritData
|
|
239
262
|
}
|
|
240
263
|
|
|
264
|
+
// Auto-derive inheritData from entity type when no explicit inherit is set.
|
|
265
|
+
// data: { entity: 'articles' } implies inheritData: ['articles']
|
|
266
|
+
if (runtime.data && runtime.inheritData === undefined) {
|
|
267
|
+
runtime.inheritData = [runtime.data.type]
|
|
268
|
+
}
|
|
269
|
+
|
|
241
270
|
return Object.keys(runtime).length > 0 ? runtime : null
|
|
242
271
|
}
|
|
243
272
|
|
package/src/schema.js
CHANGED
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Discovers component meta files and loads them for schema.json generation.
|
|
5
5
|
* Schema data is for editor-time only, not runtime.
|
|
6
|
+
*
|
|
7
|
+
* Discovery rules:
|
|
8
|
+
* - sections/ root: bare files and folders are addressable by default (implicit empty meta)
|
|
9
|
+
* - sections/ nested: meta.js required for addressability
|
|
10
|
+
* - components/ (and other paths): meta.js required (backward compatibility)
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import { readdir, readFile } from 'node:fs/promises'
|
|
9
14
|
import { existsSync } from 'node:fs'
|
|
10
|
-
import { join, dirname } from 'node:path'
|
|
15
|
+
import { join, dirname, extname, basename } from 'node:path'
|
|
11
16
|
import { pathToFileURL } from 'node:url'
|
|
17
|
+
import { inferTitle } from './utils/infer-title.js'
|
|
12
18
|
|
|
13
19
|
// Component meta file name
|
|
14
20
|
const META_FILE_NAME = 'meta.js'
|
|
@@ -16,8 +22,15 @@ const META_FILE_NAME = 'meta.js'
|
|
|
16
22
|
// Foundation config file name
|
|
17
23
|
const FOUNDATION_FILE_NAME = 'foundation.js'
|
|
18
24
|
|
|
19
|
-
// Default
|
|
20
|
-
|
|
25
|
+
// Default paths to scan for content interfaces (relative to srcDir)
|
|
26
|
+
// sections/ is the primary convention; components/ supported for backward compatibility
|
|
27
|
+
const DEFAULT_COMPONENT_PATHS = ['sections', 'components']
|
|
28
|
+
|
|
29
|
+
// Extensions recognized as component entry files
|
|
30
|
+
const COMPONENT_EXTENSIONS = new Set(['.jsx', '.tsx', '.js', '.ts'])
|
|
31
|
+
|
|
32
|
+
// The primary sections path where relaxed discovery applies
|
|
33
|
+
const SECTIONS_PATH = 'sections'
|
|
21
34
|
|
|
22
35
|
/**
|
|
23
36
|
* Load a meta.js file via dynamic import
|
|
@@ -113,12 +126,173 @@ export async function loadFoundationMeta(srcDir) {
|
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
/**
|
|
116
|
-
*
|
|
129
|
+
* Check if a filename looks like a PascalCase component (starts with uppercase)
|
|
130
|
+
*/
|
|
131
|
+
function isComponentFileName(name) {
|
|
132
|
+
return /^[A-Z]/.test(name)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if a directory has a valid entry file (Name.ext or index.ext)
|
|
137
|
+
*/
|
|
138
|
+
function hasEntryFile(dirPath, dirName) {
|
|
139
|
+
for (const ext of ['.jsx', '.tsx', '.js', '.ts']) {
|
|
140
|
+
if (existsSync(join(dirPath, `${dirName}${ext}`))) return true
|
|
141
|
+
if (existsSync(join(dirPath, `index${ext}`))) return true
|
|
142
|
+
}
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create an implicit empty meta for a section type discovered without meta.js
|
|
148
|
+
*/
|
|
149
|
+
function createImplicitMeta(name) {
|
|
150
|
+
return { title: inferTitle(name) }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build a component entry with title inference applied
|
|
155
|
+
*/
|
|
156
|
+
function buildComponentEntry(name, relativePath, meta) {
|
|
157
|
+
const entry = {
|
|
158
|
+
name,
|
|
159
|
+
path: relativePath,
|
|
160
|
+
...meta,
|
|
161
|
+
}
|
|
162
|
+
// Apply title inference if meta has no explicit title
|
|
163
|
+
if (!entry.title) {
|
|
164
|
+
entry.title = inferTitle(name)
|
|
165
|
+
}
|
|
166
|
+
return entry
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Discover section types in sections/ with relaxed rules
|
|
171
|
+
*
|
|
172
|
+
* Root level: bare files and folders are addressable by default.
|
|
173
|
+
* Nested levels: meta.js required for addressability.
|
|
174
|
+
*
|
|
117
175
|
* @param {string} srcDir - Source directory (e.g., 'src')
|
|
118
|
-
* @param {string}
|
|
176
|
+
* @param {string} sectionsRelPath - Relative path to sections dir (e.g., 'sections')
|
|
177
|
+
*/
|
|
178
|
+
async function discoverSectionsInPath(srcDir, sectionsRelPath) {
|
|
179
|
+
const fullPath = join(srcDir, sectionsRelPath)
|
|
180
|
+
|
|
181
|
+
if (!existsSync(fullPath)) {
|
|
182
|
+
return {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const entries = await readdir(fullPath, { withFileTypes: true })
|
|
186
|
+
const components = {}
|
|
187
|
+
|
|
188
|
+
// Collect names from both files and directories to detect collisions
|
|
189
|
+
const fileNames = new Set()
|
|
190
|
+
const dirNames = new Set()
|
|
191
|
+
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
const ext = extname(entry.name)
|
|
194
|
+
if (entry.isFile() && COMPONENT_EXTENSIONS.has(ext)) {
|
|
195
|
+
const name = basename(entry.name, ext)
|
|
196
|
+
if (isComponentFileName(name)) {
|
|
197
|
+
fileNames.add(name)
|
|
198
|
+
}
|
|
199
|
+
} else if (entry.isDirectory()) {
|
|
200
|
+
dirNames.add(entry.name)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check for name collisions (e.g., Hero.jsx AND Hero/)
|
|
205
|
+
for (const name of fileNames) {
|
|
206
|
+
if (dirNames.has(name)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Name collision in ${sectionsRelPath}/: both "${name}.jsx" (or similar) and "${name}/" exist. ` +
|
|
209
|
+
`Use one or the other, not both.`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Discover bare files at root
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
if (!entry.isFile()) continue
|
|
217
|
+
const ext = extname(entry.name)
|
|
218
|
+
if (!COMPONENT_EXTENSIONS.has(ext)) continue
|
|
219
|
+
const name = basename(entry.name, ext)
|
|
220
|
+
if (!isComponentFileName(name)) continue
|
|
221
|
+
|
|
222
|
+
const meta = createImplicitMeta(name)
|
|
223
|
+
components[name] = {
|
|
224
|
+
...buildComponentEntry(name, sectionsRelPath, meta),
|
|
225
|
+
// Bare file: the entry file IS the file itself (not inside a subdirectory)
|
|
226
|
+
entryFile: entry.name,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Discover directories at root
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
if (!entry.isDirectory()) continue
|
|
233
|
+
if (!isComponentFileName(entry.name)) continue
|
|
234
|
+
|
|
235
|
+
const dirPath = join(fullPath, entry.name)
|
|
236
|
+
const relativePath = join(sectionsRelPath, entry.name)
|
|
237
|
+
const result = await loadComponentMeta(dirPath)
|
|
238
|
+
|
|
239
|
+
if (result && result.meta) {
|
|
240
|
+
// Has meta.js — use explicit meta
|
|
241
|
+
if (result.meta.exposed === false) continue
|
|
242
|
+
components[entry.name] = buildComponentEntry(entry.name, relativePath, result.meta)
|
|
243
|
+
} else if (hasEntryFile(dirPath, entry.name)) {
|
|
244
|
+
// No meta.js but has entry file — implicit section type at root
|
|
245
|
+
components[entry.name] = buildComponentEntry(entry.name, relativePath, createImplicitMeta(entry.name))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Recurse into subdirectories for nested section types (meta.js required)
|
|
249
|
+
await discoverNestedSections(srcDir, dirPath, relativePath, components)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return components
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Recursively discover nested section types that have meta.js
|
|
257
|
+
*
|
|
258
|
+
* @param {string} srcDir - Source directory
|
|
259
|
+
* @param {string} parentFullPath - Absolute path to parent directory
|
|
260
|
+
* @param {string} parentRelPath - Relative path from srcDir to parent
|
|
261
|
+
* @param {Object} components - Accumulator for discovered components
|
|
262
|
+
*/
|
|
263
|
+
async function discoverNestedSections(srcDir, parentFullPath, parentRelPath, components) {
|
|
264
|
+
let entries
|
|
265
|
+
try {
|
|
266
|
+
entries = await readdir(parentFullPath, { withFileTypes: true })
|
|
267
|
+
} catch {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (!entry.isDirectory()) continue
|
|
273
|
+
|
|
274
|
+
const dirPath = join(parentFullPath, entry.name)
|
|
275
|
+
const relativePath = join(parentRelPath, entry.name)
|
|
276
|
+
const result = await loadComponentMeta(dirPath)
|
|
277
|
+
|
|
278
|
+
if (result && result.meta) {
|
|
279
|
+
if (result.meta.exposed === false) continue
|
|
280
|
+
components[entry.name] = buildComponentEntry(entry.name, relativePath, result.meta)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Continue recursing regardless — deeper levels may have meta.js
|
|
284
|
+
await discoverNestedSections(srcDir, dirPath, relativePath, components)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Discover components in a non-sections path (meta.js required)
|
|
290
|
+
*
|
|
291
|
+
* @param {string} srcDir - Source directory (e.g., 'src')
|
|
292
|
+
* @param {string} relativePath - Path relative to srcDir (e.g., 'components')
|
|
119
293
|
* @returns {Object} Map of componentName -> { name, path, ...meta }
|
|
120
294
|
*/
|
|
121
|
-
async function
|
|
295
|
+
async function discoverExplicitComponentsInPath(srcDir, relativePath) {
|
|
122
296
|
const fullPath = join(srcDir, relativePath)
|
|
123
297
|
|
|
124
298
|
if (!existsSync(fullPath)) {
|
|
@@ -135,16 +309,12 @@ async function discoverComponentsInPath(srcDir, relativePath) {
|
|
|
135
309
|
const result = await loadComponentMeta(componentDir)
|
|
136
310
|
|
|
137
311
|
if (result && result.meta) {
|
|
138
|
-
// Check if explicitly
|
|
312
|
+
// Check if explicitly hidden from discovery
|
|
139
313
|
if (result.meta.exposed === false) {
|
|
140
314
|
continue
|
|
141
315
|
}
|
|
142
316
|
|
|
143
|
-
components[entry.name] =
|
|
144
|
-
name: entry.name,
|
|
145
|
-
path: join(relativePath, entry.name), // e.g., 'components/Hero' or 'components/sections/Hero'
|
|
146
|
-
...result.meta,
|
|
147
|
-
}
|
|
317
|
+
components[entry.name] = buildComponentEntry(entry.name, join(relativePath, entry.name), result.meta)
|
|
148
318
|
}
|
|
149
319
|
}
|
|
150
320
|
|
|
@@ -152,18 +322,25 @@ async function discoverComponentsInPath(srcDir, relativePath) {
|
|
|
152
322
|
}
|
|
153
323
|
|
|
154
324
|
/**
|
|
155
|
-
* Discover all
|
|
325
|
+
* Discover all section types in a foundation
|
|
326
|
+
*
|
|
327
|
+
* For the 'sections' path: relaxed discovery (bare files and folders at root,
|
|
328
|
+
* meta.js required for nested levels).
|
|
329
|
+
* For other paths: strict discovery (meta.js required).
|
|
156
330
|
*
|
|
157
331
|
* @param {string} srcDir - Source directory (e.g., 'src')
|
|
158
|
-
* @param {string[]} [componentPaths] - Paths to
|
|
159
|
-
* Default: ['components']
|
|
160
|
-
* @returns {Object} Map of
|
|
332
|
+
* @param {string[]} [componentPaths] - Paths to scan for section types (relative to srcDir).
|
|
333
|
+
* Default: ['sections', 'components']
|
|
334
|
+
* @returns {Object} Map of sectionTypeName -> { name, path, ...meta }
|
|
161
335
|
*/
|
|
162
336
|
export async function discoverComponents(srcDir, componentPaths = DEFAULT_COMPONENT_PATHS) {
|
|
163
337
|
const components = {}
|
|
164
338
|
|
|
165
339
|
for (const relativePath of componentPaths) {
|
|
166
|
-
|
|
340
|
+
// Use relaxed discovery for the primary sections path
|
|
341
|
+
const found = relativePath === SECTIONS_PATH
|
|
342
|
+
? await discoverSectionsInPath(srcDir, relativePath)
|
|
343
|
+
: await discoverExplicitComponentsInPath(srcDir, relativePath)
|
|
167
344
|
|
|
168
345
|
for (const [name, meta] of Object.entries(found)) {
|
|
169
346
|
if (components[name]) {
|
|
@@ -210,10 +387,10 @@ export async function buildSchema(srcDir, componentPaths) {
|
|
|
210
387
|
}
|
|
211
388
|
|
|
212
389
|
/**
|
|
213
|
-
* Get list of
|
|
390
|
+
* Get list of section type names
|
|
214
391
|
*
|
|
215
392
|
* @param {string} srcDir - Source directory
|
|
216
|
-
* @param {string[]} [componentPaths] - Paths to
|
|
393
|
+
* @param {string[]} [componentPaths] - Paths to scan for section types
|
|
217
394
|
*/
|
|
218
395
|
export async function getExposedComponents(srcDir, componentPaths) {
|
|
219
396
|
const components = await discoverComponents(srcDir, componentPaths)
|