@nuasite/cms 0.19.1 → 0.20.2

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.
Files changed (39) hide show
  1. package/dist/editor.js +12615 -12689
  2. package/package.json +3 -3
  3. package/src/build-processor.ts +4 -4
  4. package/src/dev-middleware.ts +185 -189
  5. package/src/editor/api.ts +0 -251
  6. package/src/editor/components/fields.tsx +6 -6
  7. package/src/editor/components/markdown-editor-overlay.tsx +46 -70
  8. package/src/editor/components/markdown-inline-editor.tsx +34 -165
  9. package/src/editor/components/mdx-block-view.tsx +351 -47
  10. package/src/editor/components/mdx-component-picker.tsx +35 -11
  11. package/src/editor/components/media-library.tsx +1 -15
  12. package/src/editor/components/modal-shell.tsx +1 -1
  13. package/src/editor/components/toolbar.tsx +0 -75
  14. package/src/editor/constants.ts +0 -4
  15. package/src/editor/editor.ts +2 -192
  16. package/src/editor/hooks/index.ts +0 -3
  17. package/src/editor/hooks/useBlockEditorHandlers.ts +1 -8
  18. package/src/editor/hooks/useTooltipState.ts +1 -2
  19. package/src/editor/index.tsx +2 -18
  20. package/src/editor/milkdown-mdx-plugin.tsx +116 -19
  21. package/src/editor/milkdown-utils.ts +174 -0
  22. package/src/editor/post-message.ts +0 -6
  23. package/src/editor/signals.ts +0 -183
  24. package/src/editor/styles.css +0 -108
  25. package/src/editor/types.ts +0 -76
  26. package/src/html-processor.ts +9 -7
  27. package/src/source-finder/cache.ts +47 -0
  28. package/src/source-finder/collection-finder.ts +181 -0
  29. package/src/source-finder/index.ts +5 -2
  30. package/src/source-finder/search-index.ts +79 -0
  31. package/src/source-finder/snippet-utils.ts +36 -61
  32. package/src/types.ts +0 -4
  33. package/src/utils.ts +10 -0
  34. package/src/vite-plugin.ts +24 -4
  35. package/src/editor/ai.ts +0 -185
  36. package/src/editor/components/ai-chat.tsx +0 -631
  37. package/src/editor/components/ai-tooltip.tsx +0 -180
  38. package/src/editor/components/mdx-props-editor.tsx +0 -94
  39. package/src/editor/hooks/useAIHandlers.ts +0 -345
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.19.1",
17
+ "version": "0.20.2",
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": "^17.0.6",
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.3",
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"
@@ -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
 
@@ -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 { CmsMarkerOptions, CollectionEntry, ComponentDefinition } from './types'
28
- import { normalizePagePath } from './utils'
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 {
@@ -37,6 +44,9 @@ interface ViteDevServerLike {
37
44
  on: (event: string, listener: (...args: any[]) => void) => any
38
45
  removeListener: (event: string, listener: (...args: any[]) => void) => any
39
46
  }
47
+ ws?: {
48
+ send: (payload: { type: string; path?: string }) => void
49
+ }
40
50
  }
41
51
 
