@407dev/cms-astro 0.1.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 (212) hide show
  1. package/README.md +69 -0
  2. package/dist/chunk-2TKIQIOE.js +118 -0
  3. package/dist/chunk-2TKIQIOE.js.map +1 -0
  4. package/dist/chunk-53OQ5RXJ.js +912 -0
  5. package/dist/chunk-53OQ5RXJ.js.map +1 -0
  6. package/dist/chunk-56OMNQLH.js +1013 -0
  7. package/dist/chunk-56OMNQLH.js.map +1 -0
  8. package/dist/chunk-5AOOMQ7K.js +1031 -0
  9. package/dist/chunk-5AOOMQ7K.js.map +1 -0
  10. package/dist/chunk-5XRUCFDC.js +743 -0
  11. package/dist/chunk-5XRUCFDC.js.map +1 -0
  12. package/dist/chunk-62ZHCOLZ.js +25 -0
  13. package/dist/chunk-62ZHCOLZ.js.map +1 -0
  14. package/dist/chunk-6AZLRSYA.js +126 -0
  15. package/dist/chunk-6AZLRSYA.js.map +1 -0
  16. package/dist/chunk-6L3ZTCFB.js +14 -0
  17. package/dist/chunk-6L3ZTCFB.js.map +1 -0
  18. package/dist/chunk-6O7ZNNDT.js +697 -0
  19. package/dist/chunk-6O7ZNNDT.js.map +1 -0
  20. package/dist/chunk-6PQIXTMK.js +697 -0
  21. package/dist/chunk-6PQIXTMK.js.map +1 -0
  22. package/dist/chunk-7AW6M7QW.js +1013 -0
  23. package/dist/chunk-7AW6M7QW.js.map +1 -0
  24. package/dist/chunk-7E3XJLKX.js +73 -0
  25. package/dist/chunk-7E3XJLKX.js.map +1 -0
  26. package/dist/chunk-7LM54OTA.js +1031 -0
  27. package/dist/chunk-7LM54OTA.js.map +1 -0
  28. package/dist/chunk-7LMUFCWX.js +126 -0
  29. package/dist/chunk-7LMUFCWX.js.map +1 -0
  30. package/dist/chunk-A3OGL4OX.js +706 -0
  31. package/dist/chunk-A3OGL4OX.js.map +1 -0
  32. package/dist/chunk-AO4UAWOV.js +1030 -0
  33. package/dist/chunk-AO4UAWOV.js.map +1 -0
  34. package/dist/chunk-B4YYLS7Y.js +14 -0
  35. package/dist/chunk-B4YYLS7Y.js.map +1 -0
  36. package/dist/chunk-BDFVGXM2.js +697 -0
  37. package/dist/chunk-BDFVGXM2.js.map +1 -0
  38. package/dist/chunk-CXM7NZVX.js +1031 -0
  39. package/dist/chunk-CXM7NZVX.js.map +1 -0
  40. package/dist/chunk-DMLOBKYE.js +96 -0
  41. package/dist/chunk-DMLOBKYE.js.map +1 -0
  42. package/dist/chunk-DXJSYGS6.js +743 -0
  43. package/dist/chunk-DXJSYGS6.js.map +1 -0
  44. package/dist/chunk-EFUSAQZM.js +72 -0
  45. package/dist/chunk-EFUSAQZM.js.map +1 -0
  46. package/dist/chunk-F7KDGPBN.js +1013 -0
  47. package/dist/chunk-F7KDGPBN.js.map +1 -0
  48. package/dist/chunk-FBSQT45X.js +126 -0
  49. package/dist/chunk-FBSQT45X.js.map +1 -0
  50. package/dist/chunk-FEPDOMVH.js +697 -0
  51. package/dist/chunk-FEPDOMVH.js.map +1 -0
  52. package/dist/chunk-FQDOL2QW.js +25 -0
  53. package/dist/chunk-FQDOL2QW.js.map +1 -0
  54. package/dist/chunk-GB3LGQJM.js +109 -0
  55. package/dist/chunk-GB3LGQJM.js.map +1 -0
  56. package/dist/chunk-HCNP6R3P.js +736 -0
  57. package/dist/chunk-HCNP6R3P.js.map +1 -0
  58. package/dist/chunk-J6WEJDRI.js +743 -0
  59. package/dist/chunk-J6WEJDRI.js.map +1 -0
  60. package/dist/chunk-JHEAPNJ2.js +117 -0
  61. package/dist/chunk-JHEAPNJ2.js.map +1 -0
  62. package/dist/chunk-JOJDJ6GW.js +25 -0
  63. package/dist/chunk-JOJDJ6GW.js.map +1 -0
  64. package/dist/chunk-KTDV6WYB.js +202 -0
  65. package/dist/chunk-KTDV6WYB.js.map +1 -0
  66. package/dist/chunk-KZWJBKPC.js +734 -0
  67. package/dist/chunk-KZWJBKPC.js.map +1 -0
  68. package/dist/chunk-LEAQBGL6.js +109 -0
  69. package/dist/chunk-LEAQBGL6.js.map +1 -0
  70. package/dist/chunk-LGZJL2Q7.js +697 -0
  71. package/dist/chunk-LGZJL2Q7.js.map +1 -0
  72. package/dist/chunk-LQPBU46R.js +109 -0
  73. package/dist/chunk-LQPBU46R.js.map +1 -0
  74. package/dist/chunk-M55NNUTY.js +1013 -0
  75. package/dist/chunk-M55NNUTY.js.map +1 -0
  76. package/dist/chunk-M6NCYCC3.js +1013 -0
  77. package/dist/chunk-M6NCYCC3.js.map +1 -0
  78. package/dist/chunk-MGN7OZAU.js +109 -0
  79. package/dist/chunk-MGN7OZAU.js.map +1 -0
  80. package/dist/chunk-MGYZOX4K.js +1013 -0
  81. package/dist/chunk-MGYZOX4K.js.map +1 -0
  82. package/dist/chunk-MY4IEKJS.js +1031 -0
  83. package/dist/chunk-MY4IEKJS.js.map +1 -0
  84. package/dist/chunk-NCQRGAB4.js +705 -0
  85. package/dist/chunk-NCQRGAB4.js.map +1 -0
  86. package/dist/chunk-NI4NHXI4.js +697 -0
  87. package/dist/chunk-NI4NHXI4.js.map +1 -0
  88. package/dist/chunk-NPI5N7AU.js +14 -0
  89. package/dist/chunk-NPI5N7AU.js.map +1 -0
  90. package/dist/chunk-QN4CPMJF.js +126 -0
  91. package/dist/chunk-QN4CPMJF.js.map +1 -0
  92. package/dist/chunk-QYUVAKMC.js +1013 -0
  93. package/dist/chunk-QYUVAKMC.js.map +1 -0
  94. package/dist/chunk-RAAT6PDT.js +25 -0
  95. package/dist/chunk-RAAT6PDT.js.map +1 -0
  96. package/dist/chunk-RDPO5VPS.js +1013 -0
  97. package/dist/chunk-RDPO5VPS.js.map +1 -0
  98. package/dist/chunk-RQHIW2PF.js +25 -0
  99. package/dist/chunk-RQHIW2PF.js.map +1 -0
  100. package/dist/chunk-RREVHA52.js +722 -0
  101. package/dist/chunk-RREVHA52.js.map +1 -0
  102. package/dist/chunk-S7PT4PZN.js +25 -0
  103. package/dist/chunk-S7PT4PZN.js.map +1 -0
  104. package/dist/chunk-SUXV5M32.js +109 -0
  105. package/dist/chunk-SUXV5M32.js.map +1 -0
  106. package/dist/chunk-SZEORUY6.js +696 -0
  107. package/dist/chunk-SZEORUY6.js.map +1 -0
  108. package/dist/chunk-T7NGGWGY.js +25 -0
  109. package/dist/chunk-T7NGGWGY.js.map +1 -0
  110. package/dist/chunk-UEVWP5VE.js +696 -0
  111. package/dist/chunk-UEVWP5VE.js.map +1 -0
  112. package/dist/chunk-UJNKNP7U.js +748 -0
  113. package/dist/chunk-UJNKNP7U.js.map +1 -0
  114. package/dist/chunk-V4XVFWFI.js +1013 -0
  115. package/dist/chunk-V4XVFWFI.js.map +1 -0
  116. package/dist/chunk-VJU2CIKZ.js +729 -0
  117. package/dist/chunk-VJU2CIKZ.js.map +1 -0
  118. package/dist/chunk-VRQUP5L3.js +202 -0
  119. package/dist/chunk-VRQUP5L3.js.map +1 -0
  120. package/dist/chunk-WBJ5E7CL.js +742 -0
  121. package/dist/chunk-WBJ5E7CL.js.map +1 -0
  122. package/dist/chunk-XJPVYMZM.js +24 -0
  123. package/dist/chunk-XJPVYMZM.js.map +1 -0
  124. package/dist/chunk-Y3Q65C7R.js +14 -0
  125. package/dist/chunk-Y3Q65C7R.js.map +1 -0
  126. package/dist/chunk-YHEGFPTR.js +1030 -0
  127. package/dist/chunk-YHEGFPTR.js.map +1 -0
  128. package/dist/chunk-ZDLBN46C.js +697 -0
  129. package/dist/chunk-ZDLBN46C.js.map +1 -0
  130. package/dist/dev/index.d.ts +122 -0
  131. package/dist/dev/index.js +662 -0
  132. package/dist/dev/index.js.map +1 -0
  133. package/dist/fields/collections.d.ts +18 -0
  134. package/dist/fields/collections.js +37 -0
  135. package/dist/fields/collections.js.map +1 -0
  136. package/dist/index.d.ts +235 -0
  137. package/dist/index.js +956 -0
  138. package/dist/index.js.map +1 -0
  139. package/dist/middleware/index.d.ts +8 -0
  140. package/dist/middleware/index.js +13 -0
  141. package/dist/middleware/index.js.map +1 -0
  142. package/dist/preview-BEkLMD6N.d.ts +23 -0
  143. package/dist/preview-D9yewK8x.d.ts +24 -0
  144. package/dist/preview-DJEA4fj7.d.ts +23 -0
  145. package/dist/preview-DeD1Jn7U.d.ts +23 -0
  146. package/dist/toolbar/app.d.ts +28 -0
  147. package/dist/toolbar/app.js +1176 -0
  148. package/dist/toolbar/app.js.map +1 -0
  149. package/dist/types-15jfCQKb.d.ts +141 -0
  150. package/dist/types-BREdL9Gx.d.ts +140 -0
  151. package/dist/types-C6sUZNa2.d.ts +104 -0
  152. package/dist/types-CEYS5Fzc.d.ts +112 -0
  153. package/dist/types-DpeHpobR.d.ts +104 -0
  154. package/dist/types-NaKJe6Pg.d.ts +140 -0
  155. package/package.json +90 -0
  156. package/src/CmsHead.astro +25 -0
  157. package/src/CmsImage.astro +62 -0
  158. package/src/CmsVideo.astro +185 -0
  159. package/src/RichText.astro +32 -0
  160. package/src/client.test.ts +227 -0
  161. package/src/client.ts +164 -0
  162. package/src/dev/config.test.ts +258 -0
  163. package/src/dev/config.ts +319 -0
  164. package/src/dev/index.ts +5 -0
  165. package/src/dev/preview-token.ts +39 -0
  166. package/src/dev/routes.test.ts +354 -0
  167. package/src/dev/routes.ts +245 -0
  168. package/src/dev/token.test.ts +280 -0
  169. package/src/dev/token.ts +201 -0
  170. package/src/dev/verifier.test.ts +63 -0
  171. package/src/dev/verifier.ts +161 -0
  172. package/src/fields/attrs.ts +24 -0
  173. package/src/fields/collection.ts +72 -0
  174. package/src/fields/collections.ts +35 -0
  175. package/src/fields/entryScope.test.ts +62 -0
  176. package/src/fields/entryScope.ts +240 -0
  177. package/src/fields/group.ts +51 -0
  178. package/src/fields/helpers.test.ts +51 -0
  179. package/src/fields/helpers.ts +214 -0
  180. package/src/fields/index.ts +46 -0
  181. package/src/fields/scope.ts +21 -0
  182. package/src/fields/types.ts +119 -0
  183. package/src/head.test.ts +74 -0
  184. package/src/head.ts +123 -0
  185. package/src/image.test.ts +42 -0
  186. package/src/image.ts +64 -0
  187. package/src/index.test.ts +76 -0
  188. package/src/index.ts +13 -0
  189. package/src/integration.ts +218 -0
  190. package/src/jwt.ts +131 -0
  191. package/src/middleware/index.ts +4 -0
  192. package/src/middleware/preview.test.ts +184 -0
  193. package/src/middleware/preview.ts +206 -0
  194. package/src/preview/bridge-script.test.ts +683 -0
  195. package/src/preview/bridge-script.ts +763 -0
  196. package/src/preview/origins.ts +29 -0
  197. package/src/preview.ts +49 -0
  198. package/src/richtext/RichTextNode.astro +130 -0
  199. package/src/richtext/render.ts +248 -0
  200. package/src/richtext.test.ts +139 -0
  201. package/src/runtime/bundle.ts +28 -0
  202. package/src/runtime/store.ts +130 -0
  203. package/src/siteFiles.test.ts +93 -0
  204. package/src/siteFiles.ts +111 -0
  205. package/src/toolbar/app.test.ts +191 -0
  206. package/src/toolbar/app.ts +1091 -0
  207. package/src/toolbar/icon.ts +5 -0
  208. package/src/toolbar/widgets.test.ts +153 -0
  209. package/src/toolbar/widgets.ts +365 -0
  210. package/src/types.test.ts +154 -0
  211. package/src/types.ts +163 -0
  212. package/src/version.ts +3 -0
