@nuasite/cms-marker 0.0.52 → 0.0.54

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/README.md +0 -4
  2. package/dist/types/astro-transform.d.ts +21 -0
  3. package/dist/types/astro-transform.d.ts.map +1 -0
  4. package/dist/types/build-processor.d.ts +10 -0
  5. package/dist/types/build-processor.d.ts.map +1 -0
  6. package/dist/types/component-registry.d.ts +63 -0
  7. package/dist/types/component-registry.d.ts.map +1 -0
  8. package/dist/types/dev-middleware.d.ts +7 -0
  9. package/dist/types/dev-middleware.d.ts.map +1 -0
  10. package/dist/types/html-processor.d.ts +51 -0
  11. package/dist/types/html-processor.d.ts.map +1 -0
  12. package/dist/types/index.d.ts +7 -0
  13. package/dist/types/index.d.ts.map +1 -0
  14. package/dist/types/manifest-writer.d.ts +75 -0
  15. package/dist/types/manifest-writer.d.ts.map +1 -0
  16. package/dist/types/source-finder.d.ts +97 -0
  17. package/dist/types/source-finder.d.ts.map +1 -0
  18. package/dist/types/tailwind-colors.d.ts +66 -0
  19. package/dist/types/tailwind-colors.d.ts.map +1 -0
  20. package/dist/types/tsconfig.tsbuildinfo +1 -0
  21. package/dist/types/types.d.ts +195 -0
  22. package/dist/types/types.d.ts.map +1 -0
  23. package/dist/types/utils.d.ts +38 -0
  24. package/dist/types/utils.d.ts.map +1 -0
  25. package/dist/types/vite-plugin.d.ts +14 -0
  26. package/dist/types/vite-plugin.d.ts.map +1 -0
  27. package/package.json +1 -1
  28. package/src/astro-transform.ts +4 -4
  29. package/src/build-processor.ts +1 -1
  30. package/src/component-registry.ts +2 -2
  31. package/src/dev-middleware.ts +6 -6
  32. package/src/html-processor.ts +180 -6
  33. package/src/index.ts +0 -1
  34. package/src/manifest-writer.ts +47 -1
  35. package/src/source-finder.ts +142 -9
  36. package/src/tailwind-colors.ts +338 -0
  37. package/src/tsconfig.json +1 -1
  38. package/src/types.ts +123 -1
  39. package/src/utils.ts +99 -0
@@ -0,0 +1,38 @@
1
+ import type { ManifestEntry, SourceContext } from './types';
2
+ /**
3
+ * Generate a SHA256 hash of the given content
4
+ */
5
+ export declare function sha256(content: string): string;
6
+ /**
7
+ * Generate a short hash (first 12 characters of SHA256)
8
+ * Used for stableId to keep it reasonably short but still unique
9
+ */
10
+ export declare function shortHash(content: string): string;
11
+ /**
12
+ * Generate a stable ID for an element based on its content and context.
13
+ * This ID survives rebuilds as long as the content and structure remain similar.
14
+ *
15
+ * Components:
16
+ * - tag name
17
+ * - first 50 chars of text content
18
+ * - source path (if available)
19
+ * - parent tag
20
+ * - sibling index
21
+ */
22
+ export declare function generateStableId(tag: string, text: string, sourcePath?: string, context?: SourceContext): string;
23
+ /**
24
+ * Generate a hash of the source snippet for conflict detection.
25
+ * If the source file changes, this hash will differ from what's stored in manifest.
26
+ */
27
+ export declare function generateSourceHash(sourceSnippet: string): string;
28
+ /**
29
+ * Generate a content hash for the entire manifest (all entries).
30
+ * Used for quick drift detection without comparing individual entries.
31
+ */
32
+ export declare function generateManifestContentHash(entries: Record<string, ManifestEntry>): string;
33
+ /**
34
+ * Generate per-source-file hashes for granular conflict detection.
35
+ * Maps source file path -> hash of all entries from that file.
36
+ */
37
+ export declare function generateSourceFileHashes(entries: Record<string, ManifestEntry>): Record<string, string>;
38
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE3D;;GAEG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,aAAa,GACrB,MAAM,CAUR;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,MAAM,CAU1F;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBvG"}
@@ -0,0 +1,14 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { ManifestWriter } from './manifest-writer';
3
+ import type { CmsMarkerOptions, ComponentDefinition } from './types';
4
+ export interface VitePluginContext {
5
+ manifestWriter: ManifestWriter;
6
+ componentDefinitions: Record<string, ComponentDefinition>;
7
+ config: Required<CmsMarkerOptions>;
8
+ idCounter: {
9
+ value: number;
10
+ };
11
+ command: 'dev' | 'build' | 'preview' | 'sync';
12
+ }
13
+ export declare function createVitePlugin(context: VitePluginContext): Plugin[];
14
+ //# sourceMappingURL=vite-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAElC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAEpE,MAAM,WAAW,iBAAiB;IACjC,cAAc,EAAE,cAAc,CAAA;IAC9B,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IACzD,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAA;IAClC,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5B,OAAO,EAAE,KAAK,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;CAC7C;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,EAAE,CAoCrE"}
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "directory": "packages/cms-marker"
15
15
  },
