@setzkasten-cms/astro-admin 1.4.6 → 1.5.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.
Files changed (166) hide show
  1. package/dist/api-routes/_auth-guard.d.ts +27 -3
  2. package/dist/api-routes/_auth-guard.js +5 -2
  3. package/dist/api-routes/_dev-session-secret.d.ts +8 -0
  4. package/dist/api-routes/_dev-session-secret.js +8 -0
  5. package/dist/api-routes/_github-token.js +1 -1
  6. package/dist/api-routes/_role-resolver.js +6 -3
  7. package/dist/api-routes/_session-secret.d.ts +19 -0
  8. package/dist/api-routes/_session-secret.js +7 -0
  9. package/dist/api-routes/_session-signing.d.ts +45 -0
  10. package/dist/api-routes/_session-signing.js +8 -0
  11. package/dist/api-routes/_webhook-dispatcher.js +4 -4
  12. package/dist/api-routes/asset-proxy.js +1 -1
  13. package/dist/api-routes/auth-callback.js +12 -5
  14. package/dist/api-routes/auth-logout.d.ts +4 -4
  15. package/dist/api-routes/auth-logout.js +8 -2
  16. package/dist/api-routes/auth-session.d.ts +6 -0
  17. package/dist/api-routes/auth-session.js +19 -19
  18. package/dist/api-routes/auth-setzkasten-login.js +14 -7
  19. package/dist/api-routes/catalog-add.js +59 -17
  20. package/dist/api-routes/catalog-export.js +14 -4
  21. package/dist/api-routes/config.d.ts +10 -3
  22. package/dist/api-routes/config.js +26 -4
  23. package/dist/api-routes/deploy-hook.js +8 -8
  24. package/dist/api-routes/editors.d.ts +1 -1
  25. package/dist/api-routes/editors.js +5 -2
  26. package/dist/api-routes/github-proxy.js +30 -8
  27. package/dist/api-routes/global-config.js +6 -3
  28. package/dist/api-routes/history-rollback.js +31 -14
  29. package/dist/api-routes/history-version.js +8 -6
  30. package/dist/api-routes/history.js +5 -2
  31. package/dist/api-routes/icons-local.js +1 -1
  32. package/dist/api-routes/init-add-section.js +113 -47
  33. package/dist/api-routes/init-apply.js +56 -42
  34. package/dist/api-routes/init-migrate.js +43 -36
  35. package/dist/api-routes/init-scan-page.d.ts +1 -1
  36. package/dist/api-routes/init-scan-page.js +59 -13
  37. package/dist/api-routes/init-scan.js +22 -7
  38. package/dist/api-routes/migrate-to-multi.js +5 -2
  39. package/dist/api-routes/pages.js +15 -4
  40. package/dist/api-routes/section-add.js +68 -16
  41. package/dist/api-routes/section-commit-pending.js +70 -22
  42. package/dist/api-routes/section-delete.js +49 -14
  43. package/dist/api-routes/section-duplicate.js +65 -16
  44. package/dist/api-routes/section-prepare-copy.js +15 -2
  45. package/dist/api-routes/section-prepare.js +25 -4
  46. package/dist/api-routes/setup-github-app-bounce.js +15 -1
  47. package/dist/api-routes/setup-github-app-branches.js +9 -6
  48. package/dist/api-routes/setup-github-app-callback.js +24 -1
  49. package/dist/api-routes/setup-github-app-credentials.d.ts +27 -0
  50. package/dist/api-routes/setup-github-app-credentials.js +43 -0
  51. package/dist/api-routes/setup-github-app-installed.js +22 -1
  52. package/dist/api-routes/setup-github-app-repos.js +5 -2
  53. package/dist/api-routes/setup-github-app.d.ts +4 -0
  54. package/dist/api-routes/setup-github-app.js +19 -2
  55. package/dist/api-routes/updater-register.js +7 -1
  56. package/dist/api-routes/webhooks-status.js +5 -2
  57. package/dist/api-routes/webhooks-test.js +9 -8
  58. package/dist/api-routes/webhooks.js +12 -14
  59. package/dist/api-routes/websites-add.js +5 -2
  60. package/dist/api-routes/websites-remove.js +5 -2
  61. package/dist/{chunk-ZQDGGWJP.js → chunk-5KMGSFCZ.js} +2 -2
  62. package/dist/{chunk-Q3N336KR.js → chunk-CDXCYYQR.js} +29 -24
  63. package/dist/{chunk-NKDATSPA.js → chunk-DP6RTINQ.js} +1 -1
  64. package/dist/chunk-KENFINT4.js +76 -0
  65. package/dist/chunk-ONP6BRZO.js +47 -0
  66. package/dist/{chunk-INIWFKQ3.js → chunk-Q5HV47DW.js} +33 -19
  67. package/dist/chunk-QVCW6EF3.js +26 -0
  68. package/dist/{chunk-TD76R3A6.js → chunk-UHI6323G.js} +293 -174
  69. package/dist/{chunk-AM4DZXXM.js → chunk-UJAFZEX2.js} +76 -9
  70. package/package.json +12 -6
  71. package/src/api-routes/__tests__/_session-signing.test.ts +114 -0
  72. package/src/api-routes/__tests__/_session-test-helper.ts +27 -0
  73. package/src/api-routes/__tests__/add-section-helpers.test.ts +59 -25
  74. package/src/api-routes/__tests__/auth-guard.test.ts +46 -7
  75. package/src/api-routes/__tests__/catalog-api.test.ts +14 -6
  76. package/src/api-routes/__tests__/commit-trailers.test.ts +5 -5
  77. package/src/api-routes/__tests__/deferred-operations.test.ts +9 -12
  78. package/src/api-routes/__tests__/deploy-hook.test.ts +3 -8
  79. package/src/api-routes/__tests__/feature-gate.test.ts +1 -1
  80. package/src/api-routes/__tests__/github-cache.test.ts +1 -1
  81. package/src/api-routes/__tests__/github-token.test.ts +1 -1
  82. package/src/api-routes/__tests__/global-config-theme.test.ts +4 -4
  83. package/src/api-routes/__tests__/history-rollback.test.ts +6 -3
  84. package/src/api-routes/__tests__/history.test.ts +9 -6
  85. package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +11 -3
  86. package/src/api-routes/__tests__/migrate-to-multi.test.ts +5 -1
  87. package/src/api-routes/__tests__/pages-meta-store.test.ts +10 -5
  88. package/src/api-routes/__tests__/pages.test.ts +7 -2
  89. package/src/api-routes/__tests__/patch-page-file.test.ts +71 -19
  90. package/src/api-routes/__tests__/route-registry.test.ts +11 -18
  91. package/src/api-routes/__tests__/scan-page-helpers.test.ts +13 -10
  92. package/src/api-routes/__tests__/section-management.test.ts +28 -28
  93. package/src/api-routes/__tests__/setup-github-app-callback.test.ts +58 -16
  94. package/src/api-routes/__tests__/setup-github-app-repos.test.ts +4 -5
  95. package/src/api-routes/__tests__/setup-github-app.test.ts +27 -7
  96. package/src/api-routes/__tests__/storage-config-for-request.test.ts +83 -0
  97. package/src/api-routes/__tests__/updater-register.test.ts +230 -0
  98. package/src/api-routes/__tests__/webhook-signing.test.ts +1 -1
  99. package/src/api-routes/__tests__/webhooks.test.ts +19 -7
  100. package/src/api-routes/__tests__/websites-add.test.ts +2 -1
  101. package/src/api-routes/__tests__/websites-remove.test.ts +2 -1
  102. package/src/api-routes/_auth-guard.ts +47 -15
  103. package/src/api-routes/_commit-trailers.ts +3 -2
  104. package/src/api-routes/_dev-session-secret.ts +79 -0
  105. package/src/api-routes/_github-token.ts +1 -1
  106. package/src/api-routes/_pages-meta-store.ts +2 -2
  107. package/src/api-routes/_role-resolver.ts +7 -5
  108. package/src/api-routes/_session-secret.ts +46 -0
  109. package/src/api-routes/_session-signing.ts +135 -0
  110. package/src/api-routes/_vercel-origin.ts +2 -6
  111. package/src/api-routes/_webhook-dispatcher.ts +12 -16
  112. package/src/api-routes/_website-resolver.ts +3 -10
  113. package/src/api-routes/auth-callback.ts +9 -5
  114. package/src/api-routes/auth-login.ts +5 -3
  115. package/src/api-routes/auth-logout.ts +18 -1
  116. package/src/api-routes/auth-session.ts +13 -21
  117. package/src/api-routes/auth-setzkasten-login.ts +12 -9
  118. package/src/api-routes/catalog-add.ts +89 -31
  119. package/src/api-routes/catalog-export.ts +30 -10
  120. package/src/api-routes/config.ts +39 -6
  121. package/src/api-routes/deploy-hook.ts +13 -11
  122. package/src/api-routes/editors.ts +33 -22
  123. package/src/api-routes/github-proxy.ts +25 -11
  124. package/src/api-routes/global-config.ts +103 -18
  125. package/src/api-routes/history-rollback.ts +41 -14
  126. package/src/api-routes/history-version.ts +5 -6
  127. package/src/api-routes/history.ts +3 -3
  128. package/src/api-routes/icons-local.ts +2 -2
  129. package/src/api-routes/init-add-section.ts +174 -79
  130. package/src/api-routes/init-apply.ts +71 -56
  131. package/src/api-routes/init-migrate.ts +54 -48
  132. package/src/api-routes/init-scan-page.ts +77 -30
  133. package/src/api-routes/init-scan.ts +19 -11
  134. package/src/api-routes/pages.ts +16 -11
  135. package/src/api-routes/section-add.ts +98 -27
  136. package/src/api-routes/section-commit-pending.ts +87 -34
  137. package/src/api-routes/section-delete.ts +76 -27
  138. package/src/api-routes/section-duplicate.ts +95 -28
  139. package/src/api-routes/section-management.ts +3 -7
  140. package/src/api-routes/section-prepare-copy.ts +29 -8
  141. package/src/api-routes/section-prepare.ts +38 -10
  142. package/src/api-routes/setup-github-app-bounce.ts +7 -1
  143. package/src/api-routes/setup-github-app-branches.ts +6 -7
  144. package/src/api-routes/setup-github-app-callback.ts +18 -1
  145. package/src/api-routes/setup-github-app-credentials.ts +55 -0
  146. package/src/api-routes/setup-github-app-installed.ts +12 -1
  147. package/src/api-routes/setup-github-app-repos.ts +2 -3
  148. package/src/api-routes/setup-github-app.ts +14 -5
  149. package/src/api-routes/updater-check.ts +6 -4
  150. package/src/api-routes/updater-register.ts +34 -20
  151. package/src/api-routes/updater-transfer.ts +8 -6
  152. package/src/api-routes/updater-unbind.ts +14 -10
  153. package/src/api-routes/webhooks-test.ts +9 -11
  154. package/src/api-routes/webhooks.ts +15 -19
  155. package/src/init/__tests__/page-level.test.ts +279 -105
  156. package/src/init/__tests__/page-list-coverage.test.ts +70 -70
  157. package/src/init/__tests__/patcher-child-component.test.ts +12 -3
  158. package/src/init/__tests__/patcher-edge-cases.test.ts +47 -23
  159. package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +16 -6
  160. package/src/init/__tests__/patcher-page-mode.test.ts +30 -20
  161. package/src/init/__tests__/section-pipeline.test.ts +53 -19
  162. package/src/init/astro-config-patcher.ts +4 -18
  163. package/src/init/astro-detector.ts +2 -7
  164. package/src/init/astro-section-analyzer-v2.ts +475 -193
  165. package/src/init/field-label-enricher.ts +6 -6
  166. package/src/init/template-patcher-v2.ts +218 -97
