@nuasite/cms 0.19.0 → 0.20.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.
- package/dist/editor.js +11055 -10934
- package/package.json +3 -3
- package/src/build-processor.ts +4 -4
- package/src/dev-middleware.ts +171 -185
- package/src/editor/components/fields.tsx +6 -6
- package/src/editor/components/markdown-editor-overlay.tsx +41 -46
- package/src/editor/components/markdown-inline-editor.tsx +34 -165
- package/src/editor/components/mdx-block-view.tsx +351 -47
- package/src/editor/components/mdx-component-picker.tsx +35 -11
- package/src/editor/components/media-library.tsx +1 -15
- package/src/editor/components/modal-shell.tsx +1 -1
- package/src/editor/milkdown-mdx-plugin.tsx +116 -19
- package/src/editor/milkdown-utils.ts +174 -0
- package/src/editor/signals.ts +1 -18
- package/src/editor/types.ts +0 -10
- package/src/html-processor.ts +9 -7
- package/src/index.ts +3 -13
- package/src/source-finder/cache.ts +47 -0
- package/src/source-finder/collection-finder.ts +181 -0
- package/src/source-finder/index.ts +5 -2
- package/src/source-finder/search-index.ts +79 -0
- package/src/source-finder/snippet-utils.ts +36 -61
- package/src/utils.ts +10 -0
- package/src/vite-plugin.ts +24 -4
- package/src/editor/components/mdx-props-editor.tsx +0 -94
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"directory": "packages/astro-cms"
|
|
15
15
|
},
|
|
16
16
|
"license": "Apache-2.0",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.20.1",
|
|
18
18
|
"module": "src/index.ts",
|
|
19
19
|
"types": "src/index.ts",
|
|
20
20
|
"type": "module",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@tailwindcss/vite": "^4.2.2",
|
|
46
46
|
"@types/bun": "1.3.11",
|
|
47
47
|
"clsx": "^2.1.1",
|
|
48
|
-
"marked": "^
|
|
48
|
+
"marked": "^18.0.0",
|
|
49
49
|
"preact": "^10.29.1",
|
|
50
50
|
"prosemirror-commands": "^1.7.1",
|
|
51
51
|
"prosemirror-inputrules": "^1.5.1",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"tailwind-merge": "^3.5.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"astro": "6.1.
|
|
62
|
+
"astro": "6.1.4",
|
|
63
63
|
"typescript": "^6.0.2",
|
|
64
64
|
"vite": "^7.0.0",
|
|
65
65
|
"@aws-sdk/client-s3": "^3.0.0"
|
package/src/build-processor.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { extractComponentName, processHtml } from './html-processor'
|
|
|
10
10
|
import type { ManifestWriter } from './manifest-writer'
|
|
11
11
|
import { generateComponentPreviews } from './preview-generator'
|
|
12
12
|
import {
|
|
13
|
+
clearCollectionTextIndex,
|
|
13
14
|
clearSourceFinderCache,
|
|
14
15
|
extractOpeningTagWithLine,
|
|
15
16
|
findCollectionSource,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
} from './source-finder'
|
|
24
25
|
import type { ComponentInstance } from './types'
|
|
25
26
|
import type { CmsMarkerOptions, CollectionEntry } from './types'
|
|
27
|
+
import { firstNonEmptyLine } from './utils'
|
|
26
28
|
|
|
27
29
|
// Concurrency limit for parallel processing
|
|
28
30
|
const MAX_CONCURRENT = 10
|
|
@@ -323,10 +325,7 @@ async function processFile(
|
|
|
323
325
|
}
|
|
324
326
|
|
|
325
327
|
// Get the first non-empty line of the markdown body for wrapper detection
|
|
326
|
-
const bodyFirstLine = mdContent?.body
|
|
327
|
-
?.split('\n')
|
|
328
|
-
.find((line) => line.trim().length > 0)
|
|
329
|
-
?.trim()
|
|
328
|
+
const bodyFirstLine = firstNonEmptyLine(mdContent?.body)
|
|
330
329
|
|
|
331
330
|
// Create ID generator - use atomic increment
|
|
332
331
|
const pageIdStart = idCounter.value
|
|
@@ -780,6 +779,7 @@ export async function processBuildOutput(
|
|
|
780
779
|
|
|
781
780
|
// Clear caches from previous builds and initialize search index
|
|
782
781
|
clearSourceFinderCache()
|
|
782
|
+
clearCollectionTextIndex()
|
|
783
783
|
|
|
784
784
|
const htmlFiles = await findHtmlFiles(outDir)
|
|
785
785
|
|
package/src/dev-middleware.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parse } from 'node-html-parser'
|
|
2
1
|
import fs from 'node:fs/promises'
|
|
3
2
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
3
|
import path from 'node:path'
|
|
@@ -17,15 +16,23 @@ import { processHtml } from './html-processor'
|
|
|
17
16
|
import type { ManifestWriter } from './manifest-writer'
|
|
18
17
|
import type { MediaStorageAdapter } from './media/types'
|
|
19
18
|
import {
|
|
20
|
-
clearSourceFinderCache,
|
|
21
19
|
findCollectionSource,
|
|
22
20
|
findImageSourceLocation,
|
|
23
21
|
findSourceLocation,
|
|
24
22
|
initializeSearchIndex,
|
|
25
23
|
parseMarkdownContent,
|
|
24
|
+
reindexDirtyFiles,
|
|
26
25
|
} from './source-finder'
|
|
27
|
-
import type {
|
|
28
|
-
|
|
26
|
+
import type {
|
|
27
|
+
CmsMarkerOptions,
|
|
28
|
+
CollectionDefinition,
|
|
29
|
+
CollectionEntry,
|
|
30
|
+
ComponentDefinition,
|
|
31
|
+
ComponentInstance,
|
|
32
|
+
ManifestEntry,
|
|
33
|
+
PageSeoData,
|
|
34
|
+
} from './types'
|
|
35
|
+
import { firstNonEmptyLine, normalizePagePath } from './utils'
|
|
29
36
|
|
|
30
37
|
/** Minimal ViteDevServer interface to avoid version conflicts between Astro's bundled Vite and root Vite */
|
|
31
38
|
interface ViteDevServerLike {
|
|
@@ -258,18 +265,23 @@ export function createDevMiddleware(
|
|
|
258
265
|
const html = Buffer.concat(chunks!).toString('utf8')
|
|
259
266
|
const pagePath = normalizePagePath(requestUrl)
|
|
260
267
|
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
.then(({ html: transformed, entries, components, collection, seo }) => {
|
|
268
|
+
// Phase 1 (fast): mark HTML with CMS IDs and build basic entries
|
|
269
|
+
markHtmlForDev(html, pagePath, config, idCounter, manifestWriter)
|
|
270
|
+
.then(({ html: transformed, entries, components, collection, seo, collectionDefinitions: colDefs }) => {
|
|
271
|
+
// Store basic manifest immediately so editor toolbar has data
|
|
264
272
|
manifestWriter.addPage(pagePath, entries, components, collection, seo)
|
|
265
273
|
|
|
274
|
+
// Send the marked HTML to the browser without waiting for source resolution
|
|
266
275
|
res.write = originalWrite
|
|
267
276
|
res.end = originalEnd
|
|
268
277
|
if (!res.headersSent) {
|
|
269
278
|
res.removeHeader('content-length')
|
|
270
279
|
}
|
|
280
|
+
res.end(transformed, ...args)
|
|
271
281
|
|
|
272
|
-
|
|
282
|
+
// Phase 2 (background): resolve source locations and enhance manifest
|
|
283
|
+
// This runs after the page is already visible to the user
|
|
284
|
+
enhanceManifestInBackground(pagePath, entries, components, collection, seo, colDefs, config, manifestWriter)
|
|
273
285
|
})
|
|
274
286
|
.catch((error) => {
|
|
275
287
|
console.error('[cms] Error transforming HTML:', error)
|
|
@@ -289,35 +301,35 @@ export function createDevMiddleware(
|
|
|
289
301
|
})
|
|
290
302
|
}
|
|
291
303
|
|
|
292
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Phase 1 (fast): Mark HTML with CMS IDs and build basic manifest entries.
|
|
306
|
+
* Returns quickly so the page can be sent to the browser without delay.
|
|
307
|
+
* Source resolution and snippet enhancement are deferred to Phase 2.
|
|
308
|
+
*/
|
|
309
|
+
async function markHtmlForDev(
|
|
293
310
|
html: string,
|
|
294
311
|
pagePath: string,
|
|
295
312
|
config: Required<CmsMarkerOptions>,
|
|
296
313
|
idCounter: { value: number },
|
|
297
314
|
manifestWriter: ManifestWriter,
|
|
298
315
|
) {
|
|
299
|
-
//
|
|
300
|
-
|
|
316
|
+
// Re-index only files that changed since last page load (tracked by Vite watcher).
|
|
317
|
+
await reindexDirtyFiles()
|
|
301
318
|
|
|
302
319
|
// In dev mode, reset counter per page for consistent IDs during HMR
|
|
303
320
|
let pageCounter = 0
|
|
304
321
|
const idGenerator = () => `cms-${pageCounter++}`
|
|
305
322
|
|
|
306
|
-
// Check if this is a collection page
|
|
323
|
+
// Check if this is a collection page
|
|
307
324
|
const collectionInfo = await findCollectionSource(pagePath, config.contentDir)
|
|
308
325
|
const isCollectionPage = !!collectionInfo
|
|
309
326
|
|
|
310
|
-
// Parse markdown content if this is a collection page
|
|
311
327
|
let mdContent: Awaited<ReturnType<typeof parseMarkdownContent>> | undefined
|
|
312
328
|
if (collectionInfo) {
|
|
313
329
|
mdContent = await parseMarkdownContent(collectionInfo)
|
|
314
330
|
}
|
|
315
331
|
|
|
316
|
-
|
|
317
|
-
const bodyFirstLine = mdContent?.body
|
|
318
|
-
?.split('\n')
|
|
319
|
-
.find((line) => line.trim().length > 0)
|
|
320
|
-
?.trim()
|
|
332
|
+
const bodyFirstLine = firstNonEmptyLine(mdContent?.body)
|
|
321
333
|
|
|
322
334
|
const result = await processHtml(
|
|
323
335
|
html,
|
|
@@ -330,214 +342,188 @@ async function processHtmlForDev(
|
|
|
330
342
|
generateManifest: config.generateManifest,
|
|
331
343
|
markComponents: config.markComponents,
|
|
332
344
|
componentDirs: config.componentDirs,
|
|
333
|
-
// Skip marking markdown-rendered content on collection pages
|
|
334
|
-
// The markdown body is treated as a single editable unit
|
|
335
345
|
skipMarkdownContent: isCollectionPage,
|
|
336
|
-
// Pass collection info for wrapper element marking
|
|
337
346
|
collectionInfo: collectionInfo
|
|
338
347
|
? { name: collectionInfo.name, slug: collectionInfo.slug, bodyFirstLine, bodyText: mdContent?.body, contentPath: collectionInfo.file }
|
|
339
348
|
: undefined,
|
|
340
|
-
// Pass SEO options
|
|
341
349
|
seo: config.seo,
|
|
342
|
-
// Pass collection definitions for resolving frontmatter text on listing pages
|
|
343
350
|
collectionDefinitions: manifestWriter.getCollectionDefinitions(),
|
|
344
351
|
},
|
|
345
352
|
idGenerator,
|
|
346
353
|
)
|
|
347
354
|
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
fileCache.set(filePath, null)
|
|
360
|
-
return null
|
|
355
|
+
// Build collection entry if this is a collection page
|
|
356
|
+
let collectionEntry: CollectionEntry | undefined
|
|
357
|
+
if (collectionInfo && mdContent) {
|
|
358
|
+
collectionEntry = {
|
|
359
|
+
collectionName: mdContent.collectionName,
|
|
360
|
+
collectionSlug: mdContent.collectionSlug,
|
|
361
|
+
sourcePath: mdContent.file,
|
|
362
|
+
frontmatter: mdContent.frontmatter,
|
|
363
|
+
body: mdContent.body,
|
|
364
|
+
bodyStartLine: mdContent.bodyStartLine,
|
|
365
|
+
wrapperId: result.collectionWrapperId,
|
|
361
366
|
}
|
|
362
367
|
}
|
|
363
368
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
369
|
+
return {
|
|
370
|
+
html: result.html,
|
|
371
|
+
entries: result.entries,
|
|
372
|
+
components: result.components,
|
|
373
|
+
collection: collectionEntry,
|
|
374
|
+
seo: result.seo,
|
|
375
|
+
collectionDefinitions: result.collectionDefinitions,
|
|
376
|
+
}
|
|
377
|
+
}
|
|
370
378
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
379
|
+
/**
|
|
380
|
+
* Phase 2 (background): Resolve source locations, enhance snippets, populate
|
|
381
|
+
* component props, and update the manifest. Runs after the HTML response is sent.
|
|
382
|
+
*/
|
|
383
|
+
async function enhanceManifestInBackground(
|
|
384
|
+
pagePath: string,
|
|
385
|
+
entries: Record<string, ManifestEntry>,
|
|
386
|
+
components: Record<string, ComponentInstance>,
|
|
387
|
+
collection: CollectionEntry | undefined,
|
|
388
|
+
seo: PageSeoData | undefined,
|
|
389
|
+
collectionDefinitions: Record<string, CollectionDefinition> | undefined,
|
|
390
|
+
config: Required<CmsMarkerOptions>,
|
|
391
|
+
manifestWriter: ManifestWriter,
|
|
392
|
+
): Promise<void> {
|
|
393
|
+
try {
|
|
394
|
+
// Populate component props from source invocations
|
|
395
|
+
const projectRoot = getProjectRoot()
|
|
396
|
+
const fileCache = new Map<string, string[] | null>()
|
|
397
|
+
const readLines = async (filePath: string): Promise<string[] | null> => {
|
|
398
|
+
if (fileCache.has(filePath)) return fileCache.get(filePath)!
|
|
399
|
+
try {
|
|
400
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
401
|
+
const lines = content.split('\n')
|
|
402
|
+
fileCache.set(filePath, lines)
|
|
403
|
+
return lines
|
|
404
|
+
} catch {
|
|
405
|
+
fileCache.set(filePath, null)
|
|
406
|
+
return null
|
|
381
407
|
}
|
|
382
408
|
}
|
|
383
409
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
410
|
+
for (const comp of Object.values(components)) {
|
|
411
|
+
if (comp.componentName.startsWith('__array:')) continue
|
|
412
|
+
|
|
413
|
+
let found = false
|
|
414
|
+
|
|
415
|
+
if (comp.invocationSourcePath) {
|
|
416
|
+
const filePath = normalizeFilePath(comp.invocationSourcePath)
|
|
417
|
+
const lines = await readLines(path.resolve(projectRoot, filePath))
|
|
388
418
|
if (lines) {
|
|
389
419
|
const invLine = findComponentInvocationLine(lines, comp.componentName, comp.invocationIndex ?? 0)
|
|
390
420
|
if (invLine >= 0) {
|
|
391
421
|
comp.props = extractPropsFromSource(lines, invLine, comp.componentName)
|
|
392
|
-
|
|
422
|
+
found = true
|
|
393
423
|
}
|
|
394
424
|
}
|
|
395
425
|
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
426
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
for (const group of componentGroups.values()) {
|
|
409
|
-
if (group.length < 1) continue
|
|
410
|
-
// Only process groups where at least one component has empty props (spread case)
|
|
411
|
-
if (!group.some(c => Object.keys(c.props).length === 0)) continue
|
|
412
|
-
|
|
413
|
-
const firstComp = group[0]!
|
|
414
|
-
const filePath = normalizeFilePath(firstComp.invocationSourcePath ?? firstComp.sourcePath)
|
|
415
|
-
const lines = await readLines(path.resolve(projectRoot, filePath))
|
|
416
|
-
if (!lines) continue
|
|
417
|
-
|
|
418
|
-
// For inline array components (__array:varName or __array:varName#N), find the .map() line
|
|
419
|
-
// directly instead of searching for a component tag that won't exist
|
|
420
|
-
let pattern: ReturnType<typeof detectArrayPattern>
|
|
421
|
-
const parsed = parseInlineArrayName(firstComp.componentName)
|
|
422
|
-
if (parsed) {
|
|
423
|
-
const { arrayVarName, mapOccurrence } = parsed
|
|
424
|
-
const fmEndCheck = findFrontmatterEnd(lines)
|
|
425
|
-
const mapRegex = new RegExp(buildMapPattern(arrayVarName))
|
|
426
|
-
let mapLine = -1
|
|
427
|
-
let seen = 0
|
|
428
|
-
for (let i = fmEndCheck; i < lines.length; i++) {
|
|
429
|
-
if (mapRegex.test(lines[i]!)) {
|
|
430
|
-
if (seen === mapOccurrence) {
|
|
431
|
-
mapLine = i
|
|
432
|
-
break
|
|
427
|
+
if (!found) {
|
|
428
|
+
for (const candidate of getPageFileCandidates(pagePath)) {
|
|
429
|
+
const lines = await readLines(path.resolve(projectRoot, candidate))
|
|
430
|
+
if (lines) {
|
|
431
|
+
const invLine = findComponentInvocationLine(lines, comp.componentName, comp.invocationIndex ?? 0)
|
|
432
|
+
if (invLine >= 0) {
|
|
433
|
+
comp.props = extractPropsFromSource(lines, invLine, comp.componentName)
|
|
434
|
+
break
|
|
435
|
+
}
|
|
433
436
|
}
|
|
434
|
-
seen++
|
|
435
437
|
}
|
|
436
438
|
}
|
|
437
|
-
if (mapLine < 0) continue
|
|
438
|
-
pattern = { arrayVarName, mapLineIndex: mapLine }
|
|
439
|
-
} else {
|
|
440
|
-
// Find the invocation line (occurrence 0, since .map() has a single <Component> tag)
|
|
441
|
-
const invLine = findComponentInvocationLine(lines, firstComp.componentName, 0)
|
|
442
|
-
if (invLine < 0) continue
|
|
443
|
-
pattern = detectArrayPattern(lines, invLine)
|
|
444
439
|
}
|
|
445
|
-
if (!pattern) continue
|
|
446
|
-
|
|
447
|
-
const fmEnd = findFrontmatterEnd(lines)
|
|
448
|
-
if (fmEnd === 0) continue
|
|
449
440
|
|
|
450
|
-
|
|
441
|
+
// Resolve spread props for array-rendered components
|
|
442
|
+
const componentGroups = new Map<string, typeof components[string][]>()
|
|
443
|
+
for (const comp of Object.values(components)) {
|
|
444
|
+
const key = `${comp.componentName}::${comp.invocationSourcePath ?? ''}`
|
|
445
|
+
if (!componentGroups.has(key)) componentGroups.set(key, [])
|
|
446
|
+
componentGroups.get(key)!.push(comp)
|
|
447
|
+
}
|
|
451
448
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const comp = sorted[i]!
|
|
456
|
-
if (Object.keys(comp.props).length > 0) continue
|
|
449
|
+
for (const group of componentGroups.values()) {
|
|
450
|
+
if (group.length < 1) continue
|
|
451
|
+
if (!group.some(c => Object.keys(c.props).length === 0)) continue
|
|
457
452
|
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
453
|
+
const firstComp = group[0]!
|
|
454
|
+
const filePath = normalizeFilePath(firstComp.invocationSourcePath ?? firstComp.sourcePath)
|
|
455
|
+
const lines = await readLines(path.resolve(projectRoot, filePath))
|
|
456
|
+
if (!lines) continue
|
|
457
|
+
|
|
458
|
+
const fmEnd = findFrontmatterEnd(lines)
|
|
459
|
+
|
|
460
|
+
let pattern: ReturnType<typeof detectArrayPattern>
|
|
461
|
+
const parsed = parseInlineArrayName(firstComp.componentName)
|
|
462
|
+
if (parsed) {
|
|
463
|
+
const { arrayVarName, mapOccurrence } = parsed
|
|
464
|
+
const mapRegex = new RegExp(buildMapPattern(arrayVarName))
|
|
465
|
+
let mapLine = -1
|
|
466
|
+
let seen = 0
|
|
467
|
+
for (let i = fmEnd; i < lines.length; i++) {
|
|
468
|
+
if (mapRegex.test(lines[i]!)) {
|
|
469
|
+
if (seen === mapOccurrence) {
|
|
470
|
+
mapLine = i
|
|
471
|
+
break
|
|
472
|
+
}
|
|
473
|
+
seen++
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (mapLine < 0) continue
|
|
477
|
+
pattern = { arrayVarName, mapLineIndex: mapLine }
|
|
478
|
+
} else {
|
|
479
|
+
const invLine = findComponentInvocationLine(lines, firstComp.componentName, 0)
|
|
480
|
+
if (invLine < 0) continue
|
|
481
|
+
pattern = detectArrayPattern(lines, invLine)
|
|
461
482
|
}
|
|
462
|
-
|
|
463
|
-
|
|
483
|
+
if (!pattern) continue
|
|
484
|
+
if (fmEnd === 0) continue
|
|
464
485
|
|
|
465
|
-
|
|
466
|
-
let collectionEntry: CollectionEntry | undefined
|
|
467
|
-
if (collectionInfo && mdContent) {
|
|
468
|
-
collectionEntry = {
|
|
469
|
-
collectionName: mdContent.collectionName,
|
|
470
|
-
collectionSlug: mdContent.collectionSlug,
|
|
471
|
-
sourcePath: mdContent.file,
|
|
472
|
-
frontmatter: mdContent.frontmatter,
|
|
473
|
-
body: mdContent.body,
|
|
474
|
-
bodyStartLine: mdContent.bodyStartLine,
|
|
475
|
-
wrapperId: result.collectionWrapperId,
|
|
476
|
-
}
|
|
477
|
-
}
|
|
486
|
+
const frontmatterContent = lines.slice(1, fmEnd - 1).join('\n')
|
|
478
487
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
// Re-resolve sources with the fully-built search index (the earlier enhancement
|
|
484
|
-
// step runs before the index is ready, so its results may be stale).
|
|
485
|
-
for (const entry of Object.values(result.entries)) {
|
|
486
|
-
if (entry.imageMetadata?.src) {
|
|
487
|
-
const imageSource = await findImageSourceLocation(entry.imageMetadata.src, entry.imageMetadata.srcSet)
|
|
488
|
-
if (imageSource) {
|
|
489
|
-
entry.sourcePath = imageSource.file
|
|
490
|
-
entry.sourceLine = imageSource.line
|
|
491
|
-
entry.sourceSnippet = imageSource.snippet
|
|
492
|
-
}
|
|
493
|
-
} else if (entry.text && entry.tag) {
|
|
494
|
-
const textSource = await findSourceLocation(entry.text, entry.tag)
|
|
495
|
-
if (textSource) {
|
|
496
|
-
entry.sourcePath = textSource.file
|
|
497
|
-
entry.sourceLine = textSource.line
|
|
498
|
-
entry.sourceSnippet = textSource.snippet
|
|
499
|
-
if (textSource.variableName) entry.variableName = textSource.variableName
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
488
|
+
const sorted = [...group].sort((a, b) => (a.invocationIndex ?? 0) - (b.invocationIndex ?? 0))
|
|
489
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
490
|
+
const comp = sorted[i]!
|
|
491
|
+
if (Object.keys(comp.props).length > 0) continue
|
|
503
492
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
// Remove entries that don't have a resolved sourcePath
|
|
510
|
-
if (!entry.sourcePath) {
|
|
511
|
-
idsToRemove.push(id)
|
|
512
|
-
delete result.entries[id]
|
|
493
|
+
const arrayProps = extractArrayElementProps(frontmatterContent, pattern.arrayVarName, i)
|
|
494
|
+
if (arrayProps) {
|
|
495
|
+
comp.props = arrayProps
|
|
496
|
+
}
|
|
497
|
+
}
|
|
513
498
|
}
|
|
514
|
-
}
|
|
515
499
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
500
|
+
// Ensure the search index is initialized
|
|
501
|
+
await initializeSearchIndex()
|
|
502
|
+
|
|
503
|
+
// Re-resolve sources with the search index
|
|
504
|
+
for (const entry of Object.values(entries)) {
|
|
505
|
+
if (entry.imageMetadata?.src) {
|
|
506
|
+
const imageSource = await findImageSourceLocation(entry.imageMetadata.src, entry.imageMetadata.srcSet)
|
|
507
|
+
if (imageSource) {
|
|
508
|
+
entry.sourcePath = imageSource.file
|
|
509
|
+
entry.sourceLine = imageSource.line
|
|
510
|
+
entry.sourceSnippet = imageSource.snippet
|
|
511
|
+
}
|
|
512
|
+
} else if (entry.text && entry.tag) {
|
|
513
|
+
const textSource = await findSourceLocation(entry.text, entry.tag)
|
|
514
|
+
if (textSource) {
|
|
515
|
+
entry.sourcePath = textSource.file
|
|
516
|
+
entry.sourceLine = textSource.line
|
|
517
|
+
entry.sourceSnippet = textSource.snippet
|
|
518
|
+
if (textSource.variableName) entry.variableName = textSource.variableName
|
|
519
|
+
}
|
|
530
520
|
}
|
|
531
521
|
}
|
|
532
|
-
finalHtml = root.toString()
|
|
533
|
-
}
|
|
534
522
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
collection: collectionEntry,
|
|
540
|
-
seo: result.seo,
|
|
523
|
+
// Update the manifest with fully-resolved entries and component props
|
|
524
|
+
manifestWriter.addPage(pagePath, entries, components, collection, seo)
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error('[cms] Background enhancement failed:', error)
|
|
541
527
|
}
|
|
542
528
|
}
|
|
543
529
|
|
|
@@ -83,18 +83,18 @@ export function ImageField({ label, value, placeholder, onChange, onBrowse, isDi
|
|
|
83
83
|
const hasImage = !!value && value.length > 0
|
|
84
84
|
|
|
85
85
|
return (
|
|
86
|
-
<div class="space-y-1.5">
|
|
86
|
+
<div class="space-y-1.5 min-w-0">
|
|
87
87
|
<FieldLabel label={label} isDirty={isDirty} onReset={onReset} />
|
|
88
88
|
{hasImage && (
|
|
89
89
|
<div
|
|
90
|
-
class="relative w-full
|
|
90
|
+
class="relative w-full rounded-cms-sm overflow-hidden bg-white/5 border border-white/10 cursor-pointer group"
|
|
91
91
|
onClick={onBrowse}
|
|
92
92
|
data-cms-ui
|
|
93
93
|
>
|
|
94
94
|
<img
|
|
95
95
|
src={value}
|
|
96
96
|
alt={label}
|
|
97
|
-
class="w-full h-
|
|
97
|
+
class="w-full h-auto max-h-48"
|
|
98
98
|
onError={(e) => {
|
|
99
99
|
;(e.target as HTMLImageElement).style.display = 'none'
|
|
100
100
|
}}
|
|
@@ -104,14 +104,14 @@ export function ImageField({ label, value, placeholder, onChange, onBrowse, isDi
|
|
|
104
104
|
</div>
|
|
105
105
|
</div>
|
|
106
106
|
)}
|
|
107
|
-
<div class="flex gap-2">
|
|
107
|
+
<div class="flex gap-2 min-w-0">
|
|
108
108
|
<input
|
|
109
109
|
type="text"
|
|
110
110
|
value={value ?? ''}
|
|
111
111
|
placeholder={placeholder}
|
|
112
112
|
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
113
113
|
class={cn(
|
|
114
|
-
'flex-1 px-3 py-2 bg-white/10 border rounded-cms-sm text-sm text-white placeholder:text-white/40 focus:outline-none focus:ring-1 transition-colors',
|
|
114
|
+
'flex-1 min-w-0 px-3 py-2 bg-white/10 border rounded-cms-sm text-sm text-white placeholder:text-white/40 focus:outline-none focus:ring-1 transition-colors',
|
|
115
115
|
isDirty
|
|
116
116
|
? 'border-cms-primary focus:border-cms-primary focus:ring-cms-primary/30'
|
|
117
117
|
: 'border-white/20 focus:border-white/40 focus:ring-white/10',
|
|
@@ -121,7 +121,7 @@ export function ImageField({ label, value, placeholder, onChange, onBrowse, isDi
|
|
|
121
121
|
<button
|
|
122
122
|
type="button"
|
|
123
123
|
onClick={onBrowse}
|
|
124
|
-
class="px-3 py-2 bg-white/10 hover:bg-white/20 border border-white/20 rounded-cms-sm text-sm text-white transition-colors cursor-pointer"
|
|
124
|
+
class="shrink-0 px-3 py-2 bg-white/10 hover:bg-white/20 border border-white/20 rounded-cms-sm text-sm text-white transition-colors cursor-pointer"
|
|
125
125
|
data-cms-ui
|
|
126
126
|
>
|
|
127
127
|
Browse
|