@nuasite/cms 0.46.1 → 0.46.3
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 +2645 -2644
- package/package.json +7 -7
- package/src/dev-middleware.ts +48 -21
- package/src/editor/components/markdown-editor-overlay.tsx +2 -12
- package/src/shared.ts +22 -0
- package/src/source-finder/cache.ts +12 -0
- package/src/source-finder/collection-finder.ts +251 -79
- package/src/source-finder/index.ts +1 -0
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.46.
|
|
17
|
+
"version": "0.46.3",
|
|
18
18
|
"module": "src/index.ts",
|
|
19
19
|
"types": "src/index.ts",
|
|
20
20
|
"type": "module",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@nuasite/cms-core": "0.46.
|
|
30
|
-
"@nuasite/cms-types": "0.46.
|
|
29
|
+
"@nuasite/cms-core": "0.46.3",
|
|
30
|
+
"@nuasite/cms-types": "0.46.3",
|
|
31
31
|
"@astrojs/compiler": "^3.0.1",
|
|
32
32
|
"@babel/parser": "^7.29.2",
|
|
33
33
|
"node-html-parser": "^7.1.0",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"yaml": "^2.8.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@nuasite/cms-sidecar": "0.46.
|
|
39
|
-
"@nuasite/collections-admin": "0.46.
|
|
38
|
+
"@nuasite/cms-sidecar": "0.46.3",
|
|
39
|
+
"@nuasite/collections-admin": "0.46.3",
|
|
40
40
|
"@babel/types": "^7.29.0",
|
|
41
41
|
"@types/react": "^19.2.7",
|
|
42
42
|
"@types/react-dom": "^19.2.3",
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
"typescript": "^6.0.2",
|
|
77
77
|
"vite": "^7.0.0",
|
|
78
78
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
79
|
-
"@nuasite/cms-sidecar": "0.46.
|
|
80
|
-
"@nuasite/collections-admin": "0.46.
|
|
79
|
+
"@nuasite/cms-sidecar": "0.46.3",
|
|
80
|
+
"@nuasite/collections-admin": "0.46.3",
|
|
81
81
|
"react": "^19.0.0",
|
|
82
82
|
"react-dom": "^19.0.0"
|
|
83
83
|
},
|
package/src/dev-middleware.ts
CHANGED
|
@@ -17,6 +17,7 @@ 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
|
+
declaredSitePathFromData,
|
|
20
21
|
enhanceManifestWithSourceSnippets,
|
|
21
22
|
findCollectionSource,
|
|
22
23
|
findImageSourceLocation,
|
|
@@ -28,6 +29,7 @@ import type {
|
|
|
28
29
|
CmsMarkerOptions,
|
|
29
30
|
CollectionDefinition,
|
|
30
31
|
CollectionEntry,
|
|
32
|
+
CollectionEntryInfo,
|
|
31
33
|
ComponentDefinition,
|
|
32
34
|
ComponentInstance,
|
|
33
35
|
ManifestEntry,
|
|
@@ -176,24 +178,36 @@ export function createDevMiddleware(
|
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
// 2. Add collection entry pages from collection definitions,
|
|
179
|
-
// pre-populating pathnames
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
181
|
+
// pre-populating pathnames so the collections browser can redirect to
|
|
182
|
+
// detail pages without visiting them first. Prefer the entry's own
|
|
183
|
+
// declared URL (urlPath/permalink/…) from frontmatter, which is
|
|
184
|
+
// correct even when the collection is served under a *dynamic* route
|
|
185
|
+
// prefix (e.g. `[topic]/[slug]`) that discoverCollectionRoutes can't
|
|
186
|
+
// map to a static prefix. Fall back to the discovered static route
|
|
187
|
+
// prefix + slug. We build patched copies rather than mutating the
|
|
188
|
+
// originals so that heuristic pathnames don't persist if the route
|
|
189
|
+
// file is later removed.
|
|
183
190
|
const collectionDefs = manifestWriter.getCollectionDefinitions()
|
|
184
191
|
const collectionRoutes = await discoverCollectionRoutes()
|
|
185
192
|
const responseCollectionDefs: Record<string, CollectionDefinition> = {}
|
|
186
193
|
|
|
187
194
|
for (const [name, def] of Object.entries(collectionDefs)) {
|
|
188
|
-
const
|
|
189
|
-
const needsPatching = routePrefix && def.entries?.some(e => !e.pathname)
|
|
195
|
+
const routeInfo = collectionRoutes.get(def.name)
|
|
190
196
|
|
|
191
|
-
if (
|
|
197
|
+
if (routeInfo === undefined) {
|
|
198
|
+
// No page renders this collection at all — leave entries alone rather
|
|
199
|
+
// than fabricating a pathname from an unrelated frontmatter field
|
|
200
|
+
// (e.g. a data collection's own `url` field pointing elsewhere).
|
|
192
201
|
responseCollectionDefs[name] = def
|
|
193
202
|
} else {
|
|
194
|
-
|
|
203
|
+
const routePrefix = typeof routeInfo === 'string' ? routeInfo : undefined
|
|
204
|
+
const resolvePathname = (entry: CollectionEntryInfo): string | undefined =>
|
|
205
|
+
declaredSitePathFromData(entry.data) ?? (routePrefix ? `${routePrefix}${entry.slug}` : undefined)
|
|
206
|
+
const needsPatching = def.entries?.some(e => !e.pathname && resolvePathname(e))
|
|
207
|
+
|
|
208
|
+
responseCollectionDefs[name] = !needsPatching ? def : {
|
|
195
209
|
...def,
|
|
196
|
-
entries: def.entries!.map(e => e.pathname ? e : { ...e, pathname:
|
|
210
|
+
entries: def.entries!.map(e => e.pathname ? e : { ...e, pathname: resolvePathname(e) ?? e.pathname }),
|
|
197
211
|
}
|
|
198
212
|
}
|
|
199
213
|
|
|
@@ -693,8 +707,17 @@ async function discoverPagesFromFilesystem(): Promise<string[]> {
|
|
|
693
707
|
return pages
|
|
694
708
|
}
|
|
695
709
|
|
|
710
|
+
/**
|
|
711
|
+
* A collection's route info: a usable static URL prefix (e.g. '/blog/'), or
|
|
712
|
+
* `true` when the collection is confirmed routed by some page but no static
|
|
713
|
+
* prefix can be fabricated because an ancestor path segment is itself dynamic
|
|
714
|
+
* (e.g. `[topic]/[slug].astro`) — callers must resolve those entries' pathnames
|
|
715
|
+
* from their own declared URL instead of `${prefix}${slug}`.
|
|
716
|
+
*/
|
|
717
|
+
type CollectionRouteInfo = string | true
|
|
718
|
+
|
|
696
719
|
/** Cached result of collection route discovery; invalidated by file watcher */
|
|
697
|
-
let collectionRoutesCache: Map<string,
|
|
720
|
+
let collectionRoutesCache: Map<string, CollectionRouteInfo> | null = null
|
|
698
721
|
|
|
699
722
|
/** Invalidate the cached collection routes (called from vite-plugin when route files change) */
|
|
700
723
|
export function invalidateCollectionRoutesCache() {
|
|
@@ -702,16 +725,19 @@ export function invalidateCollectionRoutesCache() {
|
|
|
702
725
|
}
|
|
703
726
|
|
|
704
727
|
/**
|
|
705
|
-
* Discover
|
|
706
|
-
* (e.g. [slug].astro) that call getCollection(). Returns a
|
|
707
|
-
*
|
|
728
|
+
* Discover which collections are rendered as pages by scanning src/pages for
|
|
729
|
+
* dynamic route files (e.g. [slug].astro) that call getCollection(). Returns a
|
|
730
|
+
* map from collection name to its route info (see {@link CollectionRouteInfo}).
|
|
731
|
+
* Recurses into dynamic directories too (e.g. `[topic]/[slug].astro`) so those
|
|
732
|
+
* collections are still recognized as routed, just without a static prefix.
|
|
733
|
+
* Result is cached after first call.
|
|
708
734
|
*/
|
|
709
|
-
async function discoverCollectionRoutes(): Promise<Map<string,
|
|
735
|
+
export async function discoverCollectionRoutes(): Promise<Map<string, CollectionRouteInfo>> {
|
|
710
736
|
if (collectionRoutesCache) return collectionRoutesCache
|
|
711
737
|
|
|
712
738
|
const projectRoot = getProjectRoot()
|
|
713
739
|
const pagesDir = path.join(projectRoot, 'src', 'pages')
|
|
714
|
-
const routes = new Map<string,
|
|
740
|
+
const routes = new Map<string, CollectionRouteInfo>()
|
|
715
741
|
|
|
716
742
|
try {
|
|
717
743
|
await fs.access(pagesDir)
|
|
@@ -720,16 +746,14 @@ async function discoverCollectionRoutes(): Promise<Map<string, string>> {
|
|
|
720
746
|
return routes
|
|
721
747
|
}
|
|
722
748
|
|
|
723
|
-
async function walk(dir: string, urlPrefix: string) {
|
|
749
|
+
async function walk(dir: string, urlPrefix: string, dynamicAncestor: boolean) {
|
|
724
750
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
725
751
|
for (const entry of entries) {
|
|
726
752
|
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
|
|
727
753
|
|
|
728
754
|
const fullPath = path.join(dir, entry.name)
|
|
729
755
|
if (entry.isDirectory()) {
|
|
730
|
-
|
|
731
|
-
if (entry.name.includes('[')) continue
|
|
732
|
-
await walk(fullPath, `${urlPrefix}${entry.name}/`)
|
|
756
|
+
await walk(fullPath, `${urlPrefix}${entry.name}/`, dynamicAncestor || entry.name.includes('['))
|
|
733
757
|
} else {
|
|
734
758
|
const ext = path.extname(entry.name)
|
|
735
759
|
if (!PAGE_EXTENSIONS.has(ext)) continue
|
|
@@ -740,14 +764,17 @@ async function discoverCollectionRoutes(): Promise<Map<string, string>> {
|
|
|
740
764
|
const content = await fs.readFile(fullPath, 'utf-8')
|
|
741
765
|
const match = content.match(/getCollection\(\s*['"](\w+)['"]\s*\)/)
|
|
742
766
|
if (match?.[1]) {
|
|
743
|
-
|
|
767
|
+
// A static prefix is only usable when no ancestor directory is
|
|
768
|
+
// itself dynamic — otherwise record "routed, no prefix" so the
|
|
769
|
+
// caller still trusts declared URLs without fabricating one.
|
|
770
|
+
routes.set(match[1], dynamicAncestor ? true : urlPrefix)
|
|
744
771
|
}
|
|
745
772
|
} catch { /* skip unreadable files */ }
|
|
746
773
|
}
|
|
747
774
|
}
|
|
748
775
|
}
|
|
749
776
|
|
|
750
|
-
await walk(pagesDir, '/')
|
|
777
|
+
await walk(pagesDir, '/', false)
|
|
751
778
|
collectionRoutesCache = routes
|
|
752
779
|
return routes
|
|
753
780
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Editor, editorViewCtx } from '@milkdown/core'
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
3
|
-
import { slugify } from '../../shared'
|
|
3
|
+
import { resolveCreateRedirectUrl, slugify } from '../../shared'
|
|
4
4
|
import { updateMarkdownPage } from '../api'
|
|
5
5
|
import { STORAGE_KEYS, Z_INDEX } from '../constants'
|
|
6
6
|
import { cn } from '../lib/cn'
|
|
@@ -240,17 +240,7 @@ export function MarkdownEditorOverlay() {
|
|
|
240
240
|
})
|
|
241
241
|
|
|
242
242
|
if (result.success) {
|
|
243
|
-
|
|
244
|
-
const entries = opts.collectionDefinition.entries ?? []
|
|
245
|
-
const existingEntry = entries.find((e) => e.pathname)
|
|
246
|
-
let redirectUrl: string | undefined
|
|
247
|
-
if (existingEntry?.pathname) {
|
|
248
|
-
// Extract base path from an existing entry pathname (e.g., "/blog/first-post" → "/blog/")
|
|
249
|
-
const lastSlash = existingEntry.pathname.lastIndexOf('/')
|
|
250
|
-
if (lastSlash >= 0) {
|
|
251
|
-
redirectUrl = existingEntry.pathname.slice(0, lastSlash + 1) + slug
|
|
252
|
-
}
|
|
253
|
-
}
|
|
243
|
+
const redirectUrl = resolveCreateRedirectUrl(opts.collectionDefinition.entries ?? [], slug)
|
|
254
244
|
|
|
255
245
|
showToast(STRINGS.markdown.pageCreated, 'success')
|
|
256
246
|
resetMarkdownEditorState()
|
package/src/shared.ts
CHANGED
|
@@ -25,3 +25,25 @@ export function slugifyHref(text: string): string {
|
|
|
25
25
|
.replace(/[\s_]+/g, '-')
|
|
26
26
|
.replace(/^-+|-+$/g, '')
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Derive the pathname a newly created collection entry will resolve to, from
|
|
31
|
+
* the pathnames of existing sibling entries. Entries can resolve their own
|
|
32
|
+
* pathname from a per-entry declared URL field (see `declaredSitePathFromData`
|
|
33
|
+
* in the source-finder package), so entries in the same collection can live
|
|
34
|
+
* under different prefixes -- e.g. a collection served under a dynamic
|
|
35
|
+
* `[topic]/[slug]` route. Only reuse a sibling's prefix when every entry that
|
|
36
|
+
* has a pathname agrees on the same one; otherwise there's no way to know
|
|
37
|
+
* which prefix the new entry belongs under, and guessing wrong would send the
|
|
38
|
+
* user to an unrelated page.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveCreateRedirectUrl(entries: Array<{ pathname?: string }>, slug: string): string | undefined {
|
|
41
|
+
const prefixes = new Set(
|
|
42
|
+
entries
|
|
43
|
+
.map((e) => e.pathname)
|
|
44
|
+
.filter((p): p is string => !!p)
|
|
45
|
+
.map((p) => p.slice(0, p.lastIndexOf('/') + 1))
|
|
46
|
+
.filter((prefix) => prefix.length > 0),
|
|
47
|
+
)
|
|
48
|
+
return prefixes.size === 1 ? `${[...prefixes][0]}${slug}` : undefined
|
|
49
|
+
}
|
|
@@ -21,6 +21,9 @@ let searchIndexInitialized = false
|
|
|
21
21
|
/** Pre-built reverse index: normalizedText → SourceLocation[] (collection data files) */
|
|
22
22
|
let collectionTextIndex: Map<string, SourceLocation[]> | null = null
|
|
23
23
|
|
|
24
|
+
/** Per-collection-directory index: declared page URL → file abs path — used by the same-slug URL-disambiguation fallback */
|
|
25
|
+
const declaredUrlIndexCache = new Map<string, Map<string, string>>()
|
|
26
|
+
|
|
24
27
|
/** Lazy reverse index on i18n entries: translationKey → SearchIndexEntry[]. Rebuilt on demand after any mutation. */
|
|
25
28
|
let translationKeyIndex: Map<string, SearchIndexEntry[]> | null = null
|
|
26
29
|
|
|
@@ -93,6 +96,10 @@ export function setCollectionTextIndex(index: Map<string, SourceLocation[]> | nu
|
|
|
93
96
|
collectionTextIndex = index
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
export function getDeclaredUrlIndexCache(): Map<string, Map<string, string>> {
|
|
100
|
+
return declaredUrlIndexCache
|
|
101
|
+
}
|
|
102
|
+
|
|
96
103
|
// ============================================================================
|
|
97
104
|
// Dirty File Tracking (incremental re-indexing)
|
|
98
105
|
// ============================================================================
|
|
@@ -106,6 +113,10 @@ export function markFileDirty(absPath: string): void {
|
|
|
106
113
|
dirtyFiles.add(absPath)
|
|
107
114
|
// Also evict the parsed file cache so it's re-read from disk
|
|
108
115
|
parsedFileCache.delete(absPath)
|
|
116
|
+
// A changed file may add/remove/alter a declared URL anywhere in its
|
|
117
|
+
// collection directory — cheaper to drop the whole cache than track
|
|
118
|
+
// per-directory membership for a rarely-hit index.
|
|
119
|
+
declaredUrlIndexCache.clear()
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
export function getDirtyFiles(): Set<string> {
|
|
@@ -155,4 +166,5 @@ export function clearSourceFinderCache(): void {
|
|
|
155
166
|
searchIndexInitialized = false
|
|
156
167
|
collectionTextIndex = null
|
|
157
168
|
translationKeyIndex = null
|
|
169
|
+
declaredUrlIndexCache.clear()
|
|
158
170
|
}
|