@@ -38,7 +38,11 @@ const normalizeWs = (s: string) => s.replace(/\s+/g, ' ').trim()
38
38
  * Find text in a string where the needle may have collapsed whitespace
39
39
  * but the haystack preserves original whitespace (incl. newlines).
40
40
  */
41
- function findNormalizedText(haystack: string, needle: string, startFrom: number): { start: number; end: number } | null {
41
+ function findNormalizedText(
42
+ haystack: string,
43
+ needle: string,
44
+ startFrom: number,
45
+ ): { start: number; end: number } | null {
42
46
  // Try exact match first
43
47
  const exactIdx = haystack.indexOf(needle, startFrom)
44
48
  if (exactIdx !== -1) return { start: exactIdx, end: exactIdx + needle.length }
@@ -49,7 +53,8 @@ function findNormalizedText(haystack: string, needle: string, startFrom: number)
49
53
  const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '\\s+')
50
54
  const regex = new RegExp(escaped)
51
55
  const match = regex.exec(haystack.slice(startFrom))
52
- if (match) return { start: startFrom + match.index, end: startFrom + match.index + match[0].length }
56
+ if (match)
57
+ return { start: startFrom + match.index, end: startFrom + match.index + match[0].length }
53
58
 
54
59
  return null
55
60
  }
