@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/dist/editor.js +166 -166
- package/package.json +1 -1
- package/src/build-processor.ts +0 -2
- package/src/collection-scanner.ts +4 -4
- package/src/dev-middleware.ts +18 -18
- package/src/field-types.ts +27 -22
- package/src/index.ts +1 -1
- package/src/source-finder/cache.ts +13 -1
- package/src/source-finder/collection-finder.ts +6 -12
- package/src/source-finder/index.ts +0 -1
- package/src/source-finder/search-index.ts +2 -2
package/package.json
CHANGED
package/src/build-processor.ts
CHANGED
|
@@ -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 `
|
|
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
|
-
* - `
|
|
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
|
|
476
|
-
const fieldHelpers = schemaBody.matchAll(/(\w+)\s*:\s*
|
|
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]!
|
package/src/dev-middleware.ts
CHANGED
|
@@ -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
|
-
|
|
545
|
-
await initializeSearchIndex()
|
|
544
|
+
const enhanced = await enhanceManifestWithSourceSnippets(entries, collectionDefinitions)
|
|
546
545
|
|
|
547
|
-
//
|
|
548
|
-
for (const entry of Object.values(
|
|
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
|
|
551
|
-
if (
|
|
552
|
-
entry.sourcePath =
|
|
553
|
-
entry.sourceLine =
|
|
554
|
-
entry.sourceSnippet =
|
|
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
|
|
558
|
-
if (
|
|
559
|
-
entry.sourcePath =
|
|
560
|
-
entry.sourceLine =
|
|
561
|
-
entry.sourceSnippet =
|
|
562
|
-
if (
|
|
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,
|
|
568
|
+
manifestWriter.addPage(pagePath, enhanced, components, collection, seo)
|
|
569
569
|
} catch (error) {
|
|
570
570
|
console.error('[cms] Background enhancement failed:', error)
|
|
571
571
|
}
|
package/src/field-types.ts
CHANGED
|
@@ -1,42 +1,47 @@
|
|
|
1
|
+
import { z } from 'astro/zod'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Semantic field type
|
|
4
|
+
* Semantic field type schemas for content collections.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
15
|
+
* import { n } from '@nuasite/cms'
|
|
11
16
|
* import { z } from 'astro/zod'
|
|
12
17
|
*
|
|
13
18
|
* const schema = z.object({
|
|
14
|
-
* photo:
|
|
15
|
-
* website:
|
|
16
|
-
* contact:
|
|
17
|
-
* accent:
|
|
18
|
-
* publishedAt:
|
|
19
|
-
* startsAt:
|
|
20
|
-
* opensAt:
|
|
21
|
-
* bio:
|
|
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
|
|
30
|
+
export const n = {
|
|
26
31
|
/** Image picker (opens media library) */
|
|
27
|
-
image:
|
|
32
|
+
image: () => z.string().describe('cms:image'),
|
|
28
33
|
/** URL input */
|
|
29
|
-
url:
|
|
34
|
+
url: () => z.string().describe('cms:url'),
|
|
30
35
|
/** Email input */
|
|
31
|
-
email:
|
|
36
|
+
email: () => z.string().describe('cms:email'),
|
|
32
37
|
/** Color picker */
|
|
33
|
-
color:
|
|
38
|
+
color: () => z.string().describe('cms:color'),
|
|
34
39
|
/** Date picker */
|
|
35
|
-
date:
|
|
40
|
+
date: () => z.string().describe('cms:date'),
|
|
36
41
|
/** Date + time picker */
|
|
37
|
-
datetime:
|
|
42
|
+
datetime: () => z.string().describe('cms:datetime'),
|
|
38
43
|
/** Time picker */
|
|
39
|
-
time:
|
|
44
|
+
time: () => z.string().describe('cms:time'),
|
|
40
45
|
/** Multiline textarea */
|
|
41
|
-
textarea:
|
|
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 {
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
const index = getCollectionTextIndex()
|
|
170
|
+
if (!index) return undefined
|
|
172
171
|
const normalized = normalizeText(textContent)
|
|
173
|
-
const locations =
|
|
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
|
// ============================================================================
|
|
@@ -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
|
-
|
|
170
|
+
setCollectionTextIndex(null)
|
|
171
171
|
|
|
172
172
|
for (const absPath of filesToReindex) {
|
|
173
173
|
const relFile = path.relative(projectRoot, absPath)
|