@nuasite/cms 0.38.0 → 0.39.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.
@@ -1,4 +1,6 @@
1
1
  import type { ComponentNode, ElementNode, Node as AstroNode, TextNode } from '@astrojs/compiler/types'
2
+ import { parse as parseBabel } from '@babel/parser'
3
+ import type { Expression } from '@babel/types'
2
4
  import fs from 'node:fs/promises'
3
5
  import path from 'node:path'
4
6
 
@@ -622,6 +624,42 @@ function parseMapInvocations(fullText: string): MapInvocation[] {
622
624
  return maps
623
625
  }
624
626
 
627
+ const BARE_IDENTIFIER = /^[A-Za-z_$][\w$]*$/
628
+
629
+ function walkAccessor(node: Expression): { base: string; suffix: string } | null {
630
+ if (node.type === 'Identifier') {
631
+ return { base: node.name, suffix: '' }
632
+ }
633
+ if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') {
634
+ if (node.object.type === 'Super') return null
635
+ const inner = walkAccessor(node.object)
636
+ if (!inner) return null
637
+ const { property, computed } = node
638
+ let part: string
639
+ if (!computed && property.type === 'Identifier') {
640
+ part = `.${property.name}`
641
+ } else if (computed && property.type === 'NumericLiteral') {
642
+ part = `[${property.value}]`
643
+ } else {
644
+ return null
645
+ }
646
+ return { base: inner.base, suffix: inner.suffix + part }
647
+ }
648
+ return null
649
+ }
650
+
651
+ function parseAccessorChain(exprText: string): { base: string; suffix: string } | null {
652
+ if (BARE_IDENTIFIER.test(exprText)) return { base: exprText, suffix: '' }
653
+ try {
654
+ const file = parseBabel(exprText, { sourceType: 'module', plugins: ['typescript'] })
655
+ const stmt = file.program.body[0]
656
+ if (!stmt || stmt.type !== 'ExpressionStatement') return null
657
+ return walkAccessor(stmt.expression)
658
+ } catch {
659
+ return null
660
+ }
661
+ }
662
+
625
663
  /**
626
664
  * Resolve a `.map()` callback parameter back to the source array path.
627
665
  *
@@ -632,6 +670,8 @@ function parseMapInvocations(fullText: string): MapInvocation[] {
632
670
  * → `{ arrayPath: "categories[*].images", leafSuffix: "" }`
633
671
  * `links.map(({ label, href }) => …)` looking for `label`
634
672
  * → `{ arrayPath: "links", leafSuffix: ".label" }`
673
+ * `services.map((service) => …)` looking for `service.image`
674
+ * → `{ arrayPath: "services", leafSuffix: ".image" }`
635
675
  *
636
676
  * Returns null when the name doesn't appear as a parameter or destructured binding.
637
677
  */
@@ -639,13 +679,16 @@ export function resolveMapChain(exprTexts: string[], paramName: string): Resolve
639
679
  const maps = parseMapInvocations(exprTexts.join(''))
640
680
  if (maps.length === 0) return null
641
681
 
642
- // Prefer simple-param match (most common); fall back to destructured.
643
- const directMap = maps.find((m) => m.param === paramName)
644
- ?? maps.find((m) => m.destructured.includes(paramName))
682
+ const access = parseAccessorChain(paramName)
683
+ const baseName = access?.base ?? paramName
684
+ const memberSuffix = access?.suffix ?? ''
685
+
686
+ const directMap = maps.find((m) => m.param === baseName)
687
+ ?? maps.find((m) => m.destructured.includes(baseName))
645
688
  if (!directMap) return null
646
689
 
647
- const isDestructured = directMap.param !== paramName
648
- const leafSuffix = isDestructured ? `.${paramName}` : ''
690
+ const isDestructured = directMap.param !== baseName
691
+ const leafSuffix = (isDestructured ? `.${baseName}` : '') + memberSuffix
649
692
 
650
693
  // Resolve the array expression by substituting outer .map() params (chained / nested loops).
651
694
  let arrayPath = directMap.arrayExpr