@nuasite/cms 0.31.0 → 0.34.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.31.0",
17
+ "version": "0.34.0",
18
18
  "module": "src/index.ts",
19
19
  "types": "src/index.ts",
20
20
  "type": "module",
@@ -32,6 +32,7 @@
32
32
  "yaml": "^2.8.3"
33
33
  },
34
34
  "devDependencies": {
35
+ "@babel/types": "^7.29.0",
35
36
  "@milkdown/core": "^7.20.0",
36
37
  "@milkdown/ctx": "^7.20.0",
37
38
  "@milkdown/plugin-listener": "^7.20.0",
@@ -0,0 +1,74 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ const HUGO_INDEX_RE = /^index\.(md|mdx)$/i
6
+
7
+ /** True when the entry uses the Hugo-style `<slug>/index.md(x)` layout (vs. a flat `<slug>.md` file). */
8
+ export function isHugoStyleEntry(entryAbsPath: string): boolean {
9
+ return HUGO_INDEX_RE.test(path.basename(entryAbsPath))
10
+ }
11
+
12
+ /** First 8 hex chars of a sha256 digest — enough entropy to disambiguate collision suffixes. */
13
+ export function shortContentHash(buf: Buffer): string {
14
+ return createHash('sha256').update(buf).digest('hex').slice(0, 8)
15
+ }
16
+
17
+ /**
18
+ * Compute the on-disk target for an Astro `image()` field's value.
19
+ *
20
+ * Layout rules:
21
+ * - Hugo-style entry (`<slug>/index.md`) → file goes inside the entry's directory, bare filename.
22
+ * - Flat entry (`<slug>.md`) → file goes next to the entry, prefixed with the slug
23
+ * so multiple entries don't collide on a shared filename.
24
+ *
25
+ * If a candidate slot already holds a file with identical bytes to `compareBuffer`,
26
+ * it's reused; otherwise the filename gets a content-hash suffix.
27
+ */
28
+ export async function pickAstroImageTarget(args: {
29
+ entryAbsPath: string
30
+ slug: string
31
+ originalFilename: string
32
+ compareBuffer: Buffer
33
+ }): Promise<{ absPath: string; relPath: string }> {
34
+ const entryDir = path.dirname(args.entryAbsPath)
35
+ const isHugoStyle = isHugoStyleEntry(args.entryAbsPath)
36
+
37
+ // `originalFilename` may come from user-controlled multipart input — strip any
38
+ // directory components so a `../` segment can't escape the entry directory.
39
+ const safeFilename = path.basename(args.originalFilename)
40
+ if (!safeFilename || safeFilename === '.' || safeFilename === '..') {
41
+ throw new Error(`Invalid filename: ${args.originalFilename}`)
42
+ }
43
+
44
+ const baseName = isHugoStyle ? safeFilename : `${args.slug}-${safeFilename}`
45
+ const baseAbs = path.join(entryDir, baseName)
46
+ if (await isFreeOrMatching(baseAbs, args.compareBuffer)) {
47
+ return { absPath: baseAbs, relPath: `./${baseName}` }
48
+ }
49
+
50
+ const hash = shortContentHash(args.compareBuffer)
51
+ const ext = path.extname(safeFilename)
52
+ const stem = path.basename(baseName, ext)
53
+ for (let attempt = 0; attempt < 5; attempt++) {
54
+ const suffix = attempt === 0 ? hash : `${hash}-${attempt}`
55
+ const candidateName = `${stem}-${suffix}${ext}`
56
+ const candidateAbs = path.join(entryDir, candidateName)
57
+ if (await isFreeOrMatching(candidateAbs, args.compareBuffer)) {
58
+ return { absPath: candidateAbs, relPath: `./${candidateName}` }
59
+ }
60
+ }
61
+ throw new Error(`Could not pick a unique filename for ${safeFilename} in ${entryDir}`)
62
+ }
63
+
64
+ /** True if the slot is empty, or holds a file with identical bytes to `compareBuffer`. */
65
+ async function isFreeOrMatching(absPath: string, compareBuffer: Buffer): Promise<boolean> {
66
+ try {
67
+ const stat = await fs.stat(absPath)
68
+ if (stat.size !== compareBuffer.length) return false
69
+ const existing = await fs.readFile(absPath)
70
+ return compareBuffer.equals(existing)
71
+ } catch {
72
+ return true
73
+ }
74
+ }
@@ -2,8 +2,9 @@ import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import { isMap, isPair, isScalar, parse as parseYaml, parseDocument } from 'yaml'
4
4
  import { getProjectRoot } from './config'
5
+ import { parseContentConfig, type ParsedConfig } from './content-config-ast'
5
6
  import { slugifyHref } from './shared'
6
- import type { CollectionDefinition, CollectionEntryInfo, FieldDefinition, FieldHints, FieldType } from './types'
7
+ import type { CollectionDefinition, CollectionEntryInfo, FieldDefinition, FieldType } from './types'
7
8
 
8
9
  /** Regex patterns for type inference */
9
10
  const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}/