@@ -128,7 +133,10 @@ export async function patchTemplateForFields(
128
133
  // Adjust positions in-place on the original groups so mutations
129
134
  // (like _class fields from makeDynamicClasses) are visible to the caller.
130
135
  for (const g of repeatedGroups) {
131
- for (const inst of g.instances) { inst.start += shift; inst.end += shift }
136
+ for (const inst of g.instances) {
137
+ inst.start += shift
138
+ inst.end += shift
139
+ }
132
140
  for (const f of g.fields) {
133
141
  for (let i = 0; i < f.positions.length; i++) {
134
142
  const p = f.positions[i]
@@ -143,7 +151,13 @@ export async function patchTemplateForFields(
143
151
  }
144
152
 
145
153
  // Patch expressions first (old vars still present → patcher detects them)
146
- const patched = await patchTemplateForFields(modifiedSource, sectionKey, fields, repeatedGroups, options)
154
+ const patched = await patchTemplateForFields(
155
+ modifiedSource,
156
+ sectionKey,
157
+ fields,
158
+ repeatedGroups,
159
+ options,
160
+ )
147
161
 
148
162
  // Now remove old variable declarations from the patched result
149
163
  return removeOldVarDeclarations(patched, fields, repeatedGroups, varName)
@@ -183,8 +197,8 @@ export async function patchTemplateForFields(
183
197
  // Collect elements with mixed content (text + inline child elements)
184
198
  // These are rich text fields where the whole element is one CMS field.
185
199
  if (isElement(node) && node.children && node.children.length > 1) {
186
- const hasText = node.children.some(c => c.type === 'text' && c.value?.trim())
187
- const hasChild = node.children.some(c => isElement(c) || c.type === 'expression')
200
+ const hasText = node.children.some((c) => c.type === 'text' && c.value?.trim())
201
+ const hasChild = node.children.some((c) => isElement(c) || c.type === 'expression')
188
202
  if (hasText && hasChild) {
189
203
  const fullText = normalizeWs(getElementTextContent(node))
190
204
  if (fullText) mixedElements.push({ node, normalizedText: fullText })
@@ -249,13 +263,18 @@ export async function patchTemplateForFields(
249
263
  // built the defaultValue array before patching. Without this sync, the
250
264
  // content JSON (built from field.defaultValue) would be missing _classN values.
251
265
  for (const g of repeatedGroups) {
252
- const topField = fields.find(f => f.key === g.fieldKey)
266
+ const topField = fields.find((f) => f.key === g.fieldKey)
253
267
  if (!topField || !Array.isArray(topField.defaultValue)) continue
254
268
  const items = topField.defaultValue as Array<Record<string, unknown>>
255
269
  for (const innerField of g.fields) {
256
270
  for (let ii = 0; ii < items.length && ii < innerField.defaultValues.length; ii++) {
257
271
  const val = innerField.defaultValues[ii]
258
- if (val != null && items[ii] && typeof items[ii] === 'object' && !(innerField.key in items[ii]!)) {
272
+ if (
273
+ val != null &&
274
+ items[ii] &&
275
+ typeof items[ii] === 'object' &&
276
+ !(innerField.key in items[ii]!)
277
+ ) {
259
278
  items[ii]![innerField.key] = val
260
279
  }
261
280
  }
@@ -332,7 +351,7 @@ export async function patchTemplateForFields(
332
351
  // Treat the whole element as a rich text field with set:html for CMS rendering.
333
352
  if (edits.length === editsBefore) {
334
353
  const dv = field.defaultValue as string
335
- const mixedMatch = mixedElements.find(m => m.normalizedText === normalizeWs(dv))
354
+ const mixedMatch = mixedElements.find((m) => m.normalizedText === normalizeWs(dv))
336
355
  if (mixedMatch) {
337
356
  patchMixedContentField(source, sectionKey, field, varName, mixedMatch.node, edits)
338
357
  }
@@ -372,7 +391,10 @@ export function convertToSetHtml(source: string): string {
372
391
  const ch = result[i]!
373
392
  if (ch === '{') braceDepth++
374
393
  else if (ch === '}') braceDepth--
375
- else if (ch === '>' && braceDepth === 0) { tagEnd = i; break }
394
+ else if (ch === '>' && braceDepth === 0) {
395
+ tagEnd = i
396
+ break
397
+ }
376
398
  }
377
399
  if (tagEnd === -1) continue
378
400
 
@@ -474,7 +496,10 @@ function convertAstPositions(node: AstNode, b2c: (offset: number) => number): vo
474
496
  * AST expression positions can extend beyond the actual braces, so we search
475
497
  * the source directly using the child text content as anchor.
476
498
  */
477
- function findExpressionBounds(source: string, node: AstNode): { start: number; end: number } | null {
499
+ function findExpressionBounds(
500
+ source: string,
501
+ node: AstNode,
502
+ ): { start: number; end: number } | null {
478
503
  const approxStart = node.position?.start?.offset
479
504
  if (approxStart == null) return null
480
505
 
@@ -510,7 +535,10 @@ function findExpressionBounds(source: string, node: AstNode): { start: number; e
510
535
  if (ch === inStr && source[i - 1] !== '\\') inStr = null
511
536
  continue
512
537
  }
513
- if (ch === "'" || ch === '"' || ch === '`') { inStr = ch; continue }
538
+ if (ch === "'" || ch === '"' || ch === '`') {
539
+ inStr = ch
540
+ continue
541
+ }
514
542
  if (ch === '{') depth++
515
543
  if (ch === '}') {
516
544
  depth--
@@ -560,7 +588,10 @@ function findTextBounds(source: string, node: AstNode): { start: number; end: nu
560
588
  function getElementTextContent(node: AstNode): string {
561
589
  if (node.type === 'text') return node.value ?? ''
562
590
  if (node.type === 'expression') {
563
- return (node.children ?? []).filter(c => c.type === 'text').map(c => c.value ?? '').join('')
591
+ return (node.children ?? [])
592
+ .filter((c) => c.type === 'text')
593
+ .map((c) => c.value ?? '')
594
+ .join('')
564
595
  }
565
596
  return (node.children ?? []).map(getElementTextContent).join('')
566
597
  }
@@ -599,7 +630,7 @@ function patchMixedContentField(
599
630
  const bindingKey = `${sectionKey}.${field.key}`
600
631
 
601
632
  // Skip if already has data-sk-field
602
- if (element.attributes?.some(a => a.name === 'data-sk-field')) return
633
+ if (element.attributes?.some((a) => a.name === 'data-sk-field')) return
603
634
 
604
635
  // Find the element's opening tag in source
605
636
  const startOffset = element.position?.start?.offset
@@ -687,14 +718,15 @@ function patchIconOrImageField(
687
718
  if (targetParent) return // already found
688
719
  if (!parent || !isElement(parent)) return
689
720
  // Skip if parent already has data-sk-field
690
- if (parent.attributes?.some(a => a.name === 'data-sk-field')) return
721
+ if (parent.attributes?.some((a) => a.name === 'data-sk-field')) return
691
722
 
692
723
  if (field.type === 'icon') {
693
724
  // Match component named *Icon* or element with icon/name attr matching default
694
725
  const isIconComp = isElement(node) && /icon/i.test(node.name ?? '')
695
726
  const hasIconAttr = node.attributes?.some(
696
- a => (a.name === 'icon' || a.name === 'name' || a.name === 'data-icon') &&
697
- (a.value === defaultStr || a.value.includes(field.key))
727
+ (a) =>
728
+ (a.name === 'icon' || a.name === 'name' || a.name === 'data-icon') &&
729
+ (a.value === defaultStr || a.value.includes(field.key)),
698
730
  )
699
731
  if (isIconComp || hasIconAttr) {
700
732
  targetParent = parent
@@ -704,7 +736,7 @@ function patchIconOrImageField(
704
736
  const tag = node.name ?? ''
705
737
  const isImgEl = /^(img|Image|picture)$/.test(tag)
706
738
  const hasSrcAttr = node.attributes?.some(
707
- a => a.name === 'src' && (a.value === defaultStr || a.value.includes(field.key))
739
+ (a) => a.name === 'src' && (a.value === defaultStr || a.value.includes(field.key)),
708
740
  )
709
741
  if (isImgEl || hasSrcAttr) {
710
742
  targetParent = parent
@@ -910,15 +942,20 @@ function patchArrayField(
910
942
  // Match by looking for the field's first item value inside each expression's source.
911
943
  let mapExpr = mapExpressions[0]!
912
944
 
913
- if (mapExpressions.length > 1 && Array.isArray(field.defaultValue) && field.defaultValue.length > 0) {
945
+ if (
946
+ mapExpressions.length > 1 &&
947
+ Array.isArray(field.defaultValue) &&
948
+ field.defaultValue.length > 0
949
+ ) {
914
950
  const firstItem = field.defaultValue[0]
915
- const searchStr = typeof firstItem === 'string'
916
- ? firstItem.slice(0, 30)
917
- : typeof firstItem === 'object' && firstItem !== null
918
- ? Object.values(firstItem as Record<string, unknown>)
919
- .find((v): v is string => typeof v === 'string' && v.length >= 3)
920
- ?.slice(0, 30) ?? ''
921
- : ''
951
+ const searchStr =
952
+ typeof firstItem === 'string'
953
+ ? firstItem.slice(0, 30)
954
+ : typeof firstItem === 'object' && firstItem !== null
955
+ ? (Object.values(firstItem as Record<string, unknown>)
956
+ .find((v): v is string => typeof v === 'string' && v.length >= 3)
957
+ ?.slice(0, 30) ?? '')
958
+ : ''
922
959
 
923
960
  if (searchStr.length >= 3) {
924
961
  for (const expr of mapExpressions) {
@@ -983,7 +1020,7 @@ function patchArrayField(
983
1020
  })
984
1021
  } else {
985
1022
  // Try variable reference: varName.map( → (cmsVar?.field ?? []).map(
986
- const varMapRegex = new RegExp(`(\\w+)\\.map\\s*\\(`)
1023
+ const varMapRegex = /(\w+)\.map\s*\(/
987
1024
  const varMapMatch = exprSource.match(varMapRegex)
988
1025
  if (varMapMatch) {
989
1026
  const varRef = varMapMatch[1]!
@@ -1042,15 +1079,19 @@ function patchStaticListField(
1042
1079
  walkAst(node, (n) => {
1043
1080
  if (hasMap) return
1044
1081
  if (n.type === 'expression') {
1045
- const code = (n.children ?? []).map(c => c.value ?? '').join('')
1082
+ const code = (n.children ?? []).map((c) => c.value ?? '').join('')
1046
1083
  if (/\.map\s*\(/.test(code)) hasMap = true
1047
1084
  }
1048
1085
  })
1049
1086
  if (hasMap) return
1050
1087
  // Compare <li> text content against field items.
1051
1088
  // items[i] may contain HTML (if formatting: true) — strip tags for comparison.
1052
- const stripHtml = (s: string) => s.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim()
1053
- const liChildren = (node.children ?? []).filter(c => c.type === 'element' && c.name === 'li')
1089
+ const stripHtml = (s: string) =>
1090
+ s
1091
+ .replace(/<[^>]+>/g, '')
1092
+ .replace(/\s+/g, ' ')
1093
+ .trim()
1094
+ const liChildren = (node.children ?? []).filter((c) => c.type === 'element' && c.name === 'li')
1054
1095
  if (liChildren.length !== items.length) return
1055
1096
  const allMatch = liChildren.every((li, i) => {
1056
1097
  const text = getElementTextContent(li).replace(/\s+/g, ' ').trim()
@@ -1063,7 +1104,7 @@ function patchStaticListField(
1063
1104
 
1064
1105
  const ulNode = matchedUl as AstNode
1065
1106
  const liChildren = (ulNode.children ?? []).filter(
1066
- c => c.type === 'element' && c.name === 'li',
1107
+ (c) => c.type === 'element' && c.name === 'li',
1067
1108
  ) as AstNode[]
1068
1109
  if (liChildren.length === 0) return
1069
1110
 
@@ -1101,15 +1142,16 @@ function patchStaticListField(
1101
1142
 
1102
1143
  // Build the fallback array literal.
1103
1144
  // HTML items (formatting: true) are wrapped in backtick strings to avoid escaping issues.
1104
- const hasHtmlItems = items.some(s => /<[a-z]/.test(s))
1145
+ const hasHtmlItems = items.some((s) => /<[a-z]/.test(s))
1105
1146
  const fallbackItems = hasHtmlItems
1106
- ? items.map(s => `\`${s.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\``).join(', ')
1107
- : items.map(s => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`).join(', ')
1147
+ ? items.map((s) => `\`${s.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\``).join(', ')
1148
+ : items.map((s) => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`).join(', ')
1108
1149
 
1109
1150
  // Determine indentation of the first <li> for formatting
1110
1151
  const textBeforeFirstLi = source.slice(0, rangeStart)
1111
1152
  const lastNewline = textBeforeFirstLi.lastIndexOf('\n')
1112
- const indent = lastNewline >= 0 ? textBeforeFirstLi.slice(lastNewline + 1).match(/^[ \t]*/)?.[0] ?? '' : ''
1153
+ const indent =
1154
+ lastNewline >= 0 ? (textBeforeFirstLi.slice(lastNewline + 1).match(/^[ \t]*/)?.[0] ?? '') : ''
1113
1155
 
1114
1156
  const replacement =
1115
1157
  `{(${varName}?.${field.key} ?? [${fallbackItems}]).map((item, _i) => (\n` +
@@ -1351,7 +1393,7 @@ function collectDynamicClassEdits(
1351
1393
  function sharedClassCount(instValue: string): number {
1352
1394
  const instParts = new Set(instValue.split(/\s+/).filter(Boolean))
1353
1395
  if (tmplParts.size === 0 && instParts.size === 0) return 1 // both empty = match
1354
- return [...tmplParts].filter(p => instParts.has(p)).length
1396
+ return [...tmplParts].filter((p) => instParts.has(p)).length
1355
1397
  }
1356
1398
 
1357
1399
  // 1. Exact path — only accept if at least 1 CSS class is shared
@@ -1365,7 +1407,11 @@ function collectDynamicClassEdits(
1365
1407
  }
1366
1408
 
1367
1409
  // 2. Same tag-path (without indices) — pick the one with most shared classes
1368
- const stripIdx = (p: string) => p.split('/').map(s => s.replace(/:\d+$/, '')).join('/')
1410
+ const stripIdx = (p: string) =>
1411
+ p
1412
+ .split('/')
1413
+ .map((s) => s.replace(/:\d+$/, ''))
1414
+ .join('/')
1369
1415
  const tmplTagPath = stripIdx(tmplAttr.path)
1370
1416
  let bestIdx = -1
1371
1417
  let bestShared = 0
@@ -1373,7 +1419,10 @@ function collectDynamicClassEdits(
1373
1419
  if (claimed.has(i)) continue
1374
1420
  if (stripIdx(instAttrs[i]!.path) !== tmplTagPath) continue
1375
1421
  const shared = sharedClassCount(instAttrs[i]!.value)
1376
- if (shared > bestShared) { bestShared = shared; bestIdx = i }
1422
+ if (shared > bestShared) {
1423
+ bestShared = shared
1424
+ bestIdx = i
1425
+ }
1377
1426
  }
1378
1427
  if (bestIdx >= 0 && bestShared > 0) {
1379
1428
  claimed.add(bestIdx)
@@ -1403,7 +1452,7 @@ function collectDynamicClassEdits(
1403
1452
  // adjustments can put the class attr offset slightly after the text offset.
1404
1453
  const fieldTag = field.tag
1405
1454
  let bestAttrIdx = -1
1406
- let bestDist = Infinity
1455
+ let bestDist = Number.POSITIVE_INFINITY
1407
1456
  for (let ai = 0; ai < tmplAttrs.length; ai++) {
1408
1457
  // Extract leaf tag from path: "div:0/div:0/span:1" → "span"
1409
1458
  const pathParts = tmplAttrs[ai]!.path.split('/')
@@ -1555,7 +1604,7 @@ function patchRepeatedComponentInstances(
1555
1604
  // data-sk-field on component calls is not forwarded to the DOM by Astro.
1556
1605
  propLines.push(`fieldPrefix={\`${sectionKey}.${field.key}.\${_i}\`}`)
1557
1606
 
1558
- const propsStr = propLines.map(p => ` ${p}`).join('\n')
1607
+ const propsStr = propLines.map((p) => ` ${p}`).join('\n')
1559
1608
  const mapExpr =
1560
1609
  `{(${varName}?.${field.key} ?? ${defaultJson}).map((item, _i) => (\n` +
1561
1610
  ` <${compName}\n${propsStr}\n />\n` +
@@ -1592,14 +1641,30 @@ function findComponentTagEnd(source: string, tagStart: number, compName: string)
1592
1641
  const ch = source[i]!
1593
1642
  if (inQuote) {
1594
1643
  if (ch === inQuote && source[i - 1] !== '\\') inQuote = null
1595
- i++; continue
1644
+ i++
1645
+ continue
1646
+ }
1647
+ if (ch === '{') {
1648
+ inExpr++
1649
+ i++
1650
+ continue
1651
+ }
1652
+ if (ch === '}' && inExpr > 0) {
1653
+ inExpr--
1654
+ i++
1655
+ continue
1656
+ }
1657
+ if (inExpr > 0) {
1658
+ i++
1659
+ continue
1660
+ }
1661
+ if (ch === '"' || ch === "'") {
1662
+ inQuote = ch
1663
+ i++
1664
+ continue
1596
1665
  }
1597
- if (ch === '{') { inExpr++; i++; continue }
1598
- if (ch === '}' && inExpr > 0) { inExpr--; i++; continue }
1599
- if (inExpr > 0) { i++; continue }
1600
- if (ch === '"' || ch === "'") { inQuote = ch; i++; continue }
1601
1666
  if (ch === '/' && source[i + 1] === '>') return i + 2 // self-closing />
1602
- if (ch === '>' ) {
1667
+ if (ch === '>') {
1603
1668
  // Check for explicit closing tag </CompName>
1604
1669
  const closing = `</${compName}>`
1605
1670
  const closingIdx = source.indexOf(closing, i + 1)
@@ -1738,7 +1803,8 @@ function patchRepeatedGroups(
1738
1803
  if (typeof defaultVal === 'string' && defaultVal.length >= 1) {
1739
1804
  const found = findNormalizedText(elementSrc, defaultVal, 0)
1740
1805
  if (found) {
1741
- elementSrc = elementSrc.slice(0, found.start) + `{item.${field.key}}` + elementSrc.slice(found.end)
1806
+ elementSrc =
1807
+ elementSrc.slice(0, found.start) + `{item.${field.key}}` + elementSrc.slice(found.end)
1742
1808
  }
1743
1809
  }
1744
1810
 
@@ -1759,7 +1825,10 @@ function patchRepeatedGroups(
1759
1825
  // Apply inner edits in reverse order
1760
1826
  innerEdits.sort((a, b) => b.offset - a.offset)
1761
1827
  for (const edit of innerEdits) {
1762
- templateSrc = templateSrc.slice(0, edit.offset) + edit.insert + templateSrc.slice(edit.offset + edit.deleteCount)
1828
+ templateSrc =
1829
+ templateSrc.slice(0, edit.offset) +
1830
+ edit.insert +
1831
+ templateSrc.slice(edit.offset + edit.deleteCount)
1763
1832
  }
1764
1833
 
1765
1834
  // Add data-sk-field to the template element's opening tag (container binding)
@@ -1788,9 +1857,13 @@ function patchRepeatedGroups(
1788
1857
  const inst = instances[i]!
1789
1858
  // Also remove surrounding whitespace/newlines
1790
1859
  let deleteStart = inst.start
1791
- let deleteEnd = inst.end
1860
+ const deleteEnd = inst.end
1792
1861
  // Extend backwards to eat preceding whitespace
1793
- while (deleteStart > 0 && /\s/.test(source[deleteStart - 1]!) && source[deleteStart - 1] !== '\n') {
1862
+ while (
1863
+ deleteStart > 0 &&
1864
+ /\s/.test(source[deleteStart - 1]!) &&
1865
+ source[deleteStart - 1] !== '\n'
1866
+ ) {
1794
1867
  deleteStart--
1795
1868
  }
1796
1869
  // Eat the preceding newline too
@@ -1837,11 +1910,23 @@ function addInnerFieldBindings(
1837
1910
  let inE = 0
1838
1911
  for (let i = tagStart; i < idx; i++) {
1839
1912
  const ch = templateSrc[i]!
1840
- if (inQ) { if (ch === inQ && templateSrc[i - 1] !== '\\') inQ = null; continue }
1841
- if (ch === '{') { inE++; continue }
1842
- if (ch === '}' && inE > 0) { inE--; continue }
1913
+ if (inQ) {
1914
+ if (ch === inQ && templateSrc[i - 1] !== '\\') inQ = null
1915
+ continue
1916
+ }
1917
+ if (ch === '{') {
1918
+ inE++
1919
+ continue
1920
+ }
1921
+ if (ch === '}' && inE > 0) {
1922
+ inE--
1923
+ continue
1924
+ }
1843
1925
  if (inE > 0) continue
1844
- if (ch === '"' || ch === "'") { inQ = ch; continue }
1926
+ if (ch === '"' || ch === "'") {
1927
+ inQ = ch
1928
+ continue
1929
+ }
1845
1930
  if (ch === '>') tagEnd = i
1846
1931
  }
1847
1932
  if (tagEnd === -1) continue
@@ -1945,7 +2030,9 @@ function extractVarName(frontmatter: string): string | null {
1945
2030
  }
1946
2031
 
1947
2032
  /** Type guard for element-like nodes (element, component, custom-element). */
1948
- function isElement(node: AstNode): node is AstNode & { attributes: AstAttr[]; children: AstNode[] } {
2033
+ function isElement(
2034
+ node: AstNode,
2035
+ ): node is AstNode & { attributes: AstAttr[]; children: AstNode[] } {
1949
2036
  return (
1950
2037
  (node.type === 'element' || node.type === 'component' || node.type === 'custom-element') &&
1951
2038
  Array.isArray(node.attributes)
@@ -1953,7 +2040,11 @@ function isElement(node: AstNode): node is AstNode & { attributes: AstAttr[]; ch
1953
2040
  }
1954
2041
 
1955
2042
  /** Recursive AST walk with parent tracking. */
1956
- function walkAst(node: AstNode, callback: (node: AstNode, parent: AstNode | null) => void, parent: AstNode | null = null): void {
2043
+ function walkAst(
2044
+ node: AstNode,
2045
+ callback: (node: AstNode, parent: AstNode | null) => void,
2046
+ parent: AstNode | null = null,
2047
+ ): void {
1957
2048
  callback(node, parent)
1958
2049
  if (node.children) {
1959
2050
  for (const child of node.children) {
@@ -1978,7 +2069,12 @@ function findFirstChildElement(node: AstNode): (AstNode & { attributes: AstAttr[
1978
2069
  * Inserts before the closing `>` of the opening tag.
1979
2070
  * Uses the element's tag name to locate it reliably in the source.
1980
2071
  */
1981
- function addAttributeToElement(source: string, element: AstNode, attrStr: string, edits: Edit[]): void {
2072
+ function addAttributeToElement(
2073
+ source: string,
2074
+ element: AstNode,
2075
+ attrStr: string,
2076
+ edits: Edit[],
2077
+ ): void {
1982
2078
  const approxStart = element.position?.start?.offset
1983
2079
  if (approxStart == null) return
1984
2080
 
@@ -2014,10 +2110,19 @@ function findOpeningTagEnd(source: string, startOffset: number): number {
2014
2110
  if (ch === inQuote && source[i - 1] !== '\\') inQuote = null
2015
2111
  continue
2016
2112
  }
2017
- if (ch === '{') { inExpr++; continue }
2018
- if (ch === '}' && inExpr > 0) { inExpr--; continue }
2113
+ if (ch === '{') {
2114
+ inExpr++
2115
+ continue
2116
+ }
2117
+ if (ch === '}' && inExpr > 0) {
2118
+ inExpr--
2119
+ continue
2120
+ }
2019
2121
  if (inExpr > 0) continue
2020
- if (ch === '"' || ch === "'") { inQuote = ch; continue }
2122
+ if (ch === '"' || ch === "'") {
2123
+ inQuote = ch
2124
+ continue
2125
+ }
2021
2126
  if (ch === '>') return i
2022
2127
  }
2023
2128
  return -1
@@ -2032,7 +2137,8 @@ function applyEdits(source: string, edits: Edit[]): string {
2032
2137
 
2033
2138
  let result = source
2034
2139
  for (const edit of sorted) {
2035
- result = result.slice(0, edit.offset) + edit.insert + result.slice(edit.offset + edit.deleteCount)
2140
+ result =
2141
+ result.slice(0, edit.offset) + edit.insert + result.slice(edit.offset + edit.deleteCount)
2036
2142
  }
2037
2143
  return result
2038
2144
  }
@@ -2042,7 +2148,12 @@ function applyEdits(source: string, edits: Edit[]): string {
2042
2148
  * Called AFTER expression patching so that variable references have already
2043
2149
  * been replaced with CMS expressions.
2044
2150
  */
2045
- function removeOldVarDeclarations(source: string, fields: PatchField[], repeatedGroups?: RepeatedGroup[], cmsVarName = 'skData'): string {
2151
+ function removeOldVarDeclarations(
2152
+ source: string,
2153
+ fields: PatchField[],
2154
+ repeatedGroups?: RepeatedGroup[],
2155
+ cmsVarName = 'skData',
2156
+ ): string {
2046
2157
  const fmStart = source.indexOf('---')
2047
2158
  if (fmStart === -1) return source
2048
2159
  const fmEnd = source.indexOf('---', fmStart + 3)
@@ -2113,7 +2224,10 @@ function removeOldVarDeclarations(source: string, fields: PatchField[], repeated
2113
2224
  if (ch === inStr && rhs[i - 1] !== '\\') inStr = null
2114
2225
  continue
2115
2226
  }
2116
- if (ch === "'" || ch === '"' || ch === '`') { inStr = ch; continue }
2227
+ if (ch === "'" || ch === '"' || ch === '`') {
2228
+ inStr = ch
2229
+ continue
2230
+ }
2117
2231
  if (ch === open) depth++
2118
2232
  if (ch === close) depth--
2119
2233
  }
@@ -2193,7 +2307,8 @@ export function patchChildComponentForFieldPrefix(
2193
2307
  innerFields: Array<{ key: string; type: string }>,
2194
2308
  ): string {
2195
2309
  // Idempotency guard — already patched
2196
- if (source.includes('fieldPrefix?: string') || source.includes("fieldPrefix?:string")) return source
2310
+ if (source.includes('fieldPrefix?: string') || source.includes('fieldPrefix?:string'))
2311
+ return source
2197
2312
 
2198
2313
  const fmStart = source.indexOf('---')
2199
2314
  const fmEnd = source.indexOf('---', fmStart + 3)
@@ -2207,36 +2322,33 @@ export function patchChildComponentForFieldPrefix(
2207
2322
  if (interfaceMatch) {
2208
2323
  const ifaceEnd = result.indexOf(interfaceMatch[0]) + interfaceMatch[0].length
2209
2324
  const lastPropLine = interfaceMatch[0].slice(0, -1).trimEnd() // remove trailing `}`
2210
- result =
2211
- result.slice(0, ifaceEnd - 1) +
2212
- '\n fieldPrefix?: string;\n}' +
2213
- result.slice(ifaceEnd)
2325
+ result = result.slice(0, ifaceEnd - 1) + '\n fieldPrefix?: string;\n}' + result.slice(ifaceEnd)
2214
2326
  } else {
2215
2327
  // No interface — insert before closing ---
2216
2328
  const closeFm = result.indexOf('---', result.indexOf('---') + 3)
2217
- result = result.slice(0, closeFm) + 'interface Props { fieldPrefix?: string }\n' + result.slice(closeFm)
2329
+ result =
2330
+ result.slice(0, closeFm) +
2331
+ 'interface Props { fieldPrefix?: string }\n' +
2332
+ result.slice(closeFm)
2218
2333
  }
2219
2334
 
2220
2335
  // 2. Add fieldPrefix to destructuring
2221
2336
  // Match: const { ..., highlight = false } = Astro.props
2222
- result = result.replace(
2223
- /(\bconst\s*\{[^}]*)(}\s*=\s*Astro\.props)/,
2224
- (_m, before, after) => {
2225
- // Avoid adding twice
2226
- if (before.includes('fieldPrefix')) return _m
2227
- const trimmed = before.trimEnd()
2228
- const sep = trimmed.endsWith(',') ? ' ' : ',\n '
2229
- return `${trimmed}${sep}fieldPrefix${after}`
2230
- },
2231
- )
2337
+ result = result.replace(/(\bconst\s*\{[^}]*)(}\s*=\s*Astro\.props)/, (_m, before, after) => {
2338
+ // Avoid adding twice
2339
+ if (before.includes('fieldPrefix')) return _m
2340
+ const trimmed = before.trimEnd()
2341
+ const sep = trimmed.endsWith(',') ? ' ' : ',\n '
2342
+ return `${trimmed}${sep}fieldPrefix${after}`
2343
+ })
2232
2344
 
2233
2345
  // 3. Patch template elements — work on the template part only
2234
2346
  const closingFm = result.indexOf('---', result.indexOf('---') + 3)
2235
2347
  const frontmatterPart = result.slice(0, closingFm + 3)
2236
2348
  let templatePart = result.slice(closingFm + 3)
2237
2349
 
2238
- const scalarFields = innerFields.filter(f => f.type !== 'array')
2239
- const arrayFields = innerFields.filter(f => f.type === 'array')
2350
+ const scalarFields = innerFields.filter((f) => f.type !== 'array')
2351
+ const arrayFields = innerFields.filter((f) => f.type === 'array')
2240
2352
 
2241
2353
  // 3a. Scalar fields: add data-sk-field to the element containing {propName}
2242
2354
  for (const field of scalarFields) {
@@ -2266,10 +2378,7 @@ export function patchChildComponentForFieldPrefix(
2266
2378
  // 3b. Array fields: add index param + data-sk-field on innermost element
2267
2379
  for (const field of arrayFields) {
2268
2380
  // Match: features.map((feature) => or features.map((feature, fi) =>
2269
- const mapParamRegex = new RegExp(
2270
- `(${field.key}\\.map\\s*\\(\\s*\\(\\s*)(\\w+)(\\s*\\))`,
2271
- 'g',
2272
- )
2381
+ const mapParamRegex = new RegExp(`(${field.key}\\.map\\s*\\(\\s*\\(\\s*)(\\w+)(\\s*\\))`, 'g')
2273
2382
  // Add _fi index if missing, and annotate the ul/ol containing the map
2274
2383
  templatePart = templatePart.replace(
2275
2384
  new RegExp(`(<(?:ul|ol)[^>]*?)(\\/?>)([\\s\\S]*?${field.key}\\.map)`, 'g'),
@@ -2293,11 +2402,14 @@ export function patchChildComponentForFieldPrefix(
2293
2402
  `(${field.key}\\.map\\s*\\([^)]*\\)\\s*=>\\s*\\([\\s\\S]*?)(<(?:span|li|td)\\b)([^>]*?>)([^<]*\\{\\w+\\})`,
2294
2403
  'g',
2295
2404
  )
2296
- templatePart = templatePart.replace(mapBlockRegex, (_m, mapHead, tagOpen, tagClose, content) => {
2297
- if (tagClose.includes('data-sk-field')) return _m
2298
- const attr = ` data-sk-field={fieldPrefix ? \`\${fieldPrefix}.${field.key}.\${_fi}\` : undefined}`
2299
- return `${mapHead}${tagOpen}${tagClose.slice(0, -1)}${attr}>${content}`
2300
- })
2405
+ templatePart = templatePart.replace(
2406
+ mapBlockRegex,
2407
+ (_m, mapHead, tagOpen, tagClose, content) => {
2408
+ if (tagClose.includes('data-sk-field')) return _m
2409
+ const attr = ` data-sk-field={fieldPrefix ? \`\${fieldPrefix}.${field.key}.\${_fi}\` : undefined}`
2410
+ return `${mapHead}${tagOpen}${tagClose.slice(0, -1)}${attr}>${content}`
2411
+ },
2412
+ )
2301
2413
  }
2302
2414
 
2303
2415
  return frontmatterPart + templatePart
@@ -2310,18 +2422,27 @@ export function patchChildComponentForFieldPrefix(
2310
2422
  export function detectChildImports(
2311
2423
  source: string,
2312
2424
  fields: PatchField[],
2313
- ): Array<{ compName: string; importPath: string; innerFields: Array<{ key: string; type: string }> }> {
2314
- const result: Array<{ compName: string; importPath: string; innerFields: Array<{ key: string; type: string }> }> = []
2425
+ ): Array<{
2426
+ compName: string
2427
+ importPath: string
2428
+ innerFields: Array<{ key: string; type: string }>
2429
+ }> {
2430
+ const result: Array<{
2431
+ compName: string
2432
+ importPath: string
2433
+ innerFields: Array<{ key: string; type: string }>
2434
+ }> = []
2315
2435
 
2316
2436
  for (const field of fields) {
2317
2437
  const compName = (field as any).options?.sourceComponent as string | undefined
2318
2438
  if (!compName) continue
2319
2439
 
2320
- const innerFields: Array<{ key: string; type: string }> =
2321
- ((field as any).options?.arrayItem?.fields ?? []).map((f: { key: string; type: string }) => ({
2322
- key: f.key,
2323
- type: f.type,
2324
- }))
2440
+ const innerFields: Array<{ key: string; type: string }> = (
2441
+ (field as any).options?.arrayItem?.fields ?? []
2442
+ ).map((f: { key: string; type: string }) => ({
2443
+ key: f.key,
2444
+ type: f.type,
2445
+ }))
2325
2446
 
2326
2447
  // Find: import CompName from '...'
2327
2448
  const importRegex = new RegExp(`import\\s+${compName}\\s+from\\s+['"]([^'"]+)['"]`)