@setzkasten-cms/astro-admin 1.4.2 → 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.
- package/dist/api-routes/_auth-guard.d.ts +27 -3
- package/dist/api-routes/_auth-guard.js +5 -2
- package/dist/api-routes/_dev-session-secret.d.ts +8 -0
- package/dist/api-routes/_dev-session-secret.js +8 -0
- package/dist/api-routes/_github-token.js +1 -1
- package/dist/api-routes/_role-resolver.js +6 -3
- package/dist/api-routes/_session-secret.d.ts +19 -0
- package/dist/api-routes/_session-secret.js +7 -0
- package/dist/api-routes/_session-signing.d.ts +45 -0
- package/dist/api-routes/_session-signing.js +8 -0
- package/dist/api-routes/_webhook-dispatcher.js +4 -4
- package/dist/api-routes/asset-proxy.js +1 -1
- package/dist/api-routes/auth-callback.js +12 -5
- package/dist/api-routes/auth-logout.d.ts +4 -4
- package/dist/api-routes/auth-logout.js +8 -2
- package/dist/api-routes/auth-session.d.ts +6 -0
- package/dist/api-routes/auth-session.js +19 -19
- package/dist/api-routes/auth-setzkasten-login.js +14 -7
- package/dist/api-routes/catalog-add.js +59 -17
- package/dist/api-routes/catalog-export.js +14 -4
- package/dist/api-routes/config.d.ts +10 -3
- package/dist/api-routes/config.js +26 -4
- package/dist/api-routes/deploy-hook.js +8 -8
- package/dist/api-routes/editors.d.ts +1 -1
- package/dist/api-routes/editors.js +5 -2
- package/dist/api-routes/github-proxy.js +30 -8
- package/dist/api-routes/global-config.js +6 -3
- package/dist/api-routes/history-rollback.js +31 -14
- package/dist/api-routes/history-version.js +8 -6
- package/dist/api-routes/history.js +5 -2
- package/dist/api-routes/icons-local.js +1 -1
- package/dist/api-routes/init-add-section.js +150 -48
- package/dist/api-routes/init-apply.js +56 -42
- package/dist/api-routes/init-migrate.js +43 -36
- package/dist/api-routes/init-scan-page.d.ts +1 -1
- package/dist/api-routes/init-scan-page.js +59 -13
- package/dist/api-routes/init-scan.js +22 -7
- package/dist/api-routes/migrate-to-multi.js +5 -2
- package/dist/api-routes/pages.js +15 -4
- package/dist/api-routes/section-add.js +68 -16
- package/dist/api-routes/section-commit-pending.js +70 -22
- package/dist/api-routes/section-delete.js +49 -14
- package/dist/api-routes/section-duplicate.js +65 -16
- package/dist/api-routes/section-prepare-copy.js +15 -2
- package/dist/api-routes/section-prepare.js +25 -4
- package/dist/api-routes/setup-github-app-bounce.js +15 -1
- package/dist/api-routes/setup-github-app-branches.js +9 -6
- package/dist/api-routes/setup-github-app-callback.js +24 -1
- package/dist/api-routes/setup-github-app-credentials.d.ts +27 -0
- package/dist/api-routes/setup-github-app-credentials.js +43 -0
- package/dist/api-routes/setup-github-app-installed.js +22 -1
- package/dist/api-routes/setup-github-app-repos.js +5 -2
- package/dist/api-routes/setup-github-app.d.ts +4 -0
- package/dist/api-routes/setup-github-app.js +19 -2
- package/dist/api-routes/updater-register.js +7 -1
- package/dist/api-routes/webhooks-status.js +5 -2
- package/dist/api-routes/webhooks-test.js +9 -8
- package/dist/api-routes/webhooks.js +12 -14
- package/dist/api-routes/websites-add.js +5 -2
- package/dist/api-routes/websites-remove.js +5 -2
- package/dist/{chunk-ZQDGGWJP.js → chunk-5KMGSFCZ.js} +2 -2
- package/dist/{chunk-RHJONMLK.js → chunk-CDXCYYQR.js} +222 -5
- package/dist/{chunk-NKDATSPA.js → chunk-DP6RTINQ.js} +1 -1
- package/dist/chunk-KENFINT4.js +76 -0
- package/dist/chunk-ONP6BRZO.js +47 -0
- package/dist/{chunk-INIWFKQ3.js → chunk-Q5HV47DW.js} +33 -19
- package/dist/chunk-QVCW6EF3.js +26 -0
- package/dist/{chunk-K22A4ZBS.js → chunk-UHI6323G.js} +293 -174
- package/dist/{chunk-AM4DZXXM.js → chunk-UJAFZEX2.js} +76 -9
- package/package.json +12 -6
- package/src/api-routes/__tests__/_session-signing.test.ts +114 -0
- package/src/api-routes/__tests__/_session-test-helper.ts +27 -0
- package/src/api-routes/__tests__/add-section-helpers.test.ts +91 -97
- package/src/api-routes/__tests__/auth-guard.test.ts +46 -7
- package/src/api-routes/__tests__/catalog-api.test.ts +14 -6
- package/src/api-routes/__tests__/commit-trailers.test.ts +5 -5
- package/src/api-routes/__tests__/deferred-operations.test.ts +9 -12
- package/src/api-routes/__tests__/deploy-hook.test.ts +3 -8
- package/src/api-routes/__tests__/feature-gate.test.ts +1 -1
- package/src/api-routes/__tests__/github-cache.test.ts +1 -1
- package/src/api-routes/__tests__/github-token.test.ts +1 -1
- package/src/api-routes/__tests__/global-config-theme.test.ts +4 -4
- package/src/api-routes/__tests__/history-rollback.test.ts +6 -3
- package/src/api-routes/__tests__/history.test.ts +9 -6
- package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +11 -3
- package/src/api-routes/__tests__/migrate-to-multi.test.ts +5 -1
- package/src/api-routes/__tests__/pages-meta-store.test.ts +10 -5
- package/src/api-routes/__tests__/pages.test.ts +7 -2
- package/src/api-routes/__tests__/patch-page-file.test.ts +71 -19
- package/src/api-routes/__tests__/route-registry.test.ts +11 -18
- package/src/api-routes/__tests__/scan-page-helpers.test.ts +13 -10
- package/src/api-routes/__tests__/section-management.test.ts +28 -28
- package/src/api-routes/__tests__/setup-github-app-callback.test.ts +58 -16
- package/src/api-routes/__tests__/setup-github-app-repos.test.ts +4 -5
- package/src/api-routes/__tests__/setup-github-app.test.ts +27 -7
- package/src/api-routes/__tests__/storage-config-for-request.test.ts +83 -0
- package/src/api-routes/__tests__/updater-register.test.ts +230 -0
- package/src/api-routes/__tests__/webhook-signing.test.ts +1 -1
- package/src/api-routes/__tests__/webhooks.test.ts +19 -7
- package/src/api-routes/__tests__/websites-add.test.ts +2 -1
- package/src/api-routes/__tests__/websites-remove.test.ts +2 -1
- package/src/api-routes/_auth-guard.ts +47 -15
- package/src/api-routes/_commit-trailers.ts +3 -2
- package/src/api-routes/_dev-session-secret.ts +79 -0
- package/src/api-routes/_github-token.ts +1 -1
- package/src/api-routes/_pages-meta-store.ts +2 -2
- package/src/api-routes/_role-resolver.ts +7 -5
- package/src/api-routes/_session-secret.ts +46 -0
- package/src/api-routes/_session-signing.ts +135 -0
- package/src/api-routes/_vercel-origin.ts +2 -6
- package/src/api-routes/_webhook-dispatcher.ts +12 -16
- package/src/api-routes/_website-resolver.ts +3 -10
- package/src/api-routes/auth-callback.ts +9 -5
- package/src/api-routes/auth-login.ts +5 -3
- package/src/api-routes/auth-logout.ts +18 -1
- package/src/api-routes/auth-session.ts +13 -21
- package/src/api-routes/auth-setzkasten-login.ts +12 -9
- package/src/api-routes/catalog-add.ts +89 -31
- package/src/api-routes/catalog-export.ts +30 -10
- package/src/api-routes/config.ts +39 -6
- package/src/api-routes/deploy-hook.ts +13 -11
- package/src/api-routes/editors.ts +33 -22
- package/src/api-routes/github-proxy.ts +25 -11
- package/src/api-routes/global-config.ts +103 -18
- package/src/api-routes/history-rollback.ts +41 -14
- package/src/api-routes/history-version.ts +5 -6
- package/src/api-routes/history.ts +3 -3
- package/src/api-routes/icons-local.ts +2 -2
- package/src/api-routes/init-add-section.ts +218 -88
- package/src/api-routes/init-apply.ts +71 -56
- package/src/api-routes/init-migrate.ts +54 -48
- package/src/api-routes/init-scan-page.ts +77 -30
- package/src/api-routes/init-scan.ts +19 -11
- package/src/api-routes/pages.ts +16 -11
- package/src/api-routes/section-add.ts +98 -27
- package/src/api-routes/section-commit-pending.ts +87 -34
- package/src/api-routes/section-delete.ts +76 -27
- package/src/api-routes/section-duplicate.ts +95 -28
- package/src/api-routes/section-management.ts +3 -7
- package/src/api-routes/section-prepare-copy.ts +29 -8
- package/src/api-routes/section-prepare.ts +38 -10
- package/src/api-routes/setup-github-app-bounce.ts +7 -1
- package/src/api-routes/setup-github-app-branches.ts +6 -7
- package/src/api-routes/setup-github-app-callback.ts +18 -1
- package/src/api-routes/setup-github-app-credentials.ts +55 -0
- package/src/api-routes/setup-github-app-installed.ts +12 -1
- package/src/api-routes/setup-github-app-repos.ts +2 -3
- package/src/api-routes/setup-github-app.ts +14 -5
- package/src/api-routes/updater-check.ts +6 -4
- package/src/api-routes/updater-register.ts +34 -20
- package/src/api-routes/updater-transfer.ts +8 -6
- package/src/api-routes/updater-unbind.ts +14 -10
- package/src/api-routes/webhooks-test.ts +9 -11
- package/src/api-routes/webhooks.ts +15 -19
- package/src/init/__tests__/page-level.test.ts +279 -105
- package/src/init/__tests__/page-list-coverage.test.ts +70 -70
- package/src/init/__tests__/patcher-child-component.test.ts +126 -0
- package/src/init/__tests__/patcher-edge-cases.test.ts +47 -23
- package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +16 -6
- package/src/init/__tests__/patcher-page-mode.test.ts +30 -20
- package/src/init/__tests__/section-pipeline.test.ts +102 -16
- package/src/init/astro-config-patcher.ts +4 -18
- package/src/init/astro-detector.ts +2 -7
- package/src/init/astro-section-analyzer-v2.ts +475 -193
- package/src/init/field-label-enricher.ts +6 -6
- package/src/init/template-patcher-v2.ts +490 -56
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* and accepts arbitrary HTML children (including `<p>`).
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { describe,
|
|
20
|
+
import { describe, expect, it } from 'vitest'
|
|
21
21
|
import { patchTemplateForFields } from '../../init/template-patcher-v2'
|
|
22
22
|
import type { PatchField } from '../../init/template-patcher-v2'
|
|
23
23
|
|
|
@@ -53,15 +53,21 @@ describe('patchMixedContentField — phrasing wrapper conversion', () => {
|
|
|
53
53
|
]
|
|
54
54
|
|
|
55
55
|
it('converts a <p> mixed-content wrapper to <div> so RTE-injected <p> children remain valid', async () => {
|
|
56
|
-
const patched = await patchTemplateForFields(IMPRESSUM_SECTION, '_page_impressum', fields, [], {
|
|
56
|
+
const patched = await patchTemplateForFields(IMPRESSUM_SECTION, '_page_impressum', fields, [], {
|
|
57
|
+
mode: 'page',
|
|
58
|
+
})
|
|
57
59
|
|
|
58
60
|
// The patched description binding must NOT live on a <p> element —
|
|
59
61
|
// otherwise an RTE-edited value of "<p>…</p>" produces nested <p>'s.
|
|
60
62
|
expect(patched).not.toMatch(/<p[^>]*data-sk-field="_page_impressum\.description"/)
|
|
61
63
|
|
|
62
64
|
// Instead, the wrapper should be a <div> that retains the original classes.
|
|
63
|
-
expect(patched).toMatch(
|
|
64
|
-
|
|
65
|
+
expect(patched).toMatch(
|
|
66
|
+
/<div[^>]*class="text-sk-slate leading-relaxed"[^>]*data-sk-field="_page_impressum\.description"/,
|
|
67
|
+
)
|
|
68
|
+
expect(patched).toMatch(
|
|
69
|
+
/data-sk-field="_page_impressum\.description"[^>]*set:html=\{skData\?\.description/,
|
|
70
|
+
)
|
|
65
71
|
|
|
66
72
|
// The closing tag must match (no </p> for this binding's wrapper).
|
|
67
73
|
// A simple sanity check: the <div ... data-sk-field="…description"> must be followed by </div>.
|
|
@@ -77,12 +83,16 @@ describe('patchMixedContentField — phrasing wrapper conversion', () => {
|
|
|
77
83
|
// Headings rarely host RTE content but must remain semantically intact.
|
|
78
84
|
// patchTextField (not patchMixedContentField) handles plain text headings,
|
|
79
85
|
// so the <h2> should keep its tag name.
|
|
80
|
-
const patched = await patchTemplateForFields(IMPRESSUM_SECTION, '_page_impressum', fields, [], {
|
|
86
|
+
const patched = await patchTemplateForFields(IMPRESSUM_SECTION, '_page_impressum', fields, [], {
|
|
87
|
+
mode: 'page',
|
|
88
|
+
})
|
|
81
89
|
expect(patched).toMatch(/<h2[^>]*data-sk-field="_page_impressum\.heading2"/)
|
|
82
90
|
})
|
|
83
91
|
|
|
84
92
|
it('keeps the original <p> classes on the converted <div> wrapper', async () => {
|
|
85
|
-
const patched = await patchTemplateForFields(IMPRESSUM_SECTION, '_page_impressum', fields, [], {
|
|
93
|
+
const patched = await patchTemplateForFields(IMPRESSUM_SECTION, '_page_impressum', fields, [], {
|
|
94
|
+
mode: 'page',
|
|
95
|
+
})
|
|
86
96
|
// The Tailwind classes from the original <p class="text-sk-slate leading-relaxed">
|
|
87
97
|
// must be preserved on the new <div>.
|
|
88
98
|
expect(patched).toContain('text-sk-slate leading-relaxed')
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 5. removeOldVarDeclarations causes TDZ (alias placed before skData declaration)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe,
|
|
12
|
+
import { describe, expect, it } from 'vitest'
|
|
13
13
|
import { patchTemplateForFields } from '../../init/template-patcher-v2'
|
|
14
14
|
import type { PatchField } from '../../init/template-patcher-v2'
|
|
15
15
|
|
|
@@ -60,14 +60,22 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|
|
60
60
|
`
|
|
61
61
|
|
|
62
62
|
const fields: PatchField[] = [
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
{
|
|
64
|
+
key: 'items',
|
|
65
|
+
type: 'array',
|
|
66
|
+
defaultValue: [
|
|
67
|
+
{ name: '@setzkasten-cms/core', desc: 'Schema, Feldtypen, Validation' },
|
|
68
|
+
{ name: '@setzkasten-cms/ui', desc: 'React-UI: Provider, FormStore' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'items2',
|
|
73
|
+
type: 'array',
|
|
74
|
+
defaultValue: [
|
|
75
|
+
{ title: 'Dependency Inversion', desc: 'UI haengt nur von Ports ab' },
|
|
76
|
+
{ title: 'Schema-First', desc: 'Alles leitet sich vom TypeScript-Schema ab' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
71
79
|
]
|
|
72
80
|
|
|
73
81
|
it('should reference skData?.items for the first array', async () => {
|
|
@@ -111,9 +119,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|
|
111
119
|
</BaseLayout>
|
|
112
120
|
`
|
|
113
121
|
|
|
114
|
-
const fields: PatchField[] = [
|
|
115
|
-
{ key: 'heading', type: 'text', defaultValue: 'Titel' },
|
|
116
|
-
]
|
|
122
|
+
const fields: PatchField[] = [{ key: 'heading', type: 'text', defaultValue: 'Titel' }]
|
|
117
123
|
|
|
118
124
|
it('should add id="section-_page_test" to the <section> element', async () => {
|
|
119
125
|
const patched = await patchTemplateForFields(source, '_page_test', fields, [], { mode: 'page' })
|
|
@@ -147,9 +153,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|
|
147
153
|
</BaseLayout>
|
|
148
154
|
`
|
|
149
155
|
|
|
150
|
-
const fields: PatchField[] = [
|
|
151
|
-
{ key: 'heading', type: 'text', defaultValue: 'Architektur' },
|
|
152
|
-
]
|
|
156
|
+
const fields: PatchField[] = [{ key: 'heading', type: 'text', defaultValue: 'Architektur' }]
|
|
153
157
|
|
|
154
158
|
it('should add data-sk-field to the <h1> element', async () => {
|
|
155
159
|
const patched = await patchTemplateForFields(source, '_page_test', fields, [], { mode: 'page' })
|
|
@@ -203,7 +207,7 @@ const fields = ['Alpha', 'Beta', 'Gamma'];
|
|
|
203
207
|
const fmMatch = patched.match(/^---\n([\s\S]*?)\n---/)
|
|
204
208
|
const frontmatter = fmMatch ? fmMatch[1]! : patched
|
|
205
209
|
|
|
206
|
-
const lines = frontmatter.split('\n').map(l => l.trim())
|
|
210
|
+
const lines = frontmatter.split('\n').map((l) => l.trim())
|
|
207
211
|
expect(lines).not.toContain(';')
|
|
208
212
|
})
|
|
209
213
|
})
|
|
@@ -239,7 +243,9 @@ const fields = ['Alpha', 'Beta', 'Gamma'];
|
|
|
239
243
|
]
|
|
240
244
|
|
|
241
245
|
it('should place the fields alias AFTER the skData declaration', async () => {
|
|
242
|
-
const patched = await patchTemplateForFields(source, '_page_test', patchFields, [], {
|
|
246
|
+
const patched = await patchTemplateForFields(source, '_page_test', patchFields, [], {
|
|
247
|
+
mode: 'page',
|
|
248
|
+
})
|
|
243
249
|
|
|
244
250
|
const fmMatch = patched.match(/^---\n([\s\S]*?)\n---/)
|
|
245
251
|
const frontmatter = fmMatch ? fmMatch[1]! : patched
|
|
@@ -253,17 +259,21 @@ const fields = ['Alpha', 'Beta', 'Gamma'];
|
|
|
253
259
|
})
|
|
254
260
|
|
|
255
261
|
it('should not contain a stray semicolon line in frontmatter', async () => {
|
|
256
|
-
const patched = await patchTemplateForFields(source, '_page_test', patchFields, [], {
|
|
262
|
+
const patched = await patchTemplateForFields(source, '_page_test', patchFields, [], {
|
|
263
|
+
mode: 'page',
|
|
264
|
+
})
|
|
257
265
|
|
|
258
266
|
const fmMatch = patched.match(/^---\n([\s\S]*?)\n---/)
|
|
259
267
|
const frontmatter = fmMatch ? fmMatch[1]! : patched
|
|
260
268
|
|
|
261
|
-
const lines = frontmatter.split('\n').map(l => l.trim())
|
|
269
|
+
const lines = frontmatter.split('\n').map((l) => l.trim())
|
|
262
270
|
expect(lines).not.toContain(';')
|
|
263
271
|
})
|
|
264
272
|
|
|
265
273
|
it('should preserve fields.length in the template', async () => {
|
|
266
|
-
const patched = await patchTemplateForFields(source, '_page_test', patchFields, [], {
|
|
274
|
+
const patched = await patchTemplateForFields(source, '_page_test', patchFields, [], {
|
|
275
|
+
mode: 'page',
|
|
276
|
+
})
|
|
267
277
|
|
|
268
278
|
// Extract template (after closing ---)
|
|
269
279
|
const templatePart = patched.split('\n---\n').slice(1).join('\n---\n')
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* - Stale frontmatter var cleanup
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { describe, it, expect, beforeAll } from 'vitest'
|
|
16
15
|
import { readFileSync, readdirSync } from 'fs'
|
|
17
|
-
import {
|
|
16
|
+
import { basename, join } from 'path'
|
|
17
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
18
18
|
import { analyzeAstroSection } from '../../init/astro-section-analyzer-v2'
|
|
19
19
|
import { patchTemplateForFields } from '../../init/template-patcher-v2'
|
|
20
20
|
|
|
@@ -23,7 +23,9 @@ import { patchTemplateForFields } from '../../init/template-patcher-v2'
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
|
|
25
25
|
const SECTIONS_DIR = join(import.meta.dirname!, '..', '..', '..', '..', '..', 'test-sections')
|
|
26
|
-
const files = readdirSync(SECTIONS_DIR)
|
|
26
|
+
const files = readdirSync(SECTIONS_DIR)
|
|
27
|
+
.filter((f) => f.endsWith('.astro'))
|
|
28
|
+
.sort()
|
|
27
29
|
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
29
31
|
// Helpers (ported from test-sections-runner.mts)
|
|
@@ -40,12 +42,16 @@ function extractClassValues(src: string): string[] {
|
|
|
40
42
|
function checkCssIntegrity(
|
|
41
43
|
source: string,
|
|
42
44
|
patched: string,
|
|
43
|
-
groups: Array<{
|
|
45
|
+
groups: Array<{
|
|
46
|
+
tag: string
|
|
47
|
+
instances: Array<{ start: number; end: number }>
|
|
48
|
+
fields?: Array<{ key: string; defaultValues?: unknown[] }>
|
|
49
|
+
}>,
|
|
44
50
|
): { ok: boolean; lost: string[] } {
|
|
45
51
|
const lost: string[] = []
|
|
46
52
|
const dynamicClassValues = new Set<string>()
|
|
47
53
|
for (const g of groups) {
|
|
48
|
-
for (const f of
|
|
54
|
+
for (const f of g.fields ?? []) {
|
|
49
55
|
if (f.key.startsWith('_class') && f.defaultValues) {
|
|
50
56
|
for (const dv of f.defaultValues) {
|
|
51
57
|
if (typeof dv === 'string') dynamicClassValues.add(dv)
|
|
@@ -102,6 +108,17 @@ function checkSkFieldBindings(
|
|
|
102
108
|
}
|
|
103
109
|
continue
|
|
104
110
|
}
|
|
111
|
+
// Array fields from repeated component instances use fieldPrefix= instead of data-sk-field
|
|
112
|
+
// (Astro components don't forward unknown props to the DOM).
|
|
113
|
+
if (field.type === 'array' && (field as any).options?.sourceComponent) {
|
|
114
|
+
const fieldPrefixBinding = `${sectionKey}.${field.key}.\${_i}\``
|
|
115
|
+
if (patched.includes(fieldPrefixBinding)) {
|
|
116
|
+
found.push(`${sectionKey}.${field.key}.*`)
|
|
117
|
+
} else {
|
|
118
|
+
missing.push(`${sectionKey}.${field.key}`)
|
|
119
|
+
}
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
105
122
|
if (!fieldNeedsSkBinding(field)) {
|
|
106
123
|
skipped.push(`${sectionKey}.${field.key} (${field.type})`)
|
|
107
124
|
continue
|
|
@@ -122,9 +139,18 @@ function checkSkFieldBindings(
|
|
|
122
139
|
|
|
123
140
|
for (const g of groups) {
|
|
124
141
|
for (const innerField of g.fields) {
|
|
125
|
-
if (innerField.type === 'array') {
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
if (innerField.type === 'array') {
|
|
143
|
+
skipped.push(`${sectionKey}.${g.fieldKey}.*.${innerField.key} (array)`)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
if (!fieldNeedsSkBinding(innerField)) {
|
|
147
|
+
skipped.push(`${sectionKey}.${g.fieldKey}.*.${innerField.key} (${innerField.type})`)
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (!patched.includes(`item.${innerField.key}`)) {
|
|
151
|
+
skipped.push(`${sectionKey}.${g.fieldKey}.*.${innerField.key} (not replaced)`)
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
128
154
|
const innerBinding = `${sectionKey}.${g.fieldKey}.\${_i}.${innerField.key}`
|
|
129
155
|
if (patched.includes(innerBinding)) {
|
|
130
156
|
found.push(`${sectionKey}.${g.fieldKey}.*.${innerField.key}`)
|
|
@@ -162,7 +188,13 @@ describe('Section Pipeline', () => {
|
|
|
162
188
|
const getSectionMatch = isPageMode ? source.match(/getSection\(['"]([^'"]+)['"]\)/) : null
|
|
163
189
|
sectionKey = getSectionMatch?.[1] ?? filenameKey
|
|
164
190
|
const options = isPageMode ? { mode: 'page' as const } : undefined
|
|
165
|
-
section = await analyzeAstroSection(
|
|
191
|
+
section = await analyzeAstroSection(
|
|
192
|
+
source,
|
|
193
|
+
sectionKey,
|
|
194
|
+
name,
|
|
195
|
+
`src/components/sections/${file}`,
|
|
196
|
+
options,
|
|
197
|
+
)
|
|
166
198
|
groups = (section as any)._analyzerResult?.repeatedGroups ?? []
|
|
167
199
|
patched = await patchTemplateForFields(source, sectionKey, section.fields, groups, options)
|
|
168
200
|
})
|
|
@@ -206,10 +238,13 @@ describe('Section Pipeline', () => {
|
|
|
206
238
|
}
|
|
207
239
|
}
|
|
208
240
|
}
|
|
209
|
-
const duplicateArrays = section.fields.filter(
|
|
210
|
-
f.type === 'array' && innerArraySources.has(f.key),
|
|
241
|
+
const duplicateArrays = section.fields.filter(
|
|
242
|
+
(f: any) => f.type === 'array' && innerArraySources.has(f.key),
|
|
211
243
|
)
|
|
212
|
-
expect(
|
|
244
|
+
expect(
|
|
245
|
+
duplicateArrays,
|
|
246
|
+
`Duplicate arrays: ${duplicateArrays.map((f: any) => f.key).join(', ')}`,
|
|
247
|
+
).toHaveLength(0)
|
|
213
248
|
})
|
|
214
249
|
|
|
215
250
|
it('should remove consumed frontmatter vars', () => {
|
|
@@ -256,7 +291,10 @@ describe('Section Pipeline', () => {
|
|
|
256
291
|
|
|
257
292
|
for (const g of groups) {
|
|
258
293
|
const arrayField = section.fields.find((f: any) => f.key === g.fieldKey)
|
|
259
|
-
if (!arrayField) {
|
|
294
|
+
if (!arrayField) {
|
|
295
|
+
issues.push(`No top-level field for group ${g.fieldKey}`)
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
260
298
|
|
|
261
299
|
const items = arrayField.defaultValue as Array<Record<string, unknown>> | undefined
|
|
262
300
|
if (!Array.isArray(items) || items.length === 0) {
|
|
@@ -283,7 +321,7 @@ describe('Section Pipeline', () => {
|
|
|
283
321
|
}
|
|
284
322
|
for (const refKey of referencedKeys) {
|
|
285
323
|
if (['map', 'length', 'filter', 'forEach', 'join', 'slice'].includes(refKey)) continue
|
|
286
|
-
const hasInAnyItem = items.some(item => refKey in item)
|
|
324
|
+
const hasInAnyItem = items.some((item) => refKey in item)
|
|
287
325
|
if (!hasInAnyItem) {
|
|
288
326
|
issues.push(`{item.${refKey}} used in template but missing in content JSON`)
|
|
289
327
|
}
|
|
@@ -324,7 +362,9 @@ describe('Section Pipeline', () => {
|
|
|
324
362
|
const origCount = originalClassCounts.get(val) ?? 0
|
|
325
363
|
if (keys.length > origCount) {
|
|
326
364
|
const label = instIdx === 0 ? 'template' : `instance ${instIdx}`
|
|
327
|
-
issues.push(
|
|
365
|
+
issues.push(
|
|
366
|
+
`${g.fieldKey} ${label}: "${val.slice(0, 40)}" assigned to [${keys.join(', ')}] but appears ${origCount}×`,
|
|
367
|
+
)
|
|
328
368
|
}
|
|
329
369
|
}
|
|
330
370
|
}
|
|
@@ -369,7 +409,9 @@ describe('Section Pipeline', () => {
|
|
|
369
409
|
const actualTag = lastPart.replace(/:\d+$/, '') || g.tag
|
|
370
410
|
|
|
371
411
|
if (actualTag !== expectedTag) {
|
|
372
|
-
issues.push(
|
|
412
|
+
issues.push(
|
|
413
|
+
`${cf.key} instance ${instIdx}: expected <${expectedTag}> but on <${actualTag}>`,
|
|
414
|
+
)
|
|
373
415
|
}
|
|
374
416
|
}
|
|
375
417
|
}
|
|
@@ -390,6 +432,50 @@ describe('Section Pipeline', () => {
|
|
|
390
432
|
const bindings = checkSkFieldBindings(patched, sectionKey, section.fields, groups)
|
|
391
433
|
expect(bindings.missing, `Missing bindings: ${bindings.missing.join(', ')}`).toHaveLength(0)
|
|
392
434
|
})
|
|
435
|
+
|
|
436
|
+
it('should collapse repeated component instances into .map()', () => {
|
|
437
|
+
const componentArrayFields = section.fields.filter(
|
|
438
|
+
(f: any) => f.type === 'array' && f.options?.sourceComponent,
|
|
439
|
+
)
|
|
440
|
+
if (componentArrayFields.length === 0) return
|
|
441
|
+
for (const field of componentArrayFields) {
|
|
442
|
+
const compName = (field as any).options.sourceComponent as string
|
|
443
|
+
const tagRegex = new RegExp(`<${compName}[\\s/>]`, 'g')
|
|
444
|
+
const originalCount = (source.match(tagRegex) || []).length
|
|
445
|
+
const patchedCount = (patched.match(tagRegex) || []).length
|
|
446
|
+
expect(
|
|
447
|
+
patchedCount,
|
|
448
|
+
`<${compName}> count should be reduced (was ${originalCount}, still ${patchedCount})`,
|
|
449
|
+
).toBeLessThan(originalCount)
|
|
450
|
+
expect(patched, `Expected .map( after collapsing <${compName}>`).toContain('.map(')
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('should remove stale frontmatter vars from repeated component props', () => {
|
|
455
|
+
const componentArrayFields = section.fields.filter(
|
|
456
|
+
(f: any) =>
|
|
457
|
+
f.type === 'array' && f.options?.sourceComponent && Array.isArray(f.defaultValue),
|
|
458
|
+
)
|
|
459
|
+
if (componentArrayFields.length === 0) return
|
|
460
|
+
const fmEnd = patched.indexOf('---', patched.indexOf('---') + 3)
|
|
461
|
+
const patchedFm = fmEnd > 0 ? patched.slice(0, fmEnd) : ''
|
|
462
|
+
const staleVars: string[] = []
|
|
463
|
+
for (const field of componentArrayFields) {
|
|
464
|
+
const items = (field as any).defaultValue as Array<Record<string, unknown>>
|
|
465
|
+
for (const innerField of ((field as any).options?.arrayItem?.fields ?? []) as Array<{
|
|
466
|
+
key: string
|
|
467
|
+
}>) {
|
|
468
|
+
// Only string-array props might have been declared as frontmatter vars
|
|
469
|
+
const allVarLike = items.every((item) => Array.isArray(item[innerField.key]))
|
|
470
|
+
if (!allVarLike) continue
|
|
471
|
+
const varNames = items.map((_, i) => `${innerField.key}${i === 0 ? '' : i + 1}`)
|
|
472
|
+
for (const v of varNames) {
|
|
473
|
+
if (patchedFm.includes(`const ${v}`)) staleVars.push(v)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
expect(staleVars, `Stale frontmatter vars: ${staleVars.join(', ')}`).toHaveLength(0)
|
|
478
|
+
})
|
|
393
479
|
})
|
|
394
480
|
}
|
|
395
481
|
})
|
|
@@ -21,11 +21,7 @@ export function patchAstroConfig(source: string): string | null {
|
|
|
21
21
|
// Find the last import statement
|
|
22
22
|
const lastImportIndex = findLastImportEnd(result)
|
|
23
23
|
if (lastImportIndex >= 0) {
|
|
24
|
-
result =
|
|
25
|
-
result.slice(0, lastImportIndex) +
|
|
26
|
-
'\n' +
|
|
27
|
-
importLine +
|
|
28
|
-
result.slice(lastImportIndex)
|
|
24
|
+
result = result.slice(0, lastImportIndex) + '\n' + importLine + result.slice(lastImportIndex)
|
|
29
25
|
} else {
|
|
30
26
|
// No imports found — add at the very top
|
|
31
27
|
result = importLine + '\n' + result
|
|
@@ -67,19 +63,11 @@ function addToIntegrations(source: string): string {
|
|
|
67
63
|
|
|
68
64
|
// Check if array is empty
|
|
69
65
|
if (after.startsWith(']')) {
|
|
70
|
-
return (
|
|
71
|
-
source.slice(0, insertPos) +
|
|
72
|
-
'setzkasten()' +
|
|
73
|
-
source.slice(insertPos)
|
|
74
|
-
)
|
|
66
|
+
return source.slice(0, insertPos) + 'setzkasten()' + source.slice(insertPos)
|
|
75
67
|
}
|
|
76
68
|
|
|
77
69
|
// Array has items — prepend
|
|
78
|
-
return (
|
|
79
|
-
source.slice(0, insertPos) +
|
|
80
|
-
'\n setzkasten(),\n ' +
|
|
81
|
-
source.slice(insertPos)
|
|
82
|
-
)
|
|
70
|
+
return source.slice(0, insertPos) + '\n setzkasten(),\n ' + source.slice(insertPos)
|
|
83
71
|
}
|
|
84
72
|
|
|
85
73
|
// Case 2: No integrations array — add it to defineConfig
|
|
@@ -87,9 +75,7 @@ function addToIntegrations(source: string): string {
|
|
|
87
75
|
if (defineConfigMatch && defineConfigMatch.index !== undefined) {
|
|
88
76
|
const insertPos = defineConfigMatch.index + defineConfigMatch[0].length
|
|
89
77
|
return (
|
|
90
|
-
source.slice(0, insertPos) +
|
|
91
|
-
'\n integrations: [setzkasten()],\n' +
|
|
92
|
-
source.slice(insertPos)
|
|
78
|
+
source.slice(0, insertPos) + '\n integrations: [setzkasten()],\n' + source.slice(insertPos)
|
|
93
79
|
)
|
|
94
80
|
}
|
|
95
81
|
|
|
@@ -47,9 +47,7 @@ export function findAstroPages(files: RepoFile[], projectRoot: string): AstroPag
|
|
|
47
47
|
|
|
48
48
|
const name = relativePath.replace(/\.astro$/, '')
|
|
49
49
|
const isIndex = name === 'index' || name.endsWith('/index')
|
|
50
|
-
const pageKey = isIndex
|
|
51
|
-
? name === 'index' ? 'index' : name.replace(/\/index$/, '')
|
|
52
|
-
: name
|
|
50
|
+
const pageKey = isIndex ? (name === 'index' ? 'index' : name.replace(/\/index$/, '')) : name
|
|
53
51
|
|
|
54
52
|
pages.push({
|
|
55
53
|
filePath: file.path,
|
|
@@ -170,10 +168,7 @@ function resolveImportPath(
|
|
|
170
168
|
// Handle alias imports like @/components/... or ~/components/...
|
|
171
169
|
if (importPath.startsWith('@/') || importPath.startsWith('~/')) {
|
|
172
170
|
const aliasPath = importPath.replace(/^[@~]\//, '')
|
|
173
|
-
const candidates = [
|
|
174
|
-
`${projectRoot}/src/${aliasPath}`,
|
|
175
|
-
`${projectRoot}/src/${aliasPath}.astro`,
|
|
176
|
-
]
|
|
171
|
+
const candidates = [`${projectRoot}/src/${aliasPath}`, `${projectRoot}/src/${aliasPath}.astro`]
|
|
177
172
|
for (const candidate of candidates) {
|
|
178
173
|
if (allFiles.some((f) => f.path === candidate)) return candidate
|
|
179
174
|
}
|