16
16
  "license": "Apache-2.0",
17
- "version": "0.0.52",
17
+ "version": "0.0.54",
18
18
  "module": "src/index.ts",
19
19
  "types": "src/index.ts",
20
20
  "type": "module",
@@ -182,8 +182,8 @@ function injectSourceAttributes(code: string, ast: any, filePath: string): strin
182
182
  // Validate line exists - if not, there's a bug in AST positions or bounds checking
183
183
  if (line === undefined) {
184
184
  console.error(
185
- `[astro-cms-marker] Invalid modification at line ${mod.line + 1} in ${filePath}. ` +
186
- `This indicates a bug in @astrojs/compiler AST positions or bounds checking. Skipping modification.`
185
+ `[astro-cms-marker] Invalid modification at line ${mod.line + 1} in ${filePath}. `
186
+ + `This indicates a bug in @astrojs/compiler AST positions or bounds checking. Skipping modification.`,
187
187
  )
188
188
  continue
189
189
  }
@@ -191,8 +191,8 @@ function injectSourceAttributes(code: string, ast: any, filePath: string): strin
191
191
  // Validate column is within line bounds
192
192
  if (mod.column < 0 || mod.column > line.length) {
193
193
  console.error(
194
- `[astro-cms-marker] Invalid column ${mod.column} at line ${mod.line + 1} in ${filePath}. ` +
195
- `Line length is ${line.length}. Skipping modification.`
194
+ `[astro-cms-marker] Invalid column ${mod.column} at line ${mod.line + 1} in ${filePath}. `
195
+ + `Line length is ${line.length}. Skipping modification.`,
196
196
  )
197
197
  continue
198
198
  }
@@ -86,7 +86,7 @@ async function processFile(
86
86
  skipMarkdownContent: isCollectionPage,
87
87
  // Pass collection info for wrapper element marking
88
88
  collectionInfo: collectionInfo
89
- ? { name: collectionInfo.name, slug: collectionInfo.slug, bodyFirstLine }
89
+ ? { name: collectionInfo.name, slug: collectionInfo.slug, bodyFirstLine, contentPath: collectionInfo.file }
90
90
  : undefined,
91
91
  },
92
92
  idGenerator,
@@ -339,8 +339,8 @@ export function parseComponentUsage(
339
339
  'g',
340
340
  )
341
341
 
342
- let lineIndex = 0
343
- let charIndex = 0
342
+ const lineIndex = 0
343
+ const charIndex = 0
344
344
 