@@ -0,0 +1,24 @@
1
+ export function cmsFieldAttr(key: string): { 'data-cms-field': string } {
2
+ return { 'data-cms-field': key };
3
+ }
4
+
5
+ export function cmsGroupAttr(groupName: string, prefix: string): { 'data-cms-group': string } {
6
+ return { 'data-cms-group': `${groupName}:${prefix}` };
7
+ }
8
+
9
+ export function cmsEntryAttr(
10
+ collectionKey: string,
11
+ slug?: string,
12
+ ): { 'data-cms-collection': string; 'data-cms-entry'?: string } {
13
+ if (slug) {
14
+ return {
15
+ 'data-cms-collection': collectionKey,
16
+ 'data-cms-entry': slug,
17
+ };
18
+ }
19
+ return { 'data-cms-collection': collectionKey };
20
+ }
21
+
22
+ export function cmsEntryFieldAttr(path: string): { 'data-cms-entry-field': string } {
23
+ return { 'data-cms-entry-field': path };
24
+ }
@@ -0,0 +1,72 @@
1
+ import { type z, zodToJsonSchema } from '@407dev/blocks';
2
+ import { bundleReady } from '../runtime/bundle';
3
+ import { getActiveBundle, isDraftMode } from '../runtime/store';
4
+ import type {
5
+ CollectionEntry,
6
+ CollectionHandle,
7
+ CollectionStaticPath,
8
+ DefineCollectionOptions,
9
+ InferEntryContent,
10
+ } from './types';
11
+
12
+ export function defineCollection<TSchema extends z.ZodTypeAny>(
13
+ options: DefineCollectionOptions<TSchema>,
14
+ ): CollectionHandle<TSchema> {
15
+ const jsonSchema = zodToJsonSchema(options.schema);
16
+
17
+ return {
18
+ key: options.key,
19
+ schema: options.schema,
20
+ route: options.route,
21
+ presentation: options.presentation,
22
+ titleField: options.titleField,
23
+ listColumns: options.listColumns,
24
+ bodyField: options.bodyField,
25
+ orderable: options.orderable,
26
+ seed: options.seed,
27
+ jsonSchema: jsonSchema as Record<string, unknown>,
28
+ schemaVersion: 1,
29
+ };
30
+ }
31
+
32
+ export function getEntries<T extends CollectionHandle<z.ZodTypeAny> | string>(
33
+ handleOrKey: T,
34
+ ): Array<CollectionEntry<InferEntryContent<T>>> {
35
+ const collectionKey = typeof handleOrKey === 'string' ? handleOrKey : handleOrKey.key;
36
+ const bundle = getActiveBundle();
37
+ const rawEntries = bundle?.entries?.[collectionKey] || [];
38
+ const draft = isDraftMode();
39
+
40
+ const filtered = draft ? rawEntries : rawEntries.filter((e) => e.status === 'published');
41
+
42
+ return filtered as unknown as Array<CollectionEntry<InferEntryContent<T>>>;
43
+ }
44
+
45
+ export function getEntry<T extends CollectionHandle<z.ZodTypeAny> | string>(
46
+ handleOrKey: T,
47
+ slug: string,
48
+ ): CollectionEntry<InferEntryContent<T>> | null {
49
+ const entries = getEntries(handleOrKey);
50
+ const found = entries.find((e) => e.slug === slug);
51
+ return (found || null) as CollectionEntry<InferEntryContent<T>> | null;
52
+ }
53
+
54
+ export async function collectionPaths<T extends CollectionHandle<z.ZodTypeAny> | string>(
55
+ handleOrKey: T,
56
+ ): Promise<Array<CollectionStaticPath<InferEntryContent<T>>>> {
57
+ const bundle = getActiveBundle() || (await bundleReady);
58
+ const collectionKey = typeof handleOrKey === 'string' ? handleOrKey : handleOrKey.key;
59
+ const rawEntries = bundle?.entries?.[collectionKey] || [];
60
+
61
+ // Build static paths only for published entries
62
+ const publishedEntries = rawEntries.filter((e) => e.status === 'published');
63
+
64
+ return publishedEntries.map((entry) => ({
65
+ params: {
66
+ slug: entry.slug,
67
+ },
68
+ props: {
69
+ entry: entry as CollectionEntry<InferEntryContent<T>>,
70
+ },
71
+ }));
72
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from '@407dev/blocks';
2
+ import { defineCollection } from './collection';
3
+
4
+ export function defineEmailCampaignCollection() {
5
+ return defineCollection({
6
+ key: 'email.campaign',
7
+ presentation: 'form',
8
+ titleField: 'subject',
9
+ listColumns: ['preheader'],
10
+ bodyField: 'body',
11
+ schema: z.object({
12
+ subject: z.string(),
13
+ preheader: z.string().optional(),
14
+ body: z.any(),
15
+ cta: z
16
+ .object({
17
+ text: z.string(),
18
+ url: z.string(),
19
+ })
20
+ .optional(),
21
+ }),
22
+ });
23
+ }
24
+
25
+ export function defineSmsCampaignCollection() {
26
+ return defineCollection({
27
+ key: 'sms.campaign',
28
+ presentation: 'form',
29
+ titleField: 'name',
30
+ schema: z.object({
31
+ name: z.string(),
32
+ body: z.string(),
33
+ }),
34
+ });
35
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { runWithDraftBundle } from '../runtime/store';
3
+ import type { ContentBundle } from '../types';
4
+ import { cmsEntryAttr, cmsEntryFieldAttr } from './attrs';
5
+ import { createEntryScope } from './entryScope';
6
+
7
+ const mockBundle: ContentBundle = {
8
+ bundleVersion: 1,
9
+ site_id: 'test-site',
10
+ fields: {},
11
+ collections: {
12
+ blog: {
13
+ presentation: 'visual',
14
+ orderable: false,
15
+ schema_version: 1,
16
+ },
17
+ },
18
+ entries: {
19
+ blog: [
20
+ {
21
+ slug: 'first-post',
22
+ content: {
23
+ title: 'First Post Title',
24
+ description: 'A blog post description',
25
+ meta: {
26
+ seo_title: 'SEO Title for Post',
27
+ },
28
+ },
29
+ status: 'published',
30
+ },
31
+ ],
32
+ },
33
+ routes: [],
34
+ settings: { robots: {}, default_seo: {}, sitemap: {} },
35
+ redirects: [],
36
+ };
37
+
38
+ describe('entryScope', () => {
39
+ it('returns proper attrs for entry container', () => {
40
+ const scope = createEntryScope('blog', 'first-post');
41
+ expect(scope.attrs()).toEqual({
42
+ 'data-cms-collection': 'blog',
43
+ 'data-cms-entry': 'first-post',
44
+ });
45
+ });
46
+
47
+ it('provides cmsEntryFieldAttr helper', () => {
48
+ expect(cmsEntryFieldAttr('title')).toEqual({
49
+ 'data-cms-entry-field': 'title',
50
+ });
51
+ });
52
+
53
+ it('reads fields synchronously from draft bundle', async () => {
54
+ await runWithDraftBundle(mockBundle, async () => {
55
+ const scope = createEntryScope('blog', 'first-post');
56
+ expect(scope.text('title')).toBe('First Post Title');
57
+ expect(scope.text('description')).toBe('A blog post description');
58
+ expect(scope.text('meta.seo_title')).toBe('SEO Title for Post');
59
+ expect(scope.text('nonexistent', { defaultValue: 'Fallback' })).toBe('Fallback');
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,240 @@
1
+ import type { z } from '@407dev/blocks';
2
+ import { getFieldType, listFieldTypes } from '@407dev/field-types';
3
+ import {
4
+ createComponent,
5
+ markHTMLString,
6
+ renderTemplate,
7
+ spreadAttributes,
8
+ } from 'astro/runtime/server/index.js';
9
+ import { renderRichTextHtml } from '../richtext/render';
10
+ import { getActiveBundle } from '../runtime/store';
11
+ import { cmsEntryAttr } from './attrs';
12
+ import { getPlaceholderForType } from './helpers';
13
+ import type {
14
+ CollectionHandle,
15
+ DualCallableHelper,
16
+ FieldComponentProps,
17
+ FieldReadOptions,
18
+ } from './types';
19
+
20
+ const DEFAULT_TAGS: Record<string, string> = {
21
+ text: 'span',
22
+ number: 'span',
23
+ toggle: 'span',
24
+ select: 'span',
25
+ image: 'img',
26
+ img: 'img',
27
+ video: 'video',
28
+ vid: 'video',
29
+ richtext: 'div',
30
+ richText: 'div',
31
+ };
32
+
33
+ function getNestedValue(obj: unknown, path: string): unknown {
34
+ if (!obj || typeof obj !== 'object' || !path) return undefined;
35
+ if (path in (obj as Record<string, unknown>)) {
36
+ return (obj as Record<string, unknown>)[path];
37
+ }
38
+ const parts = path.split('.');
39
+ let current: unknown = obj;
40
+ for (const part of parts) {
41
+ if (current === null || current === undefined || typeof current !== 'object') {
42
+ return undefined;
43
+ }
44
+ current = (current as Record<string, unknown>)[part];
45
+ }
46
+ return current;
47
+ }
48
+
49
+ export function createEntryDualCallableHelper<TValue = unknown, TDefault = TValue>(
50
+ typeId: string,
51
+ publicName: string,
52
+ collectionKey: string,
53
+ slug: string,
54
+ defaultPath = '',
55
+ ): DualCallableHelper<TValue, TDefault> {
56
+ const typeDef = getFieldType(typeId) || getFieldType(publicName);
57
+ const defaultTag =
58
+ DEFAULT_TAGS[typeId] || DEFAULT_TAGS[publicName] || typeDef?.renderHint?.tag || 'span';
59
+
60
+ // biome-ignore lint/suspicious/noExplicitAny: Astro runtime internal component factory
61
+ const componentFactory = createComponent(async ($$result: any, $$props: any, _slots: any) => {
62
+ const rawPath = ($$props?.k ?? $$props?.key ?? defaultPath) as string;
63
+ const tag = ($$props?.as || $$props?.tag || defaultTag) as string;
64
+ const defaultValue = $$props?.defaultValue as TDefault | undefined;
65
+
66
+ const bundle = getActiveBundle();
67
+ const entries = bundle?.entries?.[collectionKey] || [];
68
+ const entry = entries.find((e) => e.slug === slug);
69
+ const rawVal = entry?.content ? getNestedValue(entry.content, rawPath) : undefined;
70
+
71
+ let displayVal: unknown = rawVal;
72
+ if (displayVal === undefined || displayVal === null) {
73
+ if (defaultValue !== undefined) {
74
+ displayVal = defaultValue;
75
+ } else {
76
+ displayVal = getPlaceholderForType(typeId, rawPath);
77
+ }
78
+ }
79
+
80
+ const {
81
+ k: _k,
82
+ key: _key,
83
+ defaultValue: _def,
84
+ as: _as,
85
+ tag: _tag,
86
+ components: _comp,
87
+ ...htmlProps
88
+ } = ($$props || {}) as FieldComponentProps<TDefault>;
89
+
90
+ const attrs: Record<string, unknown> = {
91
+ ...htmlProps,
92
+ };
93
+ if (rawPath) {
94
+ attrs['data-cms-entry-field'] = rawPath;
95
+ }
96
+ if (!attrs['data-cms-kind']) {
97
+ attrs['data-cms-kind'] = typeId;
98
+ }
99
+
100
+ if (typeId === 'image' || publicName === 'img') {
101
+ if (displayVal && typeof displayVal === 'object') {
102
+ const asset = displayVal as {
103
+ url?: string;
104
+ alt?: string;
105
+ width?: number;
106
+ height?: number;
107
+ };
108
+ if (asset.url && !attrs.src) attrs.src = asset.url;
109
+ if (asset.alt && !attrs.alt) attrs.alt = asset.alt;
110
+ if (asset.width && !attrs.width) attrs.width = asset.width;
111
+ if (asset.height && !attrs.height) attrs.height = asset.height;
112
+ } else if (typeof displayVal === 'string' && displayVal) {
113
+ if (!attrs.src) attrs.src = displayVal;
114
+ }
115
+
116
+ return renderTemplate`<img${spreadAttributes(attrs)}>`;
117
+ }
118
+
119
+ if (typeId === 'video' || publicName === 'vid') {
120
+ if (displayVal && typeof displayVal === 'object') {
121
+ const asset = displayVal as {
122
+ url?: string;
123
+ poster?: string;
124
+ width?: number;
125
+ height?: number;
126
+ };
127
+ if (asset.url && !attrs.src) attrs.src = asset.url;
128
+ if (asset.poster && !attrs.poster) attrs.poster = asset.poster;
129
+ if (asset.width && !attrs.width) attrs.width = asset.width;
130
+ if (asset.height && !attrs.height) attrs.height = asset.height;
131
+ } else if (typeof displayVal === 'string' && displayVal) {
132
+ if (!attrs.src) attrs.src = displayVal;
133
+ }
134
+ if (attrs.controls === undefined) attrs.controls = true;
135
+
136
+ return renderTemplate`<video${spreadAttributes(attrs)}></video>`;
137
+ }
138
+
139
+ if (typeId === 'richtext' || publicName === 'richText') {
140
+ const html = renderRichTextHtml(displayVal, { components: $$props?.components });
141
+ return renderTemplate`<${tag}${spreadAttributes(attrs)}>${markHTMLString(html)}</${tag}>`;
142
+ }
143
+
144
+ return renderTemplate`<${tag}${spreadAttributes(attrs)}>${
145
+ displayVal !== null && displayVal !== undefined ? String(displayVal) : ''
146
+ }</${tag}>`;
147
+ });
148
+
149
+ const fn = (arg0?: unknown, ...rest: unknown[]): unknown => {
150
+ const isComponentCall =
151
+ arg0 &&
152
+ typeof arg0 === 'object' &&
153
+ 'styles' in (arg0 as Record<string, unknown>) &&
154
+ 'scripts' in (arg0 as Record<string, unknown>);
155
+
156
+ if (!isComponentCall) {
157
+ let rawPath = defaultPath;
158
+ let options: FieldReadOptions<TDefault> = {};
159
+
160
+ if (typeof arg0 === 'string') {
161
+ rawPath = arg0;
162
+ options = (rest[0] || {}) as FieldReadOptions<TDefault>;
163
+ } else if (arg0 && typeof arg0 === 'object') {
164
+ options = arg0 as FieldReadOptions<TDefault>;
165
+ }
166
+
167
+ const bundle = getActiveBundle();
168
+ const entries = bundle?.entries?.[collectionKey] || [];
169
+ const entry = entries.find((e) => e.slug === slug);
170
+ const val = entry?.content ? getNestedValue(entry.content, rawPath) : undefined;
171
+
172
+ if (val !== undefined && val !== null) {
173
+ return val as TValue;
174
+ }
175
+ if (options.defaultValue !== undefined) {
176
+ return options.defaultValue as unknown as TValue;
177
+ }
178
+ return getPlaceholderForType(typeId, rawPath) as TValue;
179
+ }
180
+
181
+ return componentFactory(arg0, rest[0], rest[1]);
182
+ };
183
+
184
+ Object.assign(fn, {
185
+ isAstroComponentFactory: true,
186
+ typeId,
187
+ publicName,
188
+ collectionKey,
189
+ slug,
190
+ defaultPath,
191
+ });
192
+
193
+ return fn as unknown as DualCallableHelper<TValue, TDefault>;
194
+ }
195
+
196
+ export type EntryScopeAccessor = {
197
+ attrs(): { 'data-cms-collection': string; 'data-cms-entry'?: string };
198
+ collectionKey: string;
199
+ slug: string;
200
+ text: DualCallableHelper<string>;
201
+ number: DualCallableHelper<number>;
202
+ toggle: DualCallableHelper<boolean>;
203
+ select: DualCallableHelper<string>;
204
+ img: DualCallableHelper<unknown>;
205
+ image: DualCallableHelper<unknown>;
206
+ richText: DualCallableHelper<unknown>;
207
+ richtext: DualCallableHelper<unknown>;
208
+ // biome-ignore lint/suspicious/noExplicitAny: generic index signature for arbitrary entry helpers
209
+ [key: string]: any;
210
+ };
211
+
212
+ export function createEntryScope(
213
+ handleOrKey: CollectionHandle<z.ZodTypeAny> | string,
214
+ slug: string,
215
+ ): EntryScopeAccessor {
216
+ const collectionKey = typeof handleOrKey === 'string' ? handleOrKey : handleOrKey.key;
217
+
218
+ const boundHelpers: Record<string, DualCallableHelper<unknown, unknown>> = {};
219
+ const types = listFieldTypes();
220
+
221
+ for (const def of types) {
222
+ boundHelpers[def.id] = createEntryDualCallableHelper(def.id, def.id, collectionKey, slug);
223
+ if (def.aliases) {
224
+ for (const alias of def.aliases) {
225
+ boundHelpers[alias] = createEntryDualCallableHelper(def.id, alias, collectionKey, slug);
226
+ }
227
+ }
228
+ }
229
+
230
+ const accessor = {
231
+ ...boundHelpers,
232
+ attrs: () => cmsEntryAttr(collectionKey, slug),
233
+ collectionKey,
234
+ slug,
235
+ };
236
+
237
+ return accessor as EntryScopeAccessor;
238
+ }
239
+
240
+ export const entryScope = createEntryScope;
@@ -0,0 +1,51 @@
1
+ import { cmsGroupAttr } from './attrs';
2
+ import { createDualCallableHelper } from './helpers';
3
+ import type { BoundGroupAccessor, DualCallableHelper, GroupDefinition } from './types';
4
+
5
+ export function defineGroup<TShape extends Record<string, unknown>>(
6
+ nameOrShape: string | TShape,
7
+ shapeOrUndefined?: TShape,
8
+ ): GroupDefinition<TShape> {
9
+ const groupName = typeof nameOrShape === 'string' ? nameOrShape : 'Group';
10
+ const shape = (typeof nameOrShape === 'string' ? shapeOrUndefined || {} : nameOrShape) as TShape;
11
+
12
+ const groupDef = (scopeKey: string): BoundGroupAccessor<TShape> => {
13
+ const accessorFn = (key: string): string => {
14
+ return scopeKey ? `${scopeKey}.${key}` : key;
15
+ };
16
+
17
+ const boundHelpers: Record<string, DualCallableHelper<unknown, unknown>> = {};
18
+
19
+ for (const [key, helperDef] of Object.entries(shape)) {
20
+ const helperObj = helperDef as Record<string, unknown> | undefined;
21
+ const typeId = (helperObj?.typeId as string) || (helperObj?.publicName as string);
22
+
23
+ if (!typeId) {
24
+ throw new Error(
25
+ `f.group('${groupName}', ...) field '${key}' must reference a field helper (e.g. f.text, f.img), got ${typeof helperDef}`,
26
+ );
27
+ }
28
+
29
+ const publicName = (helperObj?.publicName as string) || typeId;
30
+ boundHelpers[key] = createDualCallableHelper(typeId, publicName, scopeKey, key);
31
+ }
32
+
33
+ Object.assign(accessorFn, boundHelpers, {
34
+ attrs: (): { 'data-cms-group': string } => cmsGroupAttr(groupName, scopeKey),
35
+ key: (key: string): string => (scopeKey ? `${scopeKey}.${key}` : key),
36
+ groupName,
37
+ prefix: scopeKey,
38
+ });
39
+
40
+ return accessorFn as BoundGroupAccessor<TShape>;
41
+ };
42
+
43
+ Object.assign(groupDef, {
44
+ groupName,
45
+ shape,
46
+ });
47
+
48
+ return groupDef as unknown as GroupDefinition<TShape>;
49
+ }
50
+
51
+ export const group = defineGroup;
@@ -0,0 +1,51 @@
1
+ import { registerFieldType } from '@407dev/field-types';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { generateAllFieldHelpers, getPlaceholderForType } from './helpers';
4
+
5
+ describe('packages/astro field helpers and recipe gate', () => {
6
+ it('generates all registered base types and aliases', () => {
7
+ const helpers = generateAllFieldHelpers();
8
+ expect(helpers.text).toBeDefined();
9
+ expect(helpers.text.typeId).toBe('text');
10
+ expect(helpers.img).toBeDefined();
11
+ expect(helpers.img.typeId).toBe('image');
12
+ expect(helpers.image).toBeDefined();
13
+ expect(helpers.image.typeId).toBe('image');
14
+ expect(helpers.richText).toBeDefined();
15
+ expect(helpers.richText.typeId).toBe('richtext');
16
+ expect(helpers.vid).toBeDefined();
17
+ expect(helpers.vid.typeId).toBe('video');
18
+ expect(helpers.video).toBeDefined();
19
+ expect(helpers.video.typeId).toBe('video');
20
+ expect(helpers.videoList).toBeDefined();
21
+ expect(helpers.videoList.typeId).toBe('media.videoList');
22
+ expect(helpers.streamVideo).toBeDefined();
23
+ expect(helpers.streamVideo.typeId).toBe('media.streamVideo');
24
+ expect(helpers.geoArea).toBeDefined();
25
+ expect(helpers.geoArea.typeId).toBe('geo.area');
26
+ expect(helpers.addressList).toBeDefined();
27
+ expect(helpers.addressList.typeId).toBe('address.list');
28
+ });
29
+
30
+ describe('Recipe gate: test.color automatic SDK exposure', () => {
31
+ it('generateAllFieldHelpers exposes testColor with typeId test.color without SDK edits', () => {
32
+ registerFieldType({
33
+ id: 'test.color',
34
+ aliases: ['testColor'],
35
+ defaultValue: '#112233',
36
+ constraintsSchema: {},
37
+ validate: () => ({ valid: true }),
38
+ });
39
+
40
+ const helpers = generateAllFieldHelpers();
41
+ expect(helpers['test.color']).toBeDefined();
42
+ expect(helpers['test.color'].typeId).toBe('test.color');
43
+
44
+ expect(helpers.testColor).toBeDefined();
45
+ expect(helpers.testColor.typeId).toBe('test.color');
46
+ expect(helpers.testColor.publicName).toBe('testColor');
47
+
48
+ expect(getPlaceholderForType('test.color', 'theme.color')).toBe('#112233');
49
+ });
50
+ });
51
+ });