@nuasite/cms 0.25.0 → 0.26.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/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "directory": "packages/astro-cms"
15
15
  },
16
16
  "license": "Apache-2.0",
17
- "version": "0.25.0",
17
+ "version": "0.26.0",
18
18
  "module": "src/index.ts",
19
19
  "types": "src/index.ts",
20
20
  "type": "module",
@@ -10,7 +10,6 @@ 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,
14
13
  clearSourceFinderCache,
15
14
  extractOpeningTagWithLine,
16
15
  findCollectionSource,
@@ -777,7 +776,6 @@ export async function processBuildOutput(
777
776
 
778
777
  // Clear caches from previous builds and initialize search index
779
778
  clearSourceFinderCache()
780
- clearCollectionTextIndex()
781
779
 
782
780
  const htmlFiles = await findHtmlFiles(outDir)
783
781
 
@@ -454,12 +454,12 @@ function parseContentConfigReferences(
454
454
  return result
455
455
  }
456
456
 
457
- /** Valid field type names exported by `field` helper from @nuasite/cms */
457
+ /** Valid field type names exported by `n` helper from @nuasite/cms */
458
458
  const FIELD_HELPER_TYPES = new Set(['image', 'url', 'email', 'color', 'date', 'datetime', 'time', 'textarea'])
459
459
 
460
460
  /**
461
461
  * Parse the content config file to extract explicit field type hints:
462
- * - `field.image(...)`, `field.url(...)`, etc. from @nuasite/cms
462
+ * - `n.image()`, `n.url()`, etc. from @nuasite/cms
463
463
  * - `z.enum([...])` for select options
464
464
  *
465
465
  * Returns a map: collectionName → fieldName → { type, options? }
@@ -472,8 +472,8 @@ function parseContentConfigFieldTypes(
472
472
  for (const { collectionName, schemaBody } of schemaBlocks) {
473
473
  const fields = new Map<string, { type: FieldType; options?: string[] }>()
474
474
 
475
- // Detect field.image(...), field.url(...), etc.
476
- const fieldHelpers = schemaBody.matchAll(/(\w+)\s*:\s*field\.(\w+)\s*\(/g)
475
+ // Detect n.image(), n.url(), etc.
476
+ const fieldHelpers = schemaBody.matchAll(/(\w+)\s*:\s*n\.(\w+)/g)
477
477
  for (const m of fieldHelpers) {
478
478
  const fieldName = m[1]!
479
479
  const helperName = m[2]!
@@ -17,10 +17,10 @@ import { processHtml } from './html-processor'
17
17
  import type { ManifestWriter } from './manifest-writer'
18
18
  import type { MediaStorageAdapter } from './media/types'
19
19
  import {
20
+ enhanceManifestWithSourceSnippets,
20
21
  findCollectionSource,
21
22
  findImageSourceLocation,
22
23
  findSourceLocation,
23
- initializeSearchIndex,
24
24
  parseMarkdownContent,
25
25
  reindexDirtyFiles,
26
26
  } from './source-finder'
@@ -424,7 +424,7 @@ async function markHtmlForDev(
424
424
  * Phase 2 (background): Resolve source locations, enhance snippets, populate
425
425
  * component props, and update the manifest. Runs after the HTML response is sent.
426
426
  */
427
- async function enhanceManifestInBackground(
427
+ export async function enhanceManifestInBackground(
428
428
  pagePath: string,
429
429
  entries: Record<string, ManifestEntry>,
430
430
  components: Record<string, ComponentInstance>,
@@ -541,31 +541,31 @@ async function enhanceManifestInBackground(
541
541
  }
542
542
  }
543
543
 
544
- // Ensure the search index is initialized
545
- await initializeSearchIndex()
544
+ const enhanced = await enhanceManifestWithSourceSnippets(entries, collectionDefinitions)
546
545
 
547
- // Re-resolve sources with the search index
548
- for (const entry of Object.values(entries)) {
546
+ // Fallback for entries without sourcePath — search index can still find them
547
+ for (const entry of Object.values(enhanced)) {
548
+ if (entry.sourceSnippet || entry.sourcePath) continue
549
549
  if (entry.imageMetadata?.src) {
550
- const imageSource = await findImageSourceLocation(entry.imageMetadata.src, entry.imageMetadata.srcSet)
551
- if (imageSource) {
552
- entry.sourcePath = imageSource.file
553
- entry.sourceLine = imageSource.line
554
- entry.sourceSnippet = imageSource.snippet
550
+ const loc = await findImageSourceLocation(entry.imageMetadata.src, entry.imageMetadata.srcSet)
551
+ if (loc) {
552
+ entry.sourcePath = loc.file
553
+ entry.sourceLine = loc.line
554
+ entry.sourceSnippet = loc.snippet
555
555
  }
556
556
  } else if (entry.text && entry.tag) {
557
- const textSource = await findSourceLocation(entry.text, entry.tag)
558
- if (textSource) {
559
- entry.sourcePath = textSource.file
560
- entry.sourceLine = textSource.line
561
- entry.sourceSnippet = textSource.snippet
562
- if (textSource.variableName) entry.variableName = textSource.variableName
557
+ const loc = await findSourceLocation(entry.text, entry.tag)
558
+ if (loc) {
559
+ entry.sourcePath = loc.file
560
+ entry.sourceLine = loc.line
561
+ entry.sourceSnippet = loc.snippet
562
+ if (loc.variableName) entry.variableName = loc.variableName
563
563
  }
564
564
  }
565
565
  }
566
566
 
567
567
  // Update the manifest with fully-resolved entries and component props
568
- manifestWriter.addPage(pagePath, entries, components, collection, seo)
568
+ manifestWriter.addPage(pagePath, enhanced, components, collection, seo)
569
569
  } catch (error) {
570
570
  console.error('[cms] Background enhancement failed:', error)
571
571
  }
@@ -1,42 +1,47 @@
1
+ import { z } from 'astro/zod'
2
+
1
3
  /**
2
- * Semantic field type wrappers for Zod schemas in content collections.
4
+ * Semantic field type schemas for content collections.
3
5
  *
4
- * These are identity functions they return exactly what's passed in.
5
- * The CMS collection scanner detects them by name in the source and
6
- * renders the appropriate editor input.
6
+ * Each method returns a `z.string()` schema that Astro resolves
7
+ * statically (concrete return type, no generics). The CMS collection
8
+ * scanner detects them by name in the source and renders the
9
+ * appropriate editor input.
10
+ *
11
+ * Chain Zod methods as usual (`.optional()`, `.default()`, etc.).
7
12
  *
8
13
  * @example
9
14
  * ```ts
10
- * import { field } from '@nuasite/cms'
15
+ * import { n } from '@nuasite/cms'
11
16
  * import { z } from 'astro/zod'
12
17
  *
13
18
  * const schema = z.object({
14
- * photo: field.image(z.string()),
15
- * website: field.url(z.string()),
16
- * contact: field.email(z.string()),
17
- * accent: field.color(z.string()),
18
- * publishedAt: field.date(z.string()),
19
- * startsAt: field.datetime(z.string()),
20
- * opensAt: field.time(z.string()),
21
- * bio: field.textarea(z.string()),
19
+ * photo: n.image(),
20
+ * website: n.url().optional(),
21
+ * contact: n.email(),
22
+ * accent: n.color(),
23
+ * publishedAt: n.date(),
24
+ * startsAt: n.datetime(),
25
+ * opensAt: n.time(),
26
+ * bio: n.textarea(),
22
27
  * })
23
28
  * ```
24
29
  */
25
- export const field = {
30
+ export const n = {
26
31
  /** Image picker (opens media library) */
27
- image: <T>(schema: T): T => schema,
32
+ image: () => z.string().describe('cms:image'),
28
33
  /** URL input */
29
- url: <T>(schema: T): T => schema,
34
+ url: () => z.string().describe('cms:url'),
30
35
  /** Email input */
31
- email: <T>(schema: T): T => schema,
36
+ email: () => z.string().describe('cms:email'),
32
37
  /** Color picker */
33
- color: <T>(schema: T): T => schema,
38
+ color: () => z.string().describe('cms:color'),
34
39
  /** Date picker */
35
- date: <T>(schema: T): T => schema,
40
+ date: () => z.string().describe('cms:date'),
36
41
  /** Date + time picker */
37
- datetime: <T>(schema: T): T => schema,
42
+ datetime: () => z.string().describe('cms:datetime'),
38
43
  /** Time picker */
39
- time: <T>(schema: T): T => schema,
44
+ time: () => z.string().describe('cms:time'),
40
45
  /** Multiline textarea */
41
- textarea: <T>(schema: T): T => schema,
46
+ textarea: () => z.string().describe('cms:textarea'),
42
47
  }
package/src/index.ts CHANGED
@@ -346,7 +346,7 @@ async function mergeRedirects(dir: URL, logger: { info: (msg: string) => void })
346
346
  logger.info(`Merged ${lineCount} CMS redirect(s) into _redirects`)
347
347
  }
348
348
 
349
- export { field } from './field-types'
349
+ export { n } from './field-types'
350
350
  export { createContemberStorageAdapter as contemberMedia } from './media/contember'
351
351
  export { createLocalStorageAdapter as localMedia } from './media/local'
352
352
  export { createS3StorageAdapter as s3Media } from './media/s3'
@@ -1,4 +1,4 @@
1
- import type { CachedParsedFile, ImageIndexEntry, SearchIndexEntry } from './types'
1
+ import type { CachedParsedFile, ImageIndexEntry, SearchIndexEntry, SourceLocation } from './types'
2
2
 
3
3
  // ============================================================================
4
4
  // File Parsing Cache - Avoid re-parsing the same files
@@ -18,6 +18,9 @@ const textSearchIndex: SearchIndexEntry[] = []
18
18
  const imageSearchIndex: ImageIndexEntry[] = []
19
19
  let searchIndexInitialized = false
20
20
 
21
+ /** Pre-built reverse index: normalizedText → SourceLocation[] (collection data files) */
22
+ let collectionTextIndex: Map<string, SourceLocation[]> | null = null
23
+
21
24
  /** Files that changed since last indexing — tracked by Vite watcher */
22
25
  const dirtyFiles = new Set<string>()
23
26
 
@@ -61,6 +64,14 @@ export function addToImageSearchIndex(entry: ImageIndexEntry): void {
61
64
  imageSearchIndex.push(entry)
62
65
  }
63
66
 
67
+ export function getCollectionTextIndex(): Map<string, SourceLocation[]> | null {
68
+ return collectionTextIndex
69
+ }
70
+
71
+ export function setCollectionTextIndex(index: Map<string, SourceLocation[]> | null): void {
72
+ collectionTextIndex = index
73
+ }
74
+
64
75
  // ============================================================================
65
76
  // Dirty File Tracking (incremental re-indexing)
66
77
  // ============================================================================
@@ -120,4 +131,5 @@ export function clearSourceFinderCache(): void {
120
131
  textSearchIndex.length = 0
121
132
  imageSearchIndex.length = 0
122
133
  searchIndexInitialized = false
134
+ collectionTextIndex = null
123
135
  }
@@ -4,7 +4,7 @@ import { isMap, isPair, isScalar, isSeq, LineCounter, parseDocument } from 'yaml
4
4
 
5
5
  import { getProjectRoot } from '../config'
6
6
  import type { CollectionDefinition } from '../types'
7
- import { getMarkdownFileCache } from './cache'
7
+ import { getCollectionTextIndex, getMarkdownFileCache, setCollectionTextIndex } from './cache'
8
8
  import { normalizeText } from './snippet-utils'
9
9
  import type { CollectionInfo, MarkdownContent, SourceLocation } from './types'
10
10
 
@@ -12,8 +12,6 @@ import type { CollectionInfo, MarkdownContent, SourceLocation } from './types'
12
12
  // Collection Text Index — pre-built reverse index for fast text→source lookups
13
13
  // ============================================================================
14
14
 
15
- /** Pre-built index: normalizedText → SourceLocation (with collection metadata) */
16
- let collectionTextIndex: Map<string, SourceLocation[]> | null = null
17
15
  let collectionTextIndexPromise: Promise<void> | null = null
18
16
 
19
17
  /**
@@ -23,7 +21,7 @@ let collectionTextIndexPromise: Promise<void> | null = null
23
21
  export async function buildCollectionTextIndex(
24
22
  collections: Record<string, CollectionDefinition>,
25
23
  ): Promise<void> {
26
- if (collectionTextIndex) return
24
+ if (getCollectionTextIndex()) return
27
25
  if (collectionTextIndexPromise) return collectionTextIndexPromise
28
26
  collectionTextIndexPromise = doBuildCollectionTextIndex(collections)
29
27
  try {
@@ -76,7 +74,7 @@ async function doBuildCollectionTextIndex(
76
74
  }))
77
75
  }
78
76
 
79
- collectionTextIndex = index
77
+ setCollectionTextIndex(index)
80
78
  }
81
79
 
82
80
  /**
@@ -168,9 +166,10 @@ export function lookupCollectionText(
168
166
  textContent: string,
169
167
  referenceIndex?: Map<string, Array<{ collection: string; fieldName: string; isArray?: boolean }>>,
170
168
  ): SourceLocation | undefined {
171
- if (!collectionTextIndex) return undefined
169
+ const index = getCollectionTextIndex()
170
+ if (!index) return undefined
172
171
  const normalized = normalizeText(textContent)
173
- const locations = collectionTextIndex.get(normalized)
172
+ const locations = index.get(normalized)
174
173
  if (!locations || locations.length === 0) return undefined
175
174
 
176
175
  // Prefer locations from referenced collections
@@ -184,11 +183,6 @@ export function lookupCollectionText(
184
183
  return locations[0]
185
184
  }
186
185
 
187
- /** Clear the collection text index (called when collection files change) */
188
- export function clearCollectionTextIndex(): void {
189
- collectionTextIndex = null
190
- }
191
-
192
186
  // ============================================================================
193
187
  // Markdown File Cache
194
188
  // ============================================================================
@@ -25,7 +25,6 @@ export { findImageSourceLocation } from './image-finder'
25
25
  // Collection/markdown finding
26
26
  export {
27
27
  buildCollectionTextIndex,
28
- clearCollectionTextIndex,
29
28
  findCollectionSource,
30
29
  findFieldInCollectionEntry,
31
30
  findMarkdownSourceLocation,
@@ -17,9 +17,9 @@ import {
17
17
  getTextSearchIndex,
18
18
  isSearchIndexInitialized,
19
19
  removeFileFromIndexes,
20
+ setCollectionTextIndex,
20
21
  setSearchIndexInitialized,
21
22
  } from './cache'
22
- import { clearCollectionTextIndex } from './collection-finder'
23
23
  import { extractImageSnippet, extractInnerHtmlFromSnippet, normalizeText } from './snippet-utils'
24
24
  import type { CachedParsedFile, SearchIndexEntry, SourceLocation } from './types'
25
25
 
@@ -167,7 +167,7 @@ async function doReindexDirtyFiles(): Promise<void> {
167
167
  // Also clear the markdown file cache and collection text index
168
168
  // so collection content is re-read and re-indexed from disk
169
169
  getMarkdownFileCache().clear()
170
- clearCollectionTextIndex()
170
+ setCollectionTextIndex(null)
171
171
 
172
172
  for (const absPath of filesToReindex) {
173
173
  const relFile = path.relative(projectRoot, absPath)