42
52
  /**
@@ -104,12 +114,19 @@ export function createDevMiddleware(
104
114
 
105
115
  const route = url.replace('/_nua/cms/', '').split('?')[0]!
106
116
 
107
- handleCmsApiRoute(route, req, res, manifestWriter, config.contentDir, options.mediaAdapter).catch(
108
- (error) => {
117
+ handleCmsApiRoute(route, req, res, manifestWriter, config.contentDir, options.mediaAdapter)
118
+ .then(() => {
119
+ // Explicitly trigger full-reload after content-modifying routes.
120
+ // In sandboxed environments (e.g. E2B), chokidar file watcher events
121
+ // may not fire reliably, so we send the HMR event directly.
122
+ if (req.method === 'POST' && server.ws) {
123
+ server.ws.send({ type: 'full-reload' })
124
+ }
125
+ })
126
+ .catch((error) => {
109
127
  console.error('[astro-cms] API error:', error)
110
128
  sendError(res, 'Internal server error', 500)
111
- },
112
- )
129
+ })
113
130
  })
114
131
  }
115
132
 
@@ -258,18 +275,23 @@ export function createDevMiddleware(
258
275
  const html = Buffer.concat(chunks!).toString('utf8')
259
276
  const pagePath = normalizePagePath(requestUrl)
260
277
 
261
- // Process HTML asynchronously
262
- processHtmlForDev(html, pagePath, config, idCounter, manifestWriter)
263
- .then(({ html: transformed, entries, components, collection, seo }) => {
278
+ // Phase 1 (fast): mark HTML with CMS IDs and build basic entries
279
+ markHtmlForDev(html, pagePath, config, idCounter, manifestWriter)
280
+ .then(({ html: transformed, entries, components, collection, seo, collectionDefinitions: colDefs }) => {
281
+ // Store basic manifest immediately so editor toolbar has data
264
282
  manifestWriter.addPage(pagePath, entries, components, collection, seo)
265
283
 
284
+ // Send the marked HTML to the browser without waiting for source resolution
266
285
  res.write = originalWrite
267
286
  res.end = originalEnd
268
287
  if (!res.headersSent) {
269
288
  res.removeHeader('content-length')
270
289
  }
290
+ res.end(transformed, ...args)
271
291
 
272
- return res.end(transformed, ...args)
292
+ // Phase 2 (background): resolve source locations and enhance manifest
293
+ // This runs after the page is already visible to the user
294
+ enhanceManifestInBackground(pagePath, entries, components, collection, seo, colDefs, config, manifestWriter)
273
295
  })
274
296
  .catch((error) => {
275
297
  console.error('[cms] Error transforming HTML:', error)
@@ -289,35 +311,35 @@ export function createDevMiddleware(
289
311
  })
290
312
  }
291
313
 
292
- async function processHtmlForDev(
314
+ /**
315
+ * Phase 1 (fast): Mark HTML with CMS IDs and build basic manifest entries.
316
+ * Returns quickly so the page can be sent to the browser without delay.
317
+ * Source resolution and snippet enhancement are deferred to Phase 2.
318
+ */
319
+ async function markHtmlForDev(
293
320
  html: string,
294
321
  pagePath: string,
295
322
  config: Required<CmsMarkerOptions>,
296
323
  idCounter: { value: number },
297
324
  manifestWriter: ManifestWriter,
298
325
  ) {
299
- // Clear cached parsed files so variable definitions reflect the latest source
300
- clearSourceFinderCache()
326
+ // Re-index only files that changed since last page load (tracked by Vite watcher).
327
+ await reindexDirtyFiles()
301
328
 
302
329
  // In dev mode, reset counter per page for consistent IDs during HMR
303
330
  let pageCounter = 0
304
331
  const idGenerator = () => `cms-${pageCounter++}`
305
332
 
306
- // Check if this is a collection page (e.g., /services/example -> services collection, example slug)
333
+ // Check if this is a collection page
307
334
  const collectionInfo = await findCollectionSource(pagePath, config.contentDir)
308
335
  const isCollectionPage = !!collectionInfo
309
336
 
310
- // Parse markdown content if this is a collection page
311
337
  let mdContent: Awaited<ReturnType<typeof parseMarkdownContent>> | undefined
312
338
  if (collectionInfo) {
313
339
  mdContent = await parseMarkdownContent(collectionInfo)
314
340
  }
315
341
 
316
- // Get the first non-empty line of the markdown body for wrapper detection
317
- const bodyFirstLine = mdContent?.body
318
- ?.split('\n')
319
- .find((line) => line.trim().length > 0)
320
- ?.trim()
342
+ const bodyFirstLine = firstNonEmptyLine(mdContent?.body)
321
343
 
322
344
  const result = await processHtml(
323
345
  html,
@@ -330,214 +352,188 @@ async function processHtmlForDev(
330
352
  generateManifest: config.generateManifest,
331
353
  markComponents: config.markComponents,
332
354
  componentDirs: config.componentDirs,
333
- // Skip marking markdown-rendered content on collection pages
334
- // The markdown body is treated as a single editable unit
335
355
  skipMarkdownContent: isCollectionPage,
336
- // Pass collection info for wrapper element marking
337
356
  collectionInfo: collectionInfo
338
357
  ? { name: collectionInfo.name, slug: collectionInfo.slug, bodyFirstLine, bodyText: mdContent?.body, contentPath: collectionInfo.file }
339
358
  : undefined,
340
- // Pass SEO options
341
359
  seo: config.seo,
342
- // Pass collection definitions for resolving frontmatter text on listing pages
343
360
  collectionDefinitions: manifestWriter.getCollectionDefinitions(),
344
361
  },
345
362
  idGenerator,
346
363
  )
347
364
 
348
- // Populate component props from source invocations
349
- const projectRoot = getProjectRoot()
350
- const fileCache = new Map<string, string[] | null>()
351
- const readLines = async (filePath: string): Promise<string[] | null> => {
352
- if (fileCache.has(filePath)) return fileCache.get(filePath)!
353
- try {
354
- const content = await fs.readFile(filePath, 'utf-8')
355
- const lines = content.split('\n')
356
- fileCache.set(filePath, lines)
357
- return lines
358
- } catch {
359
- fileCache.set(filePath, null)
360
- return null
365
+ // Build collection entry if this is a collection page
366
+ let collectionEntry: CollectionEntry | undefined
367
+ if (collectionInfo && mdContent) {
368
+ collectionEntry = {
369
+ collectionName: mdContent.collectionName,
370
+ collectionSlug: mdContent.collectionSlug,
371
+ sourcePath: mdContent.file,
372
+ frontmatter: mdContent.frontmatter,
373
+ body: mdContent.body,
374
+ bodyStartLine: mdContent.bodyStartLine,
375
+ wrapperId: result.collectionWrapperId,
361
376
  }
362
377
  }
363
378
 
364
- for (const comp of Object.values(result.components)) {
365
- // Skip inline array components — they have no <Tag> in source;
366
- // their props are resolved in the array-group pass below
367
- if (comp.componentName.startsWith('__array:')) continue
368
-
369
- let found = false
379
+ return {
380
+ html: result.html,
381
+ entries: result.entries,
382
+ components: result.components,
383
+ collection: collectionEntry,
384
+ seo: result.seo,
385
+ collectionDefinitions: result.collectionDefinitions,
386
+ }
387
+ }
370
388
 
371
- // Try invocationSourcePath first (may point to a layout, not the page)
372
- if (comp.invocationSourcePath) {
373
- const filePath = normalizeFilePath(comp.invocationSourcePath)
374
- const lines = await readLines(path.resolve(projectRoot, filePath))
375
- if (lines) {
376
- const invLine = findComponentInvocationLine(lines, comp.componentName, comp.invocationIndex ?? 0)
377
- if (invLine >= 0) {
378
- comp.props = extractPropsFromSource(lines, invLine, comp.componentName)
379
- found = true
380
- }
389
+ /**
390
+ * Phase 2 (background): Resolve source locations, enhance snippets, populate
391
+ * component props, and update the manifest. Runs after the HTML response is sent.
392
+ */
393
+ async function enhanceManifestInBackground(
394
+ pagePath: string,
395
+ entries: Record<string, ManifestEntry>,
396
+ components: Record<string, ComponentInstance>,
397
+ collection: CollectionEntry | undefined,
398
+ seo: PageSeoData | undefined,
399
+ collectionDefinitions: Record<string, CollectionDefinition> | undefined,
400
+ config: Required<CmsMarkerOptions>,
401
+ manifestWriter: ManifestWriter,
402
+ ): Promise<void> {
403
+ try {
404
+ // Populate component props from source invocations
405
+ const projectRoot = getProjectRoot()
406
+ const fileCache = new Map<string, string[] | null>()
407
+ const readLines = async (filePath: string): Promise<string[] | null> => {
408
+ if (fileCache.has(filePath)) return fileCache.get(filePath)!
409
+ try {
410
+ const content = await fs.readFile(filePath, 'utf-8')
411
+ const lines = content.split('\n')
412
+ fileCache.set(filePath, lines)
413
+ return lines
414
+ } catch {
415
+ fileCache.set(filePath, null)
416
+ return null
381
417
  }
382
418
  }
383
419
 
384
- // Fallback: search page source file candidates
385
- if (!found) {
386
- for (const candidate of getPageFileCandidates(pagePath)) {
387
- const lines = await readLines(path.resolve(projectRoot, candidate))
420
+ for (const comp of Object.values(components)) {
421
+ if (comp.componentName.startsWith('__array:')) continue
422
+
423
+ let found = false
424
+
425
+ if (comp.invocationSourcePath) {
426
+ const filePath = normalizeFilePath(comp.invocationSourcePath)
427
+ const lines = await readLines(path.resolve(projectRoot, filePath))
388
428
  if (lines) {
389
429
  const invLine = findComponentInvocationLine(lines, comp.componentName, comp.invocationIndex ?? 0)
390
430
  if (invLine >= 0) {
391
431
  comp.props = extractPropsFromSource(lines, invLine, comp.componentName)
392
- break
432
+ found = true
393
433
  }
394
434
  }
395
435
  }
396
- }
397
- }
398
-
399
- // Resolve spread props for array-rendered components.
400
- // Group components by (name, invocationSourcePath) to detect array patterns.
401
- const componentGroups = new Map<string, typeof result.components[string][]>()
402
- for (const comp of Object.values(result.components)) {
403
- const key = `${comp.componentName}::${comp.invocationSourcePath ?? ''}`
404
- if (!componentGroups.has(key)) componentGroups.set(key, [])
405
- componentGroups.get(key)!.push(comp)
406
- }
407
436
 
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
437
+ if (!found) {
438
+ for (const candidate of getPageFileCandidates(pagePath)) {
439
+ const lines = await readLines(path.resolve(projectRoot, candidate))
440
+ if (lines) {
441
+ const invLine = findComponentInvocationLine(lines, comp.componentName, comp.invocationIndex ?? 0)
442
+ if (invLine >= 0) {
443
+ comp.props = extractPropsFromSource(lines, invLine, comp.componentName)
444
+ break
445
+ }
433
446
  }
434
- seen++
435
447
  }
436
448
  }
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
449
  }
445
- if (!pattern) continue
446
-
447
- const fmEnd = findFrontmatterEnd(lines)
448
- if (fmEnd === 0) continue
449
450
 
450
- const frontmatterContent = lines.slice(1, fmEnd - 1).join('\n')
451
+ // Resolve spread props for array-rendered components
452
+ const componentGroups = new Map<string, typeof components[string][]>()
453
+ for (const comp of Object.values(components)) {
454
+ const key = `${comp.componentName}::${comp.invocationSourcePath ?? ''}`
455
+ if (!componentGroups.has(key)) componentGroups.set(key, [])
456
+ componentGroups.get(key)!.push(comp)
457
+ }
451
458
 
452
- // Sort group by invocationIndex to match array element order
453
- const sorted = [...group].sort((a, b) => (a.invocationIndex ?? 0) - (b.invocationIndex ?? 0))
454
- for (let i = 0; i < sorted.length; i++) {
455
- const comp = sorted[i]!
456
- if (Object.keys(comp.props).length > 0) continue
459
+ for (const group of componentGroups.values()) {
460
+ if (group.length < 1) continue
461
+ if (!group.some(c => Object.keys(c.props).length === 0)) continue
457
462
 
458
- const arrayProps = extractArrayElementProps(frontmatterContent, pattern.arrayVarName, i)
459
- if (arrayProps) {
460
- comp.props = arrayProps
463
+ const firstComp = group[0]!
464
+ const filePath = normalizeFilePath(firstComp.invocationSourcePath ?? firstComp.sourcePath)
465
+ const lines = await readLines(path.resolve(projectRoot, filePath))
466
+ if (!lines) continue
467
+
468
+ const fmEnd = findFrontmatterEnd(lines)
469
+
470
+ let pattern: ReturnType<typeof detectArrayPattern>
471
+ const parsed = parseInlineArrayName(firstComp.componentName)
472
+ if (parsed) {
473
+ const { arrayVarName, mapOccurrence } = parsed
474
+ const mapRegex = new RegExp(buildMapPattern(arrayVarName))
475
+ let mapLine = -1
476
+ let seen = 0
477
+ for (let i = fmEnd; i < lines.length; i++) {
478
+ if (mapRegex.test(lines[i]!)) {
479
+ if (seen === mapOccurrence) {
480
+ mapLine = i
481
+ break
482
+ }
483
+ seen++
484
+ }
485
+ }
486
+ if (mapLine < 0) continue
487
+ pattern = { arrayVarName, mapLineIndex: mapLine }
488
+ } else {
489
+ const invLine = findComponentInvocationLine(lines, firstComp.componentName, 0)
490
+ if (invLine < 0) continue
491
+ pattern = detectArrayPattern(lines, invLine)
461
492
  }
462
- }
463
- }
493
+ if (!pattern) continue
494
+ if (fmEnd === 0) continue
464
495
 
465
- // Build collection entry if this is a collection page
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
- }
496
+ const frontmatterContent = lines.slice(1, fmEnd - 1).join('\n')
478
497
 
479
- // Ensure the search index is initialized for image source lookups
480
- // (idempotent - only scans files on first call)
481
- await initializeSearchIndex()
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
- }
498
+ const sorted = [...group].sort((a, b) => (a.invocationIndex ?? 0) - (b.invocationIndex ?? 0))
499
+ for (let i = 0; i < sorted.length; i++) {
500
+ const comp = sorted[i]!
501
+ if (Object.keys(comp.props).length > 0) continue
503
502
 
504
- // Filter out entries without sourcePath - these can't be edited
505
- const idsToRemove: string[] = []
506
- for (const [id, entry] of Object.entries(result.entries)) {
507
- // Keep collection wrapper entries even without sourcePath (they use contentPath)
508
- if (entry.collectionName) continue
509
- // Remove entries that don't have a resolved sourcePath
510
- if (!entry.sourcePath) {
511
- idsToRemove.push(id)
512
- delete result.entries[id]
503
+ const arrayProps = extractArrayElementProps(frontmatterContent, pattern.arrayVarName, i)
504
+ if (arrayProps) {
505
+ comp.props = arrayProps
506
+ }
507
+ }
513
508
  }
514
- }
515
509
 
516
- // Remove CMS ID attributes from HTML for entries that were filtered out
517
- let finalHtml = result.html
518
- if (idsToRemove.length > 0) {
519
- const root = parse(result.html, {
520
- lowerCaseTagName: false,
521
- comment: true,
522
- })
523
- for (const id of idsToRemove) {
524
- const element = root.querySelector(`[${config.attributeName}="${id}"]`)
525
- if (element) {
526
- element.removeAttribute(config.attributeName)
527
- // Also remove related CMS attributes
528
- element.removeAttribute('data-cms-img')
529
- element.removeAttribute('data-cms-markdown')
510
+ // Ensure the search index is initialized
511
+ await initializeSearchIndex()
512
+
513
+ // Re-resolve sources with the search index
514
+ for (const entry of Object.values(entries)) {
515
+ if (entry.imageMetadata?.src) {
516
+ const imageSource = await findImageSourceLocation(entry.imageMetadata.src, entry.imageMetadata.srcSet)
517
+ if (imageSource) {
518
+ entry.sourcePath = imageSource.file
519
+ entry.sourceLine = imageSource.line
520
+ entry.sourceSnippet = imageSource.snippet
521
+ }
522
+ } else if (entry.text && entry.tag) {
523
+ const textSource = await findSourceLocation(entry.text, entry.tag)
524
+ if (textSource) {
525
+ entry.sourcePath = textSource.file
526
+ entry.sourceLine = textSource.line
527
+ entry.sourceSnippet = textSource.snippet
528
+ if (textSource.variableName) entry.variableName = textSource.variableName
529
+ }
530
530
  }
531
531
  }
532
- finalHtml = root.toString()
533
- }
534
532
 
535
- return {
536
- html: finalHtml,
537
- entries: result.entries,
538
- components: result.components,
539
- collection: collectionEntry,
540
- seo: result.seo,
533
+ // Update the manifest with fully-resolved entries and component props
534
+ manifestWriter.addPage(pagePath, entries, components, collection, seo)
535
+ } catch (error) {
536
+ console.error('[cms] Background enhancement failed:', error)
541
537
  }
542
538
  }
543
539