345
345
  for (let i = 0; i < lines.length; i++) {
346
346
  const line = lines[i]
@@ -2,7 +2,7 @@ import type { ViteDevServer } from 'vite'
2
2
  import { processHtml } from './html-processor'
3
3
  import type { ManifestWriter } from './manifest-writer'
4
4
  import { findCollectionSource, parseMarkdownContent } from './source-finder'
5
- import type { CollectionEntry, CmsMarkerOptions, ComponentDefinition } from './types'
5
+ import type { CmsMarkerOptions, CollectionEntry, ComponentDefinition } from './types'
6
6
 
7
7
  /**
8
8
  * Get the normalized page path from a URL
@@ -90,14 +90,14 @@ export function createDevMiddleware(
90
90
  const requestUrl = req.url || 'unknown'
91
91
 
92
92
  // Intercept response chunks
93
- res.write = function(chunk: any, ...args: any[]) {
93
+ res.write = ((chunk: any, ...args: any[]) => {
94
94
  if (chunk) {
95
95
  chunks.push(Buffer.from(chunk))
96
96
  }
97
97
  return true
98
- } as any
98
+ }) as any
99
99
 
100
- res.end = function(chunk: any, ...args: any[]) {
100
+ res.end = ((chunk: any, ...args: any[]) => {
101
101
  if (chunk) {
102
102
  chunks.push(Buffer.from(chunk))
103
103
  }
@@ -143,7 +143,7 @@ export function createDevMiddleware(
143
143
  return res.end(Buffer.concat(chunks), ...args)
144
144
  }
145
145
  return res.end(...args)
146
- } as any
146
+ }) as any
147
147
 
148
148
  next()
149
149
  })
@@ -191,7 +191,7 @@ async function processHtmlForDev(
191
191
  skipMarkdownContent: isCollectionPage,
192
192
  // Pass collection info for wrapper element marking
193
193
  collectionInfo: collectionInfo
194
- ? { name: collectionInfo.name, slug: collectionInfo.slug, bodyFirstLine }
194
+ ? { name: collectionInfo.name, slug: collectionInfo.slug, bodyFirstLine, contentPath: collectionInfo.file }
195
195
  : undefined,
196
196
  },
197
197
  idGenerator,
@@ -1,5 +1,38 @@
1
1
  import { parse } from 'node-html-parser'
2
- import type { ComponentInstance, ManifestEntry } from './types'
2
+ import { enhanceManifestWithSourceSnippets } from './source-finder'
3
+ import { extractColorClasses } from './tailwind-colors'
4
+ import type { ComponentInstance, ManifestEntry, SourceContext } from './types'
5
+ import { generateStableId } from './utils'
6
+
7
+ /**
8
+ * Inline text styling elements that should NOT be marked with CMS IDs.
9
+ * These elements are text formatting and should be part of their parent's content.
10
+ * They will be preserved as HTML when editing the parent element.
11
+ */
12
+ export const INLINE_STYLE_TAGS = [
13
+ 'strong',
14
+ 'b',
15
+ 'em',
16
+ 'i',
17
+ 'u',
18
+ 's',
19
+ 'strike',
20
+ 'del',
21
+ 'ins',
22
+ 'mark',
23
+ 'small',
24
+ 'sub',
25
+ 'sup',
26
+ 'abbr',
27
+ 'cite',
28
+ 'code',
29
+ 'kbd',
30
+ 'samp',
31
+ 'var',
32
+ 'time',
33
+ 'dfn',
34
+ 'q',
35
+ ] as const
3
36
 
4
37
  export interface ProcessHtmlOptions {
5
38
  attributeName: string
@@ -13,12 +46,20 @@ export interface ProcessHtmlOptions {
13
46
  markStyledSpans?: boolean
14
47
  /** When true, only mark elements that have source file attributes (from Astro templates) */
15
48
  skipMarkdownContent?: boolean
49
+ /**
50
+ * When true, skip marking inline text styling elements (strong, b, em, i, etc.).
51
+ * These elements will be preserved as part of their parent's HTML content.
52
+ * Defaults to true.
53
+ */
54
+ skipInlineStyleTags?: boolean
16
55
  /** Collection info for marking the wrapper element containing markdown content */
17
56
  collectionInfo?: {
18
57
  name: string
19
58
  slug: string
20
59
  /** First line of the markdown body (used to find wrapper element in build mode) */
21
60
  bodyFirstLine?: string
61
+ /** Path to the markdown file (e.g., 'src/content/blog/my-post.md') */
62
+ contentPath?: string
22
63
  }
23
64
  }
24
65
 
@@ -128,6 +169,7 @@ export async function processHtml(
128
169
  excludeComponentDirs = ['src/pages', 'src/layouts', 'src/layout'],
129
170
  markStyledSpans = true,
130
171
  skipMarkdownContent = false,
172
+ skipInlineStyleTags = true,
131
173
  collectionInfo,
132
174
  } = options
133
175
 
@@ -235,6 +277,27 @@ export async function processHtml(
235
277
  })
236
278
  }
237
279
 
280
+ // Image detection pass: mark img elements for CMS image replacement
281
+ // Store image entries separately to add to manifest later
282
+ const imageEntries = new Map<string, { src: string; alt: string }>()
283
+ root.querySelectorAll('img').forEach((node) => {
284
+ // Skip if already marked
285
+ if (node.getAttribute(attributeName)) return
286
+
287
+ const src = node.getAttribute('src')
288
+ if (!src) return // Skip images without src
289
+
290
+ const id = getNextId()
291
+ node.setAttribute(attributeName, id)
292
+ node.setAttribute('data-cms-img', 'true')
293
+
294
+ // Store image info for manifest
295
+ imageEntries.set(id, {
296
+ src,
297
+ alt: node.getAttribute('alt') || '',
298
+ })
299
+ })
300
+
238
301
  // Collection wrapper detection pass: find the element that wraps markdown content
