@nuasite/cms 0.46.4 → 0.47.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 +2 -2
- package/package.json +7 -7
- package/src/admin/tsconfig.tsbuildinfo +1 -1
- package/src/build-processor.ts +1 -1
- package/src/dev-middleware.ts +29 -19
- package/src/handlers/api-routes.ts +1 -1
- package/src/index.ts +4 -4
- package/src/manifest-writer.ts +14 -6
- package/src/migrate-astro-image.ts +2 -2
- package/src/scan-cache.ts +22 -0
- package/src/source-finder/collection-finder.ts +76 -0
- package/src/types.ts +32 -215
- package/src/collection-scanner.ts +0 -990
- package/src/content-config-ast.ts +0 -501
package/src/dev-middleware.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createCmsCore, createNodeFs } from '@nuasite/cms-core'
|
|
1
|
+
import { createCmsCore, createNodeFs, resolvePathnameFromSpec } from '@nuasite/cms-core'
|
|
2
2
|
import fs from 'node:fs/promises'
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
4
|
import path from 'node:path'
|
|
@@ -193,23 +193,33 @@ export function createDevMiddleware(
|
|
|
193
193
|
|
|
194
194
|
for (const [name, def] of Object.entries(collectionDefs)) {
|
|
195
195
|
const routeInfo = collectionRoutes.get(def.name)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
196
|
+
const routePrefix = typeof routeInfo === 'string' ? routeInfo : undefined
|
|
197
|
+
// Whether any page route renders this collection. The heuristic fallbacks
|
|
198
|
+
// below (a frontmatter-declared site path, or the discovered route prefix)
|
|
199
|
+
// only apply to routed collections — otherwise a declared `cms.pathname`
|
|
200
|
+
// spec whose fields are missing for an entry would fabricate a URL from an
|
|
201
|
+
// unrelated frontmatter field (e.g. a data collection's own `url`).
|
|
202
|
+
const routed = routeInfo !== undefined
|
|
203
|
+
|
|
204
|
+
// A declarative `cms.pathname` rule is the highest-priority source: it wins
|
|
205
|
+
// over the rendered-route pathname (addPage set entry.pathname to the page
|
|
206
|
+
// URL). resolvePathnameFromSpec is the same resolver manifest-writer uses at
|
|
207
|
+
// build, so dev-preview and the built manifest agree on the entry's URL.
|
|
208
|
+
const resolvePathname = (entry: CollectionEntryInfo): string | undefined =>
|
|
209
|
+
resolvePathnameFromSpec(def, entry.data)
|
|
210
|
+
?? entry.pathname
|
|
211
|
+
?? (routed ? declaredSitePathFromData(entry.data) : undefined)
|
|
212
|
+
?? (routePrefix ? `${routePrefix}${entry.slug}` : undefined)
|
|
213
|
+
|
|
214
|
+
// Build patched copies rather than mutating the originals so heuristic
|
|
215
|
+
// pathnames don't persist if the route file is later removed. Only patch
|
|
216
|
+
// entries whose resolved pathname differs from what they already have.
|
|
217
|
+
const patchedEntries = def.entries?.map(e => {
|
|
218
|
+
const pathname = resolvePathname(e)
|
|
219
|
+
return pathname && pathname !== e.pathname ? { ...e, pathname } : e
|
|
220
|
+
})
|
|
221
|
+
const changed = patchedEntries?.some((e, i) => e !== def.entries![i]) ?? false
|
|
222
|
+
responseCollectionDefs[name] = changed ? { ...def, entries: patchedEntries! } : def
|
|
213
223
|
|
|
214
224
|
const entries = responseCollectionDefs[name].entries
|
|
215
225
|
if (entries) {
|
|
@@ -440,7 +450,7 @@ async function markHtmlForDev(
|
|
|
440
450
|
const idGenerator = () => `cms-${pageCounter++}`
|
|
441
451
|
|
|
442
452
|
// Check if this is a collection page
|
|
443
|
-
const collectionInfo = await findCollectionSource(pagePath, config.contentDir)
|
|
453
|
+
const collectionInfo = await findCollectionSource(pagePath, config.contentDir, manifestWriter.getCollectionDefinitions())
|
|
444
454
|
const isCollectionPage = !!collectionInfo
|
|
445
455
|
|
|
446
456
|
let mdContent: Awaited<ReturnType<typeof parseMarkdownContent>> | undefined
|
|
@@ -2,11 +2,11 @@ import type { CmsCore, CmsFileSystem } from '@nuasite/cms-core'
|
|
|
2
2
|
import { listProjectImages } from '@nuasite/cms-core'
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
-
import { scanCollections } from '../collection-scanner'
|
|
6
5
|
import { getProjectRoot } from '../config'
|
|
7
6
|
import { expectedDeletions } from '../dev-middleware'
|
|
8
7
|
import type { ManifestWriter } from '../manifest-writer'
|
|
9
8
|
import type { MediaStorageAdapter } from '../media/types'
|
|
9
|
+
import { scanCollections } from '../scan-cache'
|
|
10
10
|
import { handleAddArrayItem, handleRemoveArrayItem } from './array-ops'
|
|
11
11
|
import { tryAstroImageUpload } from './astro-image-upload'
|
|
12
12
|
import { handleInsertComponent, handleRemoveComponent } from './component-ops'
|
package/src/index.ts
CHANGED
|
@@ -7,9 +7,8 @@ import { fileURLToPath } from 'node:url'
|
|
|
7
7
|
import { createLocalStorageAdapter } from '@nuasite/cms-core'
|
|
8
8
|
import remarkDirective from 'remark-directive'
|
|
9
9
|
import { processBuildOutput } from './build-processor'
|
|
10
|
-
import { scanCollections } from './collection-scanner'
|
|
11
10
|
import { ComponentRegistry } from './component-registry'
|
|
12
|
-
import { resetProjectRoot } from './config'
|
|
11
|
+
import { getProjectRoot, resetProjectRoot } from './config'
|
|
13
12
|
import { createDevMiddleware } from './dev-middleware'
|
|
14
13
|
import { getErrorCollector, resetErrorCollector } from './error-collector'
|
|
15
14
|
import { ADMIN_ROUTE, createLocalAdminMiddleware } from './local-admin'
|
|
@@ -18,6 +17,7 @@ import type { MediaStorageAdapter } from './media/types'
|
|
|
18
17
|
import { type CmsMode, resolveCmsMode } from './mode'
|
|
19
18
|
import { rehypeCmsMarker } from './rehype-cms-marker'
|
|
20
19
|
import { remarkListDirective } from './remark-list-directive'
|
|
20
|
+
import { scanCollections } from './scan-cache'
|
|
21
21
|
import type { CmsFeatures, CmsMarkerOptions, ComponentDefinition } from './types'
|
|
22
22
|
import { createPublicStaticFileChecker } from './utils'
|
|
23
23
|
import { createVitePlugin } from './vite-plugin'
|
|
@@ -482,14 +482,14 @@ export {
|
|
|
482
482
|
createS3StorageAdapter as s3Media,
|
|
483
483
|
} from '@nuasite/cms-core'
|
|
484
484
|
export { FIELD_TYPES, isFieldType } from '@nuasite/cms-types'
|
|
485
|
-
export type { CollectionLayout, CollectionLayoutSection } from '@nuasite/cms-types'
|
|
485
|
+
export type { CollectionLayout, CollectionLayoutSection, PathnameSegment, PathnameSpec } from '@nuasite/cms-types'
|
|
486
486
|
export { defineCmsCollection, n } from './field-types'
|
|
487
487
|
export type { DateHints, FileHints, ImageHints, LayoutHints, NumberHints, TextareaHints, TextHints } from './field-types'
|
|
488
488
|
export type { MediaFolderItem, MediaItem, MediaListOptions, MediaListResult, MediaStorageAdapter, MediaTypeFilter } from './media/types'
|
|
489
489
|
export { type CmsMode, detectHostedFromEnv, resolveCmsMode } from './mode'
|
|
490
490
|
export type { Color, Date, DateTime, Email, Image, Reference, Textarea, Time, Url } from './prop-types'
|
|
491
491
|
|
|
492
|
-
export { scanCollections } from '
|
|
492
|
+
export { scanCollections } from '@nuasite/cms-core'
|
|
493
493
|
export { getProjectRoot, resetProjectRoot, setProjectRoot } from './config'
|
|
494
494
|
export { type FieldMigration, migrateAstroImages, type MigrationOptions, type MigrationResult } from './migrate-astro-image'
|
|
495
495
|
export { rehypeCmsMarker } from './rehype-cms-marker'
|
package/src/manifest-writer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolvePathnameFromSpec } from '@nuasite/cms-core'
|
|
1
2
|
import fs from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { getProjectRoot } from './config'
|
|
@@ -279,12 +280,19 @@ export class ManifestWriter {
|
|
|
279
280
|
|
|
280
281
|
// Populate pathname on collection definition entries
|
|
281
282
|
for (const def of Object.values(this.collectionDefinitions)) {
|
|
282
|
-
if (def.entries)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
283
|
+
if (!def.entries) continue
|
|
284
|
+
for (const entry of def.entries) {
|
|
285
|
+
// A declarative `cms.pathname` rule is the highest-priority source: it
|
|
286
|
+
// wins over the rendered-page pathname. resolvePathnameFromSpec is the
|
|
287
|
+
// same resolver the dev middleware uses, so dev and build agree.
|
|
288
|
+
const fromSpec = resolvePathnameFromSpec(def, entry.data)
|
|
289
|
+
if (fromSpec) {
|
|
290
|
+
entry.pathname = fromSpec
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
const pathname = collectionPathMap.get(`${def.name}/${entry.slug}`)
|
|
294
|
+
if (pathname) {
|
|
295
|
+
entry.pathname = pathname
|
|
288
296
|
}
|
|
289
297
|
}
|
|
290
298
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { createNodeFs, scanCollections } from '@nuasite/cms-core'
|
|
1
2
|
import fs from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { parseDocument } from 'yaml'
|
|
4
5
|
import { pickAstroImageTarget } from './astro-image-paths'
|
|
5
|
-
import { scanCollections } from './collection-scanner'
|
|
6
6
|
import { getProjectRoot } from './config'
|
|
7
7
|
|
|
8
8
|
export interface MigrationOptions {
|
|
@@ -33,7 +33,7 @@ export async function migrateAstroImages(options: MigrationOptions = {}): Promis
|
|
|
33
33
|
const projectRoot = options.projectRoot ?? getProjectRoot()
|
|
34
34
|
const dryRun = options.dryRun ?? false
|
|
35
35
|
|
|
36
|
-
const collections = await scanCollections()
|
|
36
|
+
const collections = await scanCollections(createNodeFs(projectRoot))
|
|
37
37
|
const migrations: FieldMigration[] = []
|
|
38
38
|
const skipped: MigrationResult['skipped'] = []
|
|
39
39
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createNodeFs, type ParseCache, scanCollections as coreScanCollections } from '@nuasite/cms-core'
|
|
2
|
+
import type { CollectionDefinition } from '@nuasite/cms-types'
|
|
3
|
+
import { getProjectRoot } from './config'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Process-persistent content-config parse cache shared across every standalone
|
|
7
|
+
* collection scan (server setup + each entry create/delete). cms-core's
|
|
8
|
+
* `parseContentConfig` is mtime-keyed, so threading one cache here lets repeated
|
|
9
|
+
* scans skip re-reading and re-Babel-parsing `content.config.ts` when it hasn't
|
|
10
|
+
* changed — the CmsCore instance keeps its own cache the same way, but these
|
|
11
|
+
* top-level call sites bypass it.
|
|
12
|
+
*/
|
|
13
|
+
const parseCache: ParseCache = new Map()
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scan all content collections against the current project root, reusing the
|
|
17
|
+
* shared {@link parseCache}. Drop-in replacement for cms-core's `scanCollections`
|
|
18
|
+
* for the dev-server call sites that would otherwise each allocate a fresh cache.
|
|
19
|
+
*/
|
|
20
|
+
export function scanCollections(contentDir?: string): Promise<Record<string, CollectionDefinition>> {
|
|
21
|
+
return coreScanCollections(createNodeFs(getProjectRoot()), contentDir, parseCache)
|
|
22
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { resolvePathnameFromSpec } from '@nuasite/cms-core'
|
|
1
2
|
import fs from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { isMap, isPair, isScalar, isSeq, LineCounter, parseDocument } from 'yaml'
|
|
4
5
|
|
|
5
6
|
import { getProjectRoot } from '../config'
|
|
7
|
+
import { scanCollections } from '../scan-cache'
|
|
6
8
|
import type { CollectionDefinition } from '../types'
|
|
7
9
|
import { getCollectionTextIndex, getDeclaredUrlIndexCache, getMarkdownFileCache, setCollectionTextIndex } from './cache'
|
|
8
10
|
import { normalizeText } from './snippet-utils'
|
|
@@ -249,6 +251,7 @@ const DECLARED_URL_FIELDS = ['urlpath', 'permalink', 'pathname', 'route', 'url']
|
|
|
249
251
|
export async function findCollectionSource(
|
|
250
252
|
pagePath: string,
|
|
251
253
|
contentDir: string = 'src/content',
|
|
254
|
+
collections?: Record<string, CollectionDefinition>,
|
|
252
255
|
): Promise<CollectionInfo | undefined> {
|
|
253
256
|
// Remove leading/trailing slashes
|
|
254
257
|
const cleanPath = pagePath.replace(/^\/+|\/+$/g, '')
|
|
@@ -259,6 +262,18 @@ export async function findCollectionSource(
|
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
const requestedUrl = normalizeSitePath(`/${cleanPath}`)
|
|
265
|
+
|
|
266
|
+
// Highest priority: derive each entry's canonical URL from its collection's
|
|
267
|
+
// declarative `cms.pathname` rule (the same spec the forward resolver/manifest
|
|
268
|
+
// use) and match. This keeps reverse resolution (URL → source file) consistent
|
|
269
|
+
// with forward resolution without the entry needing a redundant `urlPath`
|
|
270
|
+
// frontmatter field, and correctly resolves entries whose filename differs
|
|
271
|
+
// from their URL slug (e.g. people stored as `<role>__<slug>.md` served at
|
|
272
|
+
// `/<family>/<slug>`). Collections without a `cms.pathname` rule contribute
|
|
273
|
+
// nothing here and fall through to the filename/declared-URL logic below.
|
|
274
|
+
const bySpec = await resolveBySpec(requestedUrl, contentDir, collections)
|
|
275
|
+
if (bySpec) return bySpec
|
|
276
|
+
|
|
262
277
|
const contentPath = path.join(getProjectRoot(), contentDir)
|
|
263
278
|
|
|
264
279
|
try {
|
|
@@ -357,6 +372,67 @@ export async function findCollectionSource(
|
|
|
357
372
|
return undefined
|
|
358
373
|
}
|
|
359
374
|
|
|
375
|
+
// Reverse index (computed spec URL → source) cached per scanned-collections
|
|
376
|
+
// object. The scan cache hands back a fresh object identity whenever the
|
|
377
|
+
// content config or any entry changes, so a WeakMap keyed on it rebuilds the
|
|
378
|
+
// index exactly when the underlying data changes and never goes stale.
|
|
379
|
+
const specPathnameIndexCache = new WeakMap<object, Map<string, CollectionInfo>>()
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Resolve `requestedUrl` to a source file by applying each collection's
|
|
383
|
+
* declarative `cms.pathname` rule to its entries' frontmatter data. Returns
|
|
384
|
+
* undefined when no collection declares a rule or none produces `requestedUrl`.
|
|
385
|
+
*
|
|
386
|
+
* `collections` should be the definitions already scanned once at plugin setup
|
|
387
|
+
* (`manifestWriter.getCollectionDefinitions()`) — passing them keeps this off the
|
|
388
|
+
* hot per-page path with no rescans and guarantees the reverse index matches the
|
|
389
|
+
* forward resolver's specs exactly. When omitted (tests, misc callers) it falls
|
|
390
|
+
* back to a self-contained scan.
|
|
391
|
+
*/
|
|
392
|
+
async function resolveBySpec(
|
|
393
|
+
requestedUrl: string,
|
|
394
|
+
contentDir: string,
|
|
395
|
+
collections: Record<string, CollectionDefinition> | undefined,
|
|
396
|
+
): Promise<CollectionInfo | undefined> {
|
|
397
|
+
let defs = collections
|
|
398
|
+
if (!defs) {
|
|
399
|
+
try {
|
|
400
|
+
defs = await scanCollections(contentDir)
|
|
401
|
+
} catch {
|
|
402
|
+
return undefined
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let index = specPathnameIndexCache.get(defs)
|
|
407
|
+
if (!index) {
|
|
408
|
+
index = buildSpecPathnameIndex(defs)
|
|
409
|
+
specPathnameIndexCache.set(defs, index)
|
|
410
|
+
}
|
|
411
|
+
return index.get(requestedUrl)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Build a `computed-URL → CollectionInfo` map from every collection that
|
|
416
|
+
* declares a `cms.pathname` rule. Collections and entries are visited in sorted
|
|
417
|
+
* order and the first writer of a URL wins, so a URL collision (a content bug)
|
|
418
|
+
* resolves deterministically rather than depending on scan/readdir order.
|
|
419
|
+
*/
|
|
420
|
+
function buildSpecPathnameIndex(collections: Record<string, CollectionDefinition>): Map<string, CollectionInfo> {
|
|
421
|
+
const index = new Map<string, CollectionInfo>()
|
|
422
|
+
for (const name of Object.keys(collections).sort()) {
|
|
423
|
+
const def = collections[name]
|
|
424
|
+
if (!def?.pathname || !def.entries) continue
|
|
425
|
+
const entries = [...def.entries].sort((a, b) => a.slug.localeCompare(b.slug))
|
|
426
|
+
for (const entry of entries) {
|
|
427
|
+
if (!entry.data) continue
|
|
428
|
+
const url = resolvePathnameFromSpec(def, entry.data)
|
|
429
|
+
if (!url || index.has(url)) continue
|
|
430
|
+
index.set(url, { name: def.name, slug: entry.slug, file: entry.sourcePath })
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return index
|
|
434
|
+
}
|
|
435
|
+
|
|
360
436
|
/** Normalize a site-absolute path: ensure a leading slash, drop query/hash and any trailing slash. */
|
|
361
437
|
function normalizeSitePath(p: string): string {
|
|
362
438
|
let s = p.split('?')[0]?.split('#')[0] ?? p
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Attribute,
|
|
3
|
+
AvailableColors,
|
|
4
|
+
AvailableTextStyles,
|
|
5
|
+
BackgroundImageMetadata,
|
|
6
|
+
CollectionDefinition,
|
|
7
|
+
CollectionEntry,
|
|
8
|
+
ComponentDefinition,
|
|
9
|
+
ContentConstraints,
|
|
10
|
+
ImageMetadata,
|
|
11
|
+
ManifestMetadata,
|
|
12
|
+
PageEntry,
|
|
13
|
+
} from '@nuasite/cms-types'
|
|
2
14
|
|
|
3
15
|
// Structural contract types live in @nuasite/cms-types (the shared wire model).
|
|
4
16
|
// Re-exported here so existing `@nuasite/cms` imports keep working unchanged.
|
|
@@ -12,6 +24,8 @@ export type {
|
|
|
12
24
|
FieldHints,
|
|
13
25
|
FieldType,
|
|
14
26
|
MutationResult,
|
|
27
|
+
PathnameSegment,
|
|
28
|
+
PathnameSpec,
|
|
15
29
|
} from '@nuasite/cms-types'
|
|
16
30
|
export type {
|
|
17
31
|
AddRedirectRequest,
|
|
@@ -26,6 +40,23 @@ export type {
|
|
|
26
40
|
RedirectRule,
|
|
27
41
|
UpdateRedirectRequest,
|
|
28
42
|
} from '@nuasite/cms-types'
|
|
43
|
+
// Render/manifest model types now live in @nuasite/cms-types (the shared, DOM-free
|
|
44
|
+
// wire model). Re-exported here so existing `@nuasite/cms` imports keep working.
|
|
45
|
+
export type {
|
|
46
|
+
Attribute,
|
|
47
|
+
AvailableColors,
|
|
48
|
+
AvailableTextStyles,
|
|
49
|
+
BackgroundImageMetadata,
|
|
50
|
+
CmsManifest,
|
|
51
|
+
ComponentInstance,
|
|
52
|
+
ContentConstraints,
|
|
53
|
+
ImageMetadata,
|
|
54
|
+
ManifestEntry,
|
|
55
|
+
ManifestMetadata,
|
|
56
|
+
PageEntry,
|
|
57
|
+
TailwindColor,
|
|
58
|
+
TextStyleValue,
|
|
59
|
+
} from '@nuasite/cms-types'
|
|
29
60
|
|
|
30
61
|
/** SEO tracking options */
|
|
31
62
|
export interface SeoOptions {
|
|
@@ -52,40 +83,6 @@ export interface CmsMarkerOptions {
|
|
|
52
83
|
seo?: SeoOptions
|
|
53
84
|
}
|
|
54
85
|
|
|
55
|
-
/** Background image metadata for elements using bg-[url()] */
|
|
56
|
-
export interface BackgroundImageMetadata {
|
|
57
|
-
/** Full Tailwind class, e.g. bg-[url('/path.png')] */
|
|
58
|
-
bgImageClass: string
|
|
59
|
-
/** Extracted image URL, e.g. /path.png */
|
|
60
|
-
imageUrl: string
|
|
61
|
-
/** Background size class: bg-auto | bg-cover | bg-contain */
|
|
62
|
-
bgSize?: string
|
|
63
|
-
/** Background position class: bg-center | bg-top | bg-bottom-left | ... */
|
|
64
|
-
bgPosition?: string
|
|
65
|
-
/** Background repeat class: bg-repeat | bg-no-repeat | bg-repeat-x | bg-repeat-y */
|
|
66
|
-
bgRepeat?: string
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Image metadata for better tracking and integrity */
|
|
70
|
-
export interface ImageMetadata {
|
|
71
|
-
/** Image source URL */
|
|
72
|
-
src: string
|
|
73
|
-
/** Alt text */
|
|
74
|
-
alt: string
|
|
75
|
-
/** SHA256 hash of image content (for integrity checking) */
|
|
76
|
-
hash?: string
|
|
77
|
-
/** Image dimensions */
|
|
78
|
-
dimensions?: { width: number; height: number }
|
|
79
|
-
/** Responsive image srcset */
|
|
80
|
-
srcSet?: string
|
|
81
|
-
/** Image sizes attribute */
|
|
82
|
-
sizes?: string
|
|
83
|
-
/** 0-based DOM-order index of this `<img>` among same-(src, sourceFile)
|
|
84
|
-
* occurrences on the page. Disambiguates source location when the same
|
|
85
|
-
* image URL appears multiple times in the same source file. */
|
|
86
|
-
srcOccurrence?: number
|
|
87
|
-
}
|
|
88
|
-
|
|
89
86
|
/** Identifies the (collection, entry, field) destination for an editor upload. */
|
|
90
87
|
export interface MediaUploadContext {
|
|
91
88
|
collection: string
|
|
@@ -93,186 +90,6 @@ export interface MediaUploadContext {
|
|
|
93
90
|
field: string
|
|
94
91
|
}
|
|
95
92
|
|
|
96
|
-
/** Content constraints for validation */
|
|
97
|
-
export interface ContentConstraints {
|
|
98
|
-
/** Maximum content length */
|
|
99
|
-
maxLength?: number
|
|
100
|
-
/** Minimum content length */
|
|
101
|
-
minLength?: number
|
|
102
|
-
/** Regex pattern for validation */
|
|
103
|
-
pattern?: string
|
|
104
|
-
/** Allowed HTML tags for rich text content */
|
|
105
|
-
allowedTags?: string[]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Represents a single Tailwind color with its shades and values */
|
|
109
|
-
export interface TailwindColor {
|
|
110
|
-
/** Color name (e.g., 'red', 'blue', 'primary') */
|
|
111
|
-
name: string
|
|
112
|
-
/** Map of shade to CSS color value (e.g., { '500': '#ef4444', '600': '#dc2626' }) */
|
|
113
|
-
values: Record<string, string>
|
|
114
|
-
/** Whether this is a custom/theme color vs default Tailwind */
|
|
115
|
-
isCustom?: boolean
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Attribute with source information for git diff tracking */
|
|
119
|
-
export interface Attribute {
|
|
120
|
-
/** The resolved attribute value (from rendered HTML) */
|
|
121
|
-
value: string
|
|
122
|
-
/** The expression text if dynamic (e.g., "component.githubUrl") */
|
|
123
|
-
sourceExpression?: string
|
|
124
|
-
/** Path to the source file where the value is defined */
|
|
125
|
-
sourcePath?: string
|
|
126
|
-
/** Line number where the value is defined in source (1-indexed) */
|
|
127
|
-
sourceLine?: number
|
|
128
|
-
/** The exact source snippet that can be replaced for git diff */
|
|
129
|
-
sourceSnippet?: string
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Available colors palette from Tailwind config */
|
|
133
|
-
export interface AvailableColors {
|
|
134
|
-
/** All available colors with their shades */
|
|
135
|
-
colors: TailwindColor[]
|
|
136
|
-
/** Default Tailwind color names */
|
|
137
|
-
defaultColors: string[]
|
|
138
|
-
/** Custom/theme color names */
|
|
139
|
-
customColors: string[]
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Text style value with class name and CSS properties */
|
|
143
|
-
export interface TextStyleValue {
|
|
144
|
-
/** Tailwind class name (e.g., 'font-bold', 'text-xl') */
|
|
145
|
-
class: string
|
|
146
|
-
/** Display label for UI */
|
|
147
|
-
label: string
|
|
148
|
-
/** CSS properties to apply (e.g., { fontWeight: '700' }) */
|
|
149
|
-
css: Record<string, string>
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Available text styles from Tailwind config */
|
|
153
|
-
export interface AvailableTextStyles {
|
|
154
|
-
/** Font weight options (font-normal, font-bold, etc.) */
|
|
155
|
-
fontWeight: TextStyleValue[]
|
|
156
|
-
/** Font size options (text-xs, text-sm, text-base, etc.) */
|
|
157
|
-
fontSize: TextStyleValue[]
|
|
158
|
-
/** Text decoration options (underline, line-through, etc.) */
|
|
159
|
-
textDecoration: TextStyleValue[]
|
|
160
|
-
/** Font style options (italic, not-italic) */
|
|
161
|
-
fontStyle: TextStyleValue[]
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export interface ManifestEntry {
|
|
165
|
-
id: string
|
|
166
|
-
tag: string
|
|
167
|
-
/** Plain text content (for display/search) */
|
|
168
|
-
text: string
|
|
169
|
-
/** HTML content when element contains inline styling (strong, em, etc.) */
|
|
170
|
-
html?: string
|
|
171
|
-
sourcePath?: string
|
|
172
|
-
sourceLine?: number
|
|
173
|
-
/** Full element snippet from opening to closing tag (for text content updates) */
|
|
174
|
-
sourceSnippet?: string
|
|
175
|
-
variableName?: string
|
|
176
|
-
childCmsIds?: string[]
|
|
177
|
-
parentComponentId?: string
|
|
178
|
-
/** Collection name for collection entries (e.g., 'services', 'blog') */
|
|
179
|
-
collectionName?: string
|
|
180
|
-
/** Entry slug for collection entries (e.g., '3d-tisk') */
|
|
181
|
-
collectionSlug?: string
|
|
182
|
-
/** Schema field name when this entry was resolved to a specific collection field (e.g., 'image', 'cover'). */
|
|
183
|
-
collectionFieldName?: string
|
|
184
|
-
/** Path to the markdown content file (e.g., 'src/content/blog/my-post.md') */
|
|
185
|
-
contentPath?: string
|
|
186
|
-
|
|
187
|
-
// === Robustness fields ===
|
|
188
|
-
|
|
189
|
-
/** Stable ID derived from content + context hash, survives rebuilds */
|
|
190
|
-
stableId?: string
|
|
191
|
-
/** SHA256 hash of sourceSnippet at generation time for conflict detection */
|
|
192
|
-
sourceHash?: string
|
|
193
|
-
/** Image metadata for img elements (replaces imageSrc/imageAlt) */
|
|
194
|
-
imageMetadata?: ImageMetadata
|
|
195
|
-
/** Background image metadata for elements using bg-[url()] */
|
|
196
|
-
backgroundImage?: BackgroundImageMetadata
|
|
197
|
-
/** Content validation constraints */
|
|
198
|
-
constraints?: ContentConstraints
|
|
199
|
-
/** Color classes applied to this element (for buttons, etc.) */
|
|
200
|
-
colorClasses?: Record<string, Attribute>
|
|
201
|
-
/** All HTML attributes with source information */
|
|
202
|
-
attributes?: Record<string, Attribute>
|
|
203
|
-
/** Whether inline text styling (bold, italic, etc.) can be applied.
|
|
204
|
-
* False when text comes from a string variable/prop that cannot contain HTML markup. */
|
|
205
|
-
allowStyling?: boolean
|
|
206
|
-
|
|
207
|
-
// === Reference field metadata ===
|
|
208
|
-
|
|
209
|
-
/** Collection the text was found in when it came through a reference (e.g., 'authors') */
|
|
210
|
-
referenceCollection?: string
|
|
211
|
-
/** Collections that have reference fields pointing to referenceCollection */
|
|
212
|
-
referencedBy?: Array<{ collection: string; fieldName: string; isArray?: boolean }>
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export interface ComponentInstance {
|
|
216
|
-
id: string
|
|
217
|
-
componentName: string
|
|
218
|
-
file: string
|
|
219
|
-
sourcePath: string
|
|
220
|
-
sourceLine: number
|
|
221
|
-
props: Record<string, any>
|
|
222
|
-
slots?: Record<string, string>
|
|
223
|
-
parentId?: string
|
|
224
|
-
/** File where this component is invoked (parent page/layout) */
|
|
225
|
-
invocationSourcePath?: string
|
|
226
|
-
/** 0-based index among same-name component invocations in the parent file */
|
|
227
|
-
invocationIndex?: number
|
|
228
|
-
/** Whether this component represents an inline HTML element inside a .map() array */
|
|
229
|
-
isInlineArray?: boolean
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Manifest metadata for versioning and conflict detection */
|
|
233
|
-
export interface ManifestMetadata {
|
|
234
|
-
/** Manifest schema version */
|
|
235
|
-
version: string
|
|
236
|
-
/** ISO timestamp when manifest was generated */
|
|
237
|
-
generatedAt: string
|
|
238
|
-
/** Build system that generated the manifest (e.g., 'astro', 'vite') */
|
|
239
|
-
generatedBy?: string
|
|
240
|
-
/** Build ID for correlation */
|
|
241
|
-
buildId?: string
|
|
242
|
-
/** SHA256 hash of all entry content for quick drift detection */
|
|
243
|
-
contentHash?: string
|
|
244
|
-
/** Per-source-file hashes for granular conflict detection */
|
|
245
|
-
sourceFileHashes?: Record<string, string>
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** Page entry for the global manifest */
|
|
249
|
-
export interface PageEntry {
|
|
250
|
-
/** Page URL pathname (e.g., '/', '/about') */
|
|
251
|
-
pathname: string
|
|
252
|
-
/** Page title from SEO data */
|
|
253
|
-
title?: string
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export interface CmsManifest {
|
|
257
|
-
/** Manifest metadata for versioning and conflict detection */
|
|
258
|
-
metadata?: ManifestMetadata
|
|
259
|
-
entries: Record<string, ManifestEntry>
|
|
260
|
-
components: Record<string, ComponentInstance>
|
|
261
|
-
componentDefinitions: Record<string, ComponentDefinition>
|
|
262
|
-
/** Content collection entries indexed by "collectionName/slug" */
|
|
263
|
-
collections?: Record<string, CollectionEntry>
|
|
264
|
-
/** Collection definitions with inferred schemas */
|
|
265
|
-
collectionDefinitions?: Record<string, CollectionDefinition>
|
|
266
|
-
/** Available Tailwind colors from the project's config */
|
|
267
|
-
availableColors?: AvailableColors
|
|
268
|
-
/** Available text styles from the project's Tailwind config */
|
|
269
|
-
availableTextStyles?: AvailableTextStyles
|
|
270
|
-
/** All pages in the site with pathname and title */
|
|
271
|
-
pages?: PageEntry[]
|
|
272
|
-
/** Component names allowed in the MDX component picker (undefined = all) */
|
|
273
|
-
mdxComponents?: string[]
|
|
274
|
-
}
|
|
275
|
-
|
|
276
93
|
// === SEO Types ===
|
|
277
94
|
|
|
278
95
|
/** Source tracking information for SEO elements */
|