@@ -416,162 +417,50 @@ async function scanCollection(collectionPath: string, collectionName: string, co
416
417
  }
417
418
 
418
419
  /**
419
- * Read and parse the Astro content config file, extracting schema blocks for each collection.
420
- * Returns parsed blocks with collection names and their raw schema bodies.
420
+ * Filter scanned fields to schema-only and apply per-field overrides (type, hints, required)
421
+ * in a single pass. Filtering must happen first since it can shrink `def.fields`.
421
422
  */
422
- async function parseContentConfigSchemaBlocks(): Promise<Array<{ collectionName: string; schemaBody: string }>> {
423
- const projectRoot = getProjectRoot()
424
-
425
- for (const configPath of ['src/content/config.ts', 'src/content.config.ts']) {
426
- try {
427
- const fullPath = path.join(projectRoot, configPath)
428
- const content = await fs.readFile(fullPath, 'utf-8')
429
-
430
- // Map variable names to collection names from exports
431
- const varToName = new Map<string, string>()
432
- const exportMatch = content.match(/export\s+const\s+collections\s*=\s*\{([\s\S]*?)\}/)
433
- if (exportMatch) {
434
- const pairs = exportMatch[1]!.matchAll(/(\w+)\s*:\s*(\w+)/g)
435
- for (const m of pairs) {
436
- varToName.set(m[2]!, m[1]!)
437
- }
438
- }
439
-
440
- // Find schema block starts via regex, then extract bodies with brace counting
441
- // to correctly handle nested objects like n.number({ min: 1, max: 100 })
442
- const schemaStart = /(?:const\s+(\w+)\s*=\s*)?defineCollection\s*\(\s*\{[\s\S]*?schema\s*:\s*(?:z|n)\.object\s*\(\s*\{/g
443
- const blocks: Array<{ collectionName: string; schemaBody: string }> = []
444
-
445
- let match
446
- while ((match = schemaStart.exec(content)) !== null) {
447
- const varName = match[1]
448
- const collectionName = varName ? varToName.get(varName) : undefined
449
- if (!collectionName) continue
450
-
451
- // Brace-balanced extraction: the regex consumed the opening {,
452
- // so start at depth 1 and scan forward for the matching }
453
- const bodyStart = match.index + match[0].length
454
- let depth = 1
455
- let i = bodyStart
456
- while (i < content.length && depth > 0) {
457
- if (content[i] === '{') depth++
458
- else if (content[i] === '}') depth--
459
- i++
460
- }
461
-
462
- if (depth === 0) {
463
- // i is one past the matching }, so body is [bodyStart, i-1)
464
- blocks.push({ collectionName, schemaBody: content.slice(bodyStart, i - 1) })
465
- }
466
- }
467
-
468
- if (blocks.length > 0) return blocks
469
- } catch {
470
- // File doesn't exist, try next
471
- }
472
- }
473
- return []
474
- }
475
-
476
- /**
477
- * Parse the Astro content config file to extract explicit reference() declarations.
478
- * Returns a map: collectionName → { fieldName → { target, isArray } }
479
- */
480
- function parseContentConfigReferences(
481
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
482
- ): Map<string, Map<string, { target: string; isArray: boolean }>> {
483
- const result = new Map<string, Map<string, { target: string; isArray: boolean }>>()
484
-
485
- for (const { collectionName, schemaBody } of schemaBlocks) {
486
- const fields = new Map<string, { target: string; isArray: boolean }>()
487
- const fieldRefs = schemaBody.matchAll(/(\w+)\s*:\s*(z\.array\s*\(\s*)?reference\s*\(\s*['"](\w+)['"]\s*\)/g)
488
- for (const m of fieldRefs) {
489
- fields.set(m[1]!, { target: m[3]!, isArray: !!m[2] })
490
- }
491
-
492
- if (fields.size > 0) {
493
- result.set(collectionName, fields)
494
- }
495
- }
496
- return result
497
- }
498
-
499
- /** Valid field type names exported by `n` helper from @nuasite/cms */
500
- const FIELD_HELPER_TYPES = new Set(['text', 'number', 'image', 'url', 'email', 'tel', 'color', 'date', 'datetime', 'time', 'textarea'])
423
+ function applyParsedConfig(
424
+ collections: Record<string, CollectionDefinition>,
425
+ parsed: ParsedConfig,
426
+ ): void {
427
+ for (const [collectionName, parsedColl] of parsed) {
428
+ const def = collections[collectionName]
429
+ if (!def) continue
501
430
 
502
- /**
503
- * Parse the content config file to extract explicit field type hints:
504
- * - `n.image()`, `n.url()`, etc. from @nuasite/cms
505
- * - `z.enum([...])` for select options
506
- *
507
- * Returns a map: collectionName → fieldName → { type, options? }
508
- */
509
- function parseContentConfigFieldTypes(
510
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
511
- ): Map<string, Map<string, { type: FieldType; options?: string[] }>> {
512
- const result = new Map<string, Map<string, { type: FieldType; options?: string[] }>>()
513
-
514
- for (const { collectionName, schemaBody } of schemaBlocks) {
515
- const fields = new Map<string, { type: FieldType; options?: string[] }>()
516
-
517
- // Detect n.image(), n.url(), etc.
518
- const fieldHelpers = schemaBody.matchAll(/(\w+)\s*:\s*n\.(\w+)/g)
519
- for (const m of fieldHelpers) {
520
- const fieldName = m[1]!
521
- const helperName = m[2]!
522
- if (FIELD_HELPER_TYPES.has(helperName)) {
523
- fields.set(fieldName, { type: helperName as FieldType })
524
- }
431
+ if (parsedColl.fields.length > 0) {
432
+ const schemaNames = new Set(parsedColl.fields.map(f => f.name))
433
+ def.fields = def.fields.filter(f => schemaNames.has(f.name))
525
434
  }
526
435
 
527
- // Detect z.enum(['a', 'b', 'c'])
528
- const enumFields = schemaBody.matchAll(/(\w+)\s*:\s*z\.enum\s*\(\s*\[([\s\S]*?)\]\s*\)/g)
529
- for (const m of enumFields) {
530
- const fieldName = m[1]!
531
- const enumBody = m[2]!
532
- const options = [...enumBody.matchAll(/['"]([^'"]+)['"]/g)].map(o => o[1]!)
533
- if (options.length > 0) {
534
- fields.set(fieldName, { type: 'select', options })
436
+ const fieldsByName = new Map(def.fields.map(f => [f.name, f]))
437
+ for (const pf of parsedColl.fields) {
438
+ const field = fieldsByName.get(pf.name)
439
+ if (!field) continue
440
+ if (pf.type) {
441
+ field.type = pf.type
442
+ if (pf.options) field.options = pf.options
535
443
  }
536
- }
537
-
538
- if (fields.size > 0) {
539
- result.set(collectionName, fields)
540
- }
541
- }
542
- return result
543
- }
544
-
545
- /**
546
- * Parse the content config to find `.orderBy('asc'|'desc')` markers on fields.
547
- * Matches patterns like `fieldName: n.number().orderBy('asc')`.
548
- * Returns a map: collectionName → { field, direction }.
549
- */
550
- function parseContentConfigOrderBy(
551
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
552
- ): Map<string, { field: string; direction: 'asc' | 'desc' }> {
553
- const result = new Map<string, { field: string; direction: 'asc' | 'desc' }>()
554
- for (const { collectionName, schemaBody } of schemaBlocks) {
555
- const match = schemaBody.match(/(\w+)\s*:.*\.orderBy\s*\(\s*(?:['"](\w+)['"])?\s*\)/)
556
- if (match) {
557
- const direction = match[2] === 'desc' ? 'desc' as const : 'asc' as const
558
- result.set(collectionName, { field: match[1]!, direction })
444
+ if (pf.hints) field.hints = pf.hints
445
+ if (pf.astroImage) field.astroImage = true
446
+ field.required = pf.required
559
447
  }
560
448
  }
561
- return result
562
449
  }
563
450
 
564
- /**
565
- * Apply orderBy configuration: set the field name and direction on the definition, then re-sort entries.
566
- */
451
+ /** Apply orderBy configuration: set the field name and direction on the definition, then re-sort entries. */
567
452
  function applyCollectionOrderBy(
568
453
  collections: Record<string, CollectionDefinition>,
569
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
454
+ parsed: ParsedConfig,
570
455
  ): void {
571
- const orderByFields = parseContentConfigOrderBy(schemaBlocks)
572
- for (const [collectionName, { field: fieldName, direction }] of orderByFields) {
456
+ for (const [collectionName, parsedColl] of parsed) {
457
+ const orderField = parsedColl.fields.find(f => f.orderBy)
458
+ if (!orderField?.orderBy) continue
573
459
  const def = collections[collectionName]
574
460
  if (!def) continue
461
+
462
+ const fieldName = orderField.name
463
+ const direction = orderField.orderBy.direction
575
464
  def.orderBy = fieldName
576
465
  def.orderDirection = direction
577
466
  if (def.entries && def.entries.length > 1) {
@@ -590,200 +479,35 @@ function applyCollectionOrderBy(
590
479
  }
591
480
  }
592
481
 
593
- /** Match `fieldName:` patterns at the start of lines within a schema body. */
594
- const SCHEMA_FIELD_PATTERN = /^\s*(\w+)\s*:/gm
595
-
596
482
  /**
597
- * When a content config schema exists, filter scanned fields to only include
598
- * those defined in the schema. This prevents stale or extra frontmatter fields
599
- * from appearing in the CMS editor.
483
+ * Detect reference fields. Prefers explicit `reference()` declarations from the content
484
+ * config; if none are found anywhere, falls back to heuristic slug matching.
600
485
  */
601
- function filterFieldsBySchema(
486
+ function detectReferenceFields(
602
487
  collections: Record<string, CollectionDefinition>,
603
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
488
+ parsed: ParsedConfig,
604
489
  ): void {
605
- for (const { collectionName, schemaBody } of schemaBlocks) {
490
+ let appliedAny = false
491
+ for (const [collectionName, parsedColl] of parsed) {
606
492
  const def = collections[collectionName]
607
493
  if (!def) continue
608
- const schemaNames = new Set<string>()
609
- for (const m of schemaBody.matchAll(SCHEMA_FIELD_PATTERN)) {
610
- schemaNames.add(m[1]!)
611
- }
612
- if (schemaNames.size === 0) continue
613
- def.fields = def.fields.filter(f => schemaNames.has(f.name))
614
- }
615
- }
616
-
617
- /**
618
- * Apply a parsed per-field config map to scanned collection definitions.
619
- */
620
- function applyPerFieldConfig<T>(
621
- collections: Record<string, CollectionDefinition>,
622
- configMap: Map<string, Map<string, T>>,
623
- apply: (field: FieldDefinition, value: T) => void,
624
- ): void {
625
- for (const [collectionName, fieldMap] of configMap) {
626
- const def = collections[collectionName]
627
- if (!def) continue
628
- for (const [fieldName, value] of fieldMap) {
629
- const field = def.fields.find(f => f.name === fieldName)
494
+ for (const pf of parsedColl.fields) {
495
+ if (!pf.reference) continue
496
+ const field = def.fields.find(f => f.name === pf.name)
630
497
  if (!field) continue
631
- apply(field, value)
632
- }
633
- }
634
- }
635
-
636
- /** Apply field type overrides from config parsing to scanned collections. */
637
- function applyConfigFieldTypes(
638
- collections: Record<string, CollectionDefinition>,
639
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
640
- ): void {
641
- applyPerFieldConfig(collections, parseContentConfigFieldTypes(schemaBlocks), (field, override) => {
642
- field.type = override.type
643
- if (override.options) field.options = override.options
644
- })
645
- }
646
-
647
- /** All recognized hint keys */
648
- const VALID_HINT_KEYS = new Set(['min', 'max', 'step', 'placeholder', 'maxLength', 'minLength', 'rows', 'accept'])
649
- /** Subset of hint keys that take numeric values */
650
- const NUMERIC_HINT_KEYS = new Set(['min', 'max', 'step', 'maxLength', 'minLength', 'rows'])
651
-
652
- /**
653
- * Parse `n.type({ key: value, ... })` options objects from schema blocks.
654
- * Returns a map: collectionName → fieldName → FieldHints.
655
- */
656
- function parseContentConfigFieldHints(
657
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
658
- ): Map<string, Map<string, FieldHints>> {
659
- const result = new Map<string, Map<string, FieldHints>>()
660
-
661
- for (const { collectionName, schemaBody } of schemaBlocks) {
662
- const fields = new Map<string, FieldHints>()
663
-
664
- // Match: fieldName: n.helperName({ ...options })
665
- const fieldMatches = schemaBody.matchAll(/(\w+)\s*:\s*n\.\w+\s*\(\s*\{([\s\S]*?)}\s*\)/g)
666
- for (const m of fieldMatches) {
667
- const fieldName = m[1]!
668
- const optionsBody = m[2]!
669
- const raw: Record<string, string | number> = {}
670
-
671
- // Extract key-value pairs from the options body
672
- const kvMatches = optionsBody.matchAll(/(\w+)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?[\d.]+))/g)
673
- for (const kv of kvMatches) {
674
- const key = kv[1]!
675
- if (!VALID_HINT_KEYS.has(key)) continue
676
- const strValue = kv[2] ?? kv[3]
677
- const numValue = kv[4]
678
-
679
- if (numValue != null && NUMERIC_HINT_KEYS.has(key)) {
680
- raw[key] = Number(numValue)
681
- } else if (strValue != null) {
682
- if (NUMERIC_HINT_KEYS.has(key)) {
683
- const parsed = Number(strValue)
684
- raw[key] = Number.isNaN(parsed) ? strValue : parsed
685
- } else {
686
- raw[key] = strValue
687
- }
688
- }
689
- }
690
- const hints = raw as FieldHints
691
-
692
- if (Object.keys(hints).length > 0) {
693
- fields.set(fieldName, hints)
694
- }
695
- }
696
-
697
- if (fields.size > 0) {
698
- result.set(collectionName, fields)
699
- }
700
- }
701
- return result
702
- }
703
-
704
- /**
705
- * Parse required/optional status from schema blocks.
706
- * In Zod, fields are required by default. `.optional()`, `.nullable()`, and `.default(...)` make them not required.
707
- */
708
- function parseContentConfigRequiredFields(
709
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
710
- ): Map<string, Map<string, boolean>> {
711
- const result = new Map<string, Map<string, boolean>>()
712
-
713
- for (const { collectionName, schemaBody } of schemaBlocks) {
714
- const fields = new Map<string, boolean>()
715
-
716
- const fieldMatches = [...schemaBody.matchAll(SCHEMA_FIELD_PATTERN)]
717
- for (let i = 0; i < fieldMatches.length; i++) {
718
- const fieldName = fieldMatches[i]![1]!
719
- const start = fieldMatches[i]!.index!
720
- const end = i + 1 < fieldMatches.length ? fieldMatches[i + 1]!.index! : schemaBody.length
721
- const fieldSource = schemaBody.slice(start, end)
722
-
723
- const isOptional = /\.(optional|nullable|default)\s*\(/.test(fieldSource)
724
- fields.set(fieldName, !isOptional)
725
- }
726
-
727
- if (fields.size > 0) {
728
- result.set(collectionName, fields)
729
- }
730
- }
731
- return result
732
- }
733
-
734
- /** Apply field hints from content config parsing to scanned collections. */
735
- function applyConfigFieldHints(
736
- collections: Record<string, CollectionDefinition>,
737
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
738
- ): void {
739
- applyPerFieldConfig(collections, parseContentConfigFieldHints(schemaBlocks), (field, hints) => {
740
- field.hints = hints
741
- })
742
- }
743
-
744
- /** Apply required/optional status from content config to scanned collections. */
745
- function applyConfigRequiredFields(
746
- collections: Record<string, CollectionDefinition>,
747
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
748
- ): void {
749
- applyPerFieldConfig(collections, parseContentConfigRequiredFields(schemaBlocks), (field, required) => {
750
- field.required = required
751
- })
752
- }
753
-
754
- /**
755
- * After all collections are scanned, detect reference fields.
756
- * Prefers explicit reference() declarations from the content config file.
757
- * Falls back to heuristic slug matching when no config is available.
758
- */
759
- async function detectReferenceFields(
760
- collections: Record<string, CollectionDefinition>,
761
- schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
762
- ): Promise<void> {
763
- // Try parsing the content config first — this is the source of truth
764
- const configRefs = parseContentConfigReferences(schemaBlocks)
765
- if (configRefs.size > 0) {
766
- for (const [collectionName, fieldRefs] of configRefs) {
767
- const def = collections[collectionName]
768
- if (!def) continue
769
- for (const [fieldName, ref] of fieldRefs) {
770
- const field = def.fields.find(f => f.name === fieldName)
771
- if (!field) continue
772
- if (ref.isArray) {
773
- field.type = 'array'
774
- field.itemType = 'reference'
775
- } else {
776
- field.type = 'reference'
777
- }
778
- field.collection = ref.target
779
- field.options = undefined
498
+ appliedAny = true
499
+ if (pf.reference.isArray) {
500
+ field.type = 'array'
501
+ field.itemType = 'reference'
502
+ } else {
503
+ field.type = 'reference'
780
504
  }
505
+ field.collection = pf.reference.target
506
+ field.options = undefined
781
507
  }
782
- return
783
508
  }
784
509
 
785
- // Fallback: heuristic detection by matching field values against collection slugs
786
- detectReferenceFieldsBySlugMatch(collections)
510
+ if (!appliedAny) detectReferenceFieldsBySlugMatch(collections)
787
511
  }
788
512
 
789
513
  function detectReferenceFieldsBySlugMatch(collections: Record<string, CollectionDefinition>): void {
@@ -1020,15 +744,12 @@ export async function scanCollections(contentDir: string = 'src/content'): Promi
1020
744
  // Content directory doesn't exist or isn't readable
1021
745
  }
1022
746
 
1023
- // Post-scan: apply explicit type hints, field hints, required status, detect references, derived fields, and ordering
1024
- const schemaBlocks = await parseContentConfigSchemaBlocks()
1025
- filterFieldsBySchema(collections, schemaBlocks)
1026
- applyConfigFieldTypes(collections, schemaBlocks)
1027
- applyConfigFieldHints(collections, schemaBlocks)
1028
- applyConfigRequiredFields(collections, schemaBlocks)
1029
- await detectReferenceFields(collections, schemaBlocks)
747
+ // Post-scan: apply schema-driven field config, detect references, derived fields, and ordering
748
+ const parsed = await parseContentConfig()
749
+ applyParsedConfig(collections, parsed)
750
+ detectReferenceFields(collections, parsed)
1030
751
  detectDerivedHrefFields(collections)
1031
- applyCollectionOrderBy(collections, schemaBlocks)
752
+ applyCollectionOrderBy(collections, parsed)
1032
753
 
1033
754
  return collections
1034
755
  }