239
302
  // Two strategies:
240
303
  // 1. Dev mode: look for elements with data-astro-source-file containing children without it
@@ -274,6 +337,7 @@ export async function processHtml(
274
337
  // Mark this as the collection wrapper using the standard attribute
275
338
  const id = getNextId()
276
339
  node.setAttribute(attributeName, id)
340
+ node.setAttribute('data-cms-markdown', 'true')
277
341
  collectionWrapperId = id
278
342
  foundWrapper = true
279
343
  // Don't break - we want the deepest wrapper, so we'll overwrite
@@ -333,6 +397,7 @@ export async function processHtml(
333
397
  // Markdown body should have at least 2 block children
334
398
  const id = getNextId()
335
399
  best.node.setAttribute(attributeName, id)
400
+ best.node.setAttribute('data-cms-markdown', 'true')
336
401
  collectionWrapperId = id
337
402
  foundWrapper = true
338
403
  }
@@ -349,6 +414,23 @@ export async function processHtml(
349
414
  if (includeTags && !includeTags.includes(tag)) return
350
415
  if (node.getAttribute(attributeName)) return // Already marked
351
416
 
417
+ // Skip inline text styling elements (strong, b, em, i, etc.)
418
+ // These should be part of their parent's text content, not separately editable
419
+ // Only apply when includeTags is null (all tags) - if specific tags are listed, respect them
420
+ if (skipInlineStyleTags && includeTags === null && INLINE_STYLE_TAGS.includes(tag as typeof INLINE_STYLE_TAGS[number])) {
421
+ return
422
+ }
423
+
424
+ // Skip styled spans (spans with only text styling Tailwind classes)
425
+ // These are also inline text formatting and should be part of parent content
426
+ // Only apply when includeTags is null or doesn't include 'span'
427
+ if (skipInlineStyleTags && (includeTags === null || !includeTags.includes('span')) && tag === 'span') {
428
+ const classAttr = node.getAttribute('class')
429
+ if (classAttr && hasOnlyTextStyleClasses(classAttr)) {
430
+ return
431
+ }
432
+ }
433
+
352
434
  const textContent = (node.innerText ?? '').trim()
353
435
  if (!includeEmptyText && !textContent) return
354
436
 
@@ -369,7 +451,7 @@ export async function processHtml(
369
451
 
370
452
  if (sourceFile && sourceLine) {
371
453
  const lineNum = parseInt(sourceLine.split(':')[0] ?? '1', 10)
372
- if (!isNaN(lineNum)) {
454
+ if (!Number.isNaN(lineNum)) {
373
455
  sourceLocationMap.set(id, { file: sourceFile, line: lineNum })
374
456
  }
375
457
  // Only remove source attributes if this is NOT a component root
@@ -447,20 +529,54 @@ export async function processHtml(
447
529
  parent = parent.parentNode
448
530
  }
449
531
 
532
+ // Extract source context for resilient matching
533
+ const sourceContext = extractSourceContext(node, attributeName)
534
+
535
+ // Check if element contains inline style elements (strong, b, em, etc.) or styled spans
536
+ // If so, store the HTML content for source file updates
537
+ const inlineStyleSelector = INLINE_STYLE_TAGS.join(', ')
538
+ const hasInlineStyleElements = node.querySelector(inlineStyleSelector) !== null
539
+ const hasStyledSpans = node.querySelector('[data-cms-styled]') !== null
540
+ const htmlContent = (hasInlineStyleElements || hasStyledSpans) ? node.innerHTML : undefined
541
+
542
+ // Check if this is an image entry
543
+ const imageInfo = imageEntries.get(id)
544
+ const isImage = !!imageInfo
545
+
546
+ const entryText = isImage ? (imageInfo.alt || imageInfo.src) : textWithPlaceholders.trim()
547
+ const entrySourcePath = sourceLocation?.file || sourcePath
548
+
549
+ // Generate stable ID based on content and context
550
+ const stableId = generateStableId(tag, entryText, entrySourcePath, sourceContext)
551
+
552
+ // Extract color classes for buttons and other elements
553
+ const classAttr = node.getAttribute('class')
554
+ const colorClasses = extractColorClasses(classAttr)
555
+
450
556
  entries[id] = {
451
557
  id,
452
558
  tag,
453
- text: textWithPlaceholders.trim(),
454
- sourcePath: sourceLocation?.file || sourcePath,
559
+ text: entryText,
560
+ html: htmlContent,
561
+ sourcePath: entrySourcePath,
455
562
  childCmsIds: childCmsIds.length > 0 ? childCmsIds : undefined,
456
563
  sourceLine: sourceLocation?.line,
457
564
  sourceSnippet: undefined,
458
- sourceType: isCollectionWrapper ? 'collection' : undefined,
565
+ sourceType: isImage ? 'image' : (isCollectionWrapper ? 'collection' : undefined),
459
566
  variableName: undefined,
460
567
  parentComponentId,
461
568
  // Add collection info for the wrapper entry
462
569
  collectionName: isCollectionWrapper ? collectionInfo?.name : undefined,
463
570
  collectionSlug: isCollectionWrapper ? collectionInfo?.slug : undefined,
571
+ contentPath: isCollectionWrapper ? collectionInfo?.contentPath : undefined,
572
+ // Add image info for image entries
573
+ imageSrc: imageInfo?.src,
574
+ imageAlt: imageInfo?.alt,
575
+ // Robustness fields
576
+ stableId,
577
+ sourceContext,
578
+ // Color classes for buttons/styled elements
579
+ colorClasses,
464
580
  }
465
581
  })
466
582
  }
@@ -472,9 +588,13 @@ export async function processHtml(
472
588
  node.removeAttribute('data-astro-source-line')
473
589
  })
474
590
 
591
+ // Enhance manifest entries with actual source snippets from source files
592
+ // This allows the CMS to match and replace dynamic content in source files
593
+ const enhancedEntries = await enhanceManifestWithSourceSnippets(entries)
594
+
475
595
  return {
476
596
  html: root.toString(),
477
- entries,
597
+ entries: enhancedEntries,
478
598
  components,
479
599
  collectionWrapperId,
480
600
  }
@@ -497,3 +617,57 @@ function extractComponentName(sourceFile: string): string {
497
617
  export function cleanText(text: string): string {
498
618
  return text.trim().replace(/\s+/g, ' ').toLowerCase()
499
619
  }
620
+
621
+ /**
622
+ * Extract source context for an element to enable resilient matching.
623
+ * This captures information about the element's position in the DOM
624
+ * that can be used as fallback when exact matching fails.
625
+ */
626
+ function extractSourceContext(node: any, attributeName: string): SourceContext | undefined {
627
+ const parent = node.parentNode
628
+ if (!parent) return undefined
629
+
630
+ const siblings = parent.childNodes?.filter((child: any) => {
631
+ // Only consider element nodes, not text nodes
632
+ return child.nodeType === 1 && child.tagName
633
+ }) || []
634
+
635
+ const siblingIndex = siblings.indexOf(node)
636
+
637
+ // Get preceding sibling's text (first 30 chars)
638
+ let precedingText: string | undefined
639
+ if (siblingIndex > 0) {
640
+ const prevSibling = siblings[siblingIndex - 1]
641
+ const prevText = (prevSibling?.innerText || '').trim()
642
+ if (prevText) {
643
+ precedingText = prevText.substring(0, 30)
644
+ }
645
+ }
646
+
647
+ // Get following sibling's text (first 30 chars)
648
+ let followingText: string | undefined
649
+ if (siblingIndex < siblings.length - 1) {
650
+ const nextSibling = siblings[siblingIndex + 1]
651
+ const nextText = (nextSibling?.innerText || '').trim()
652
+ if (nextText) {
653
+ followingText = nextText.substring(0, 30)
654
+ }
655
+ }
656
+
657
+ // Get parent info
658
+ const parentTag = parent.tagName?.toLowerCase?.()
659
+ const parentClasses = parent.getAttribute?.('class') || undefined
660
+
661
+ // Only return context if we have meaningful data
662
+ if (!precedingText && !followingText && !parentTag) {
663
+ return undefined
664
+ }
665
+
666
+ return {
667
+ precedingText,
668
+ followingText,
669
+ parentTag,
670
+ siblingIndex: siblingIndex >= 0 ? siblingIndex : undefined,
671
+ parentClasses,
672
+ }
673
+ }
package/src/index.ts CHANGED
@@ -94,4 +94,3 @@ export default function cmsMarker(options: CmsMarkerOptions = {}): AstroIntegrat
94
94
  export { findCollectionSource, parseMarkdownContent } from './source-finder'
95
95
  export type { CollectionInfo, MarkdownContent } from './source-finder'
96
96
  export type { CmsManifest, CmsMarkerOptions, CollectionEntry, ComponentDefinition, ComponentInstance, ManifestEntry } from './types'
97
-
@@ -1,6 +1,11 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
- import type { CmsManifest, CollectionEntry, ComponentDefinition, ComponentInstance, ManifestEntry } from './types'
3
+ import { parseTailwindConfig } from './tailwind-colors'
4
+ import type { AvailableColors, CmsManifest, CollectionEntry, ComponentDefinition, ComponentInstance, ManifestEntry, ManifestMetadata } from './types'
5
+ import { generateManifestContentHash, generateSourceFileHashes } from './utils'
6
+
7
+ /** Current manifest schema version */
8
+ const MANIFEST_VERSION = '1.0'
4
9
 
5
10
  /**
6
11
  * Manages streaming manifest writes during build.
@@ -16,6 +21,7 @@ export class ManifestWriter {
16
21
  private outDir: string = ''
17
22
  private manifestFile: string
18
23
  private componentDefinitions: Record<string, ComponentDefinition>
24
+ private availableColors: AvailableColors | undefined
19
25
  private writeQueue: Promise<void> = Promise.resolve()
20
26
 
21
27
  constructor(manifestFile: string, componentDefinitions: Record<string, ComponentDefinition> = {}) {
@@ -44,6 +50,22 @@ export class ManifestWriter {
44
50
  this.globalManifest.componentDefinitions = definitions
45
51
  }
46
52
 
53
+ /**
54
+ * Load available Tailwind colors from the project's CSS config
55
+ */
56
+ async loadAvailableColors(projectRoot: string = process.cwd()): Promise<void> {
57
+ this.availableColors = await parseTailwindConfig(projectRoot)
58
+ this.globalManifest.availableColors = this.availableColors
59
+ }
60
+
61
+ /**
62
+ * Set available colors directly (for testing or custom colors)
63
+ */
64
+ setAvailableColors(colors: AvailableColors): void {
65
+ this.availableColors = colors
66
+ this.globalManifest.availableColors = colors
67
+ }
68
+
47
69
  /**
48
70
  * Get the manifest path for a given page
49
71
  * Places manifest next to the page: /about -> /about.json, / -> /index.json
@@ -100,13 +122,25 @@ export class ManifestWriter {
100
122
 
101
123
  await fs.mkdir(manifestDir, { recursive: true })
102
124
 
125
+ // Generate metadata for this page manifest
126
+ const metadata: ManifestMetadata = {
127
+ version: MANIFEST_VERSION,
128
+ generatedAt: new Date().toISOString(),
129
+ generatedBy: 'astro-cms-marker',
130
+ contentHash: generateManifestContentHash(entries),
131
+ sourceFileHashes: generateSourceFileHashes(entries),
132
+ }
133
+
103
134
  const pageManifest: {
135
+ metadata: ManifestMetadata
104
136
  page: string
105
137
  entries: Record<string, ManifestEntry>
106
138
  components: Record<string, ComponentInstance>
107
139
  componentDefinitions: Record<string, ComponentDefinition>
108
140
  collection?: CollectionEntry
141
+ availableColors?: AvailableColors
109
142
  } = {
143
+ metadata,
110
144
  page: pagePath,
111
145
  entries,
112
146
  components,
@@ -117,6 +151,10 @@ export class ManifestWriter {
117
151
  pageManifest.collection = collection
118
152
  }
119
153
 
154
+ if (this.availableColors) {
155
+ pageManifest.availableColors = this.availableColors
156
+ }
157
+
120
158
  await fs.writeFile(manifestPath, JSON.stringify(pageManifest, null, 2), 'utf-8')
121
159
  }
122
160
 
@@ -176,7 +214,15 @@ export class ManifestWriter {
176
214
  components: {},
177
215
  componentDefinitions: this.componentDefinitions,
178
216
  collections: {},
217
+ availableColors: this.availableColors,
179
218
  }
180
219
  this.writeQueue = Promise.resolve()
181
220
  }
221
+
222
+ /**
223
+ * Get available colors (for use in dev middleware)
224
+ */
225
+ getAvailableColors(): AvailableColors | undefined {
226
+ return this.availableColors
227
+ }
182
228
  }