@nuasite/cms 0.46.3 → 0.46.4

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 CHANGED
@@ -386,7 +386,7 @@ function IC(t, e) {
386
386
  function _C(t, e) {
387
387
  return typeof e == "function" ? e(t) : e;
388
388
  }
389
- const u5 = "0.46.3", h5 = u5, ct = {
389
+ const u5 = "0.46.4", h5 = u5, ct = {
390
390
  /** Highlight overlay for hovered elements */
391
391
  HIGHLIGHT: 2147483644,
392
392
  /** Hover outline for elements/components */
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.3",
17
+ "version": "0.46.4",
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.3",
30
- "@nuasite/cms-types": "0.46.3",
29
+ "@nuasite/cms-core": "0.46.4",
30
+ "@nuasite/cms-types": "0.46.4",
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.3",
39
- "@nuasite/collections-admin": "0.46.3",
38
+ "@nuasite/cms-sidecar": "0.46.4",
39
+ "@nuasite/collections-admin": "0.46.4",
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.3",
80
- "@nuasite/collections-admin": "0.46.3",
79
+ "@nuasite/cms-sidecar": "0.46.4",
80
+ "@nuasite/collections-admin": "0.46.4",
81
81
  "react": "^19.0.0",
82
82
  "react-dom": "^19.0.0"
83
83
  },
@@ -1,3 +1,4 @@
1
+ import path from 'node:path'
1
2
  import type { CachedParsedFile, ImageIndexEntry, SearchIndexEntry, SourceLocation } from './types'
2
3
 
3
4
  // ============================================================================
@@ -113,10 +114,16 @@ export function markFileDirty(absPath: string): void {
113
114
  dirtyFiles.add(absPath)
114
115
  // Also evict the parsed file cache so it's re-read from disk
115
116
  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()
117
+ // A changed file may add/remove/alter its declared URL, so drop the URL→file
118
+ // index for its collection directory. The index is keyed by collection dir:
119
+ // for a flat `<dir>/<slug>.md` that's the file's directory; for Hugo-style
120
+ // `<dir>/<slug>/index.md` it's the parent of the file's directory. Evict both
121
+ // candidates (deleting a non-existent key is a no-op) instead of clearing the
122
+ // whole cache, which would rebuild every collection's index on the next
123
+ // unmatched-page lookup.
124
+ const fileDir = path.dirname(absPath)
125
+ declaredUrlIndexCache.delete(fileDir)
126
+ declaredUrlIndexCache.delete(path.dirname(fileDir))
120
127
  }
121
128
 
122
129
  export function getDirtyFiles(): Set<string> {
@@ -300,7 +300,7 @@ export async function findCollectionSource(
300
300
  // Prefer the entry whose declared canonical URL equals the requested
301
301
  // path. Only kicks in when an entry actually declares a URL, so
302
302
  // URL-less projects fall through to the filename logic unchanged.
303
- const byUrl = await resolveByDeclaredUrl(matches, requestedUrl, contentPath)
303
+ const byUrl = await resolveByDeclaredUrl(matches, requestedUrl, contentPath, pathParts[0])
304
304
  if (byUrl) {
305
305
  // byUrl.file may differ from the file the filename match found
306
306
  // (that's the whole point of this fallback) — its slug must be
@@ -331,6 +331,29 @@ export async function findCollectionSource(
331
331
  }
332
332
  }
333
333
 
334
+ // No filename-based candidate matched any tail slug. The source file may be
335
+ // named unlike its URL entirely — e.g. people entries stored as
336
+ // `<role>__<slug>.md` but served at `/<family>/<slug>`, so no tail segment
337
+ // ever hits a `<slug>.md` file. Fall back to resolving by declared canonical
338
+ // URL across every collection.
339
+ //
340
+ // Guarded to multi-segment paths: the tail-slug loop above only ran for
341
+ // `pathParts.length >= 2`, so single-segment paths (bare static pages like
342
+ // `/about`) were never treated as collection pages. Keeping that boundary
343
+ // avoids both a full declared-URL scan on every such page and the risk of
344
+ // mis-attributing a static page to a collection entry that happens to
345
+ // declare the same URL.
346
+ if (pathParts.length >= 2) {
347
+ const byDeclaredUrl = await findByDeclaredUrlAcross(collectionDirs, contentPath, requestedUrl, pathParts[0])
348
+ if (byDeclaredUrl) {
349
+ return {
350
+ name: byDeclaredUrl.name,
351
+ slug: slugFromFilePath(byDeclaredUrl.file),
352
+ file: path.relative(getProjectRoot(), byDeclaredUrl.file),
353
+ }
354
+ }
355
+ }
356
+
334
357
  return undefined
335
358
  }
336
359
 
@@ -428,6 +451,7 @@ async function resolveByDeclaredUrl(
428
451
  matches: { name: string; file: string }[],
429
452
  requestedUrl: string,
430
453
  contentPath: string,
454
+ urlPrefix: string | undefined,
431
455
  ): Promise<{ name: string; file: string } | undefined> {
432
456
  let sawDeclaredUrl = false
433
457
  for (const m of matches) {
@@ -441,13 +465,30 @@ async function resolveByDeclaredUrl(
441
465
 
442
466
  // Contradiction: the right entry is named differently from its slug. Scan
443
467
  // the collection(s) that produced filename matches for a declared-URL hit.
444
- // `matches` (and thus this Set) is built by iterating the sorted
445
- // `collectionDirs`, so directory order here is deterministic.
446
- for (const dir of new Set(matches.map(m => m.name))) {
468
+ return findByDeclaredUrlAcross([...new Set(matches.map(m => m.name))], contentPath, requestedUrl, urlPrefix)
469
+ }
470
+
471
+ /**
472
+ * Resolve `requestedUrl` to a source file by its declared canonical URL across
473
+ * the given collection directories, using the cached per-directory URL→file
474
+ * index. When more than one collection declares the same URL, a directory whose
475
+ * name equals the URL's first path segment wins; otherwise the sorted-first
476
+ * match is returned so the result is deterministic regardless of readdir order.
477
+ */
478
+ async function findByDeclaredUrlAcross(
479
+ dirs: string[],
480
+ contentPath: string,
481
+ requestedUrl: string,
482
+ urlPrefix: string | undefined,
483
+ ): Promise<{ name: string; file: string } | undefined> {
484
+ let firstHit: { name: string; file: string } | undefined
485
+ for (const dir of [...dirs].sort()) {
447
486
  const hit = await findFileByDeclaredUrl(path.join(contentPath, dir), requestedUrl)
448
- if (hit) return { name: dir, file: hit }
487
+ if (!hit) continue
488
+ if (dir === urlPrefix) return { name: dir, file: hit }
489
+ firstHit ??= { name: dir, file: hit }
449
490
  }
450
- return undefined
491
+ return firstHit
451
492
  }
452
493
 
453
494
  /**