@byline/cli 3.20.4 → 4.0.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 (198) hide show
  1. package/README.md +42 -2
  2. package/dist/cli.js +5 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/doctor.d.ts.map +1 -1
  5. package/dist/commands/doctor.js +1 -0
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/init.d.ts +3 -0
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +10 -14
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/setup-checks.d.ts +9 -0
  12. package/dist/commands/setup-checks.d.ts.map +1 -1
  13. package/dist/commands/setup-checks.js +43 -18
  14. package/dist/commands/setup-checks.js.map +1 -1
  15. package/dist/commands/setup.js +1 -1
  16. package/dist/commands/setup.js.map +1 -1
  17. package/dist/context.d.ts +2 -0
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/context.js +6 -0
  20. package/dist/context.js.map +1 -1
  21. package/dist/lib/dependency-version.d.ts +12 -0
  22. package/dist/lib/dependency-version.d.ts.map +1 -0
  23. package/dist/lib/dependency-version.js +101 -0
  24. package/dist/lib/dependency-version.js.map +1 -0
  25. package/dist/lib/planned-writes.d.ts +8 -0
  26. package/dist/lib/planned-writes.d.ts.map +1 -0
  27. package/dist/lib/planned-writes.js +177 -0
  28. package/dist/lib/planned-writes.js.map +1 -0
  29. package/dist/lib/planned-writes.test.d.ts +2 -0
  30. package/dist/lib/planned-writes.test.d.ts.map +1 -0
  31. package/dist/lib/planned-writes.test.js +113 -0
  32. package/dist/lib/planned-writes.test.js.map +1 -0
  33. package/dist/lib/release-policy.d.ts +12 -0
  34. package/dist/lib/release-policy.d.ts.map +1 -0
  35. package/dist/lib/release-policy.js +20 -0
  36. package/dist/lib/release-policy.js.map +1 -0
  37. package/dist/lib/release-policy.test.d.ts +2 -0
  38. package/dist/lib/release-policy.test.d.ts.map +1 -0
  39. package/dist/lib/release-policy.test.js +36 -0
  40. package/dist/lib/release-policy.test.js.map +1 -0
  41. package/dist/lib/route-config.d.ts +40 -0
  42. package/dist/lib/route-config.d.ts.map +1 -0
  43. package/dist/lib/route-config.js +322 -0
  44. package/dist/lib/route-config.js.map +1 -0
  45. package/dist/lib/static-config.d.ts +20 -0
  46. package/dist/lib/static-config.d.ts.map +1 -0
  47. package/dist/lib/static-config.js +444 -0
  48. package/dist/lib/static-config.js.map +1 -0
  49. package/dist/lib/static-config.test.d.ts +2 -0
  50. package/dist/lib/static-config.test.d.ts.map +1 -0
  51. package/dist/lib/static-config.test.js +105 -0
  52. package/dist/lib/static-config.test.js.map +1 -0
  53. package/dist/lib/template-hook-boundary.test.node.d.ts +2 -0
  54. package/dist/lib/template-hook-boundary.test.node.d.ts.map +1 -0
  55. package/dist/lib/template-hook-boundary.test.node.js +30 -0
  56. package/dist/lib/template-hook-boundary.test.node.js.map +1 -0
  57. package/dist/lib/template-path.d.ts +5 -0
  58. package/dist/lib/template-path.d.ts.map +1 -0
  59. package/dist/lib/template-path.js +9 -0
  60. package/dist/lib/template-path.js.map +1 -0
  61. package/dist/lib/workspace-root.d.ts +28 -0
  62. package/dist/lib/workspace-root.d.ts.map +1 -0
  63. package/dist/lib/workspace-root.js +205 -0
  64. package/dist/lib/workspace-root.js.map +1 -0
  65. package/dist/manifest/deps.d.ts +1 -1
  66. package/dist/manifest/deps.d.ts.map +1 -1
  67. package/dist/manifest/deps.js +18 -8
  68. package/dist/manifest/deps.js.map +1 -1
  69. package/dist/phases/deps.d.ts +14 -1
  70. package/dist/phases/deps.d.ts.map +1 -1
  71. package/dist/phases/deps.js +312 -137
  72. package/dist/phases/deps.js.map +1 -1
  73. package/dist/phases/deps.test.d.ts +2 -0
  74. package/dist/phases/deps.test.d.ts.map +1 -0
  75. package/dist/phases/deps.test.js +343 -0
  76. package/dist/phases/deps.test.js.map +1 -0
  77. package/dist/phases/preflight.d.ts.map +1 -1
  78. package/dist/phases/preflight.js +59 -11
  79. package/dist/phases/preflight.js.map +1 -1
  80. package/dist/phases/prompts.d.ts +2 -0
  81. package/dist/phases/prompts.d.ts.map +1 -1
  82. package/dist/phases/prompts.js +51 -2
  83. package/dist/phases/prompts.js.map +1 -1
  84. package/dist/phases/routes-ui.test.d.ts +2 -0
  85. package/dist/phases/routes-ui.test.d.ts.map +1 -0
  86. package/dist/phases/routes-ui.test.js +766 -0
  87. package/dist/phases/routes-ui.test.js.map +1 -0
  88. package/dist/phases/routes.d.ts +4 -1
  89. package/dist/phases/routes.d.ts.map +1 -1
  90. package/dist/phases/routes.js +610 -122
  91. package/dist/phases/routes.js.map +1 -1
  92. package/dist/phases/scaffold-smoke.test.d.ts +2 -0
  93. package/dist/phases/scaffold-smoke.test.d.ts.map +1 -0
  94. package/dist/phases/scaffold-smoke.test.js +168 -0
  95. package/dist/phases/scaffold-smoke.test.js.map +1 -0
  96. package/dist/phases/scaffold.d.ts +4 -1
  97. package/dist/phases/scaffold.d.ts.map +1 -1
  98. package/dist/phases/scaffold.js +275 -72
  99. package/dist/phases/scaffold.js.map +1 -1
  100. package/dist/phases/scaffold.test.d.ts +2 -0
  101. package/dist/phases/scaffold.test.d.ts.map +1 -0
  102. package/dist/phases/scaffold.test.js +178 -0
  103. package/dist/phases/scaffold.test.js.map +1 -0
  104. package/dist/phases/ui.d.ts +4 -1
  105. package/dist/phases/ui.d.ts.map +1 -1
  106. package/dist/phases/ui.js +51 -59
  107. package/dist/phases/ui.js.map +1 -1
  108. package/dist/phases/wire/index.d.ts.map +1 -1
  109. package/dist/phases/wire/index.js +10 -6
  110. package/dist/phases/wire/index.js.map +1 -1
  111. package/dist/phases/wire/shared.d.ts +4 -1
  112. package/dist/phases/wire/shared.d.ts.map +1 -1
  113. package/dist/phases/wire/vite-config.d.ts.map +1 -1
  114. package/dist/phases/wire/vite-config.js +69 -20
  115. package/dist/phases/wire/vite-config.js.map +1 -1
  116. package/dist/phases/wire/vite-config.test.d.ts +2 -0
  117. package/dist/phases/wire/vite-config.test.d.ts.map +1 -0
  118. package/dist/phases/wire/vite-config.test.js +97 -0
  119. package/dist/phases/wire/vite-config.test.js.map +1 -0
  120. package/dist/prompts.d.ts +1 -0
  121. package/dist/prompts.d.ts.map +1 -1
  122. package/dist/prompts.js +7 -2
  123. package/dist/prompts.js.map +1 -1
  124. package/dist/runner.js +1 -1
  125. package/dist/runner.js.map +1 -1
  126. package/dist/templates/byline/admin.config.ts +8 -7
  127. package/dist/templates/byline/clients.server.ts +77 -0
  128. package/dist/templates/byline/collection-types.contract.ts +67 -0
  129. package/dist/templates/byline/collections/index.ts +17 -0
  130. package/dist/templates/byline/collections/server-hooks.ts +7 -0
  131. package/dist/templates/byline/generated/collection-types.ts +11 -0
  132. package/dist/templates/byline/i18n.ts +3 -3
  133. package/dist/templates/byline/locales.ts +9 -9
  134. package/dist/templates/byline/public.ts +12 -0
  135. package/dist/templates/byline/routes.ts +7 -6
  136. package/dist/templates/byline/scripts/generate-types.ts +81 -0
  137. package/dist/templates/byline/server.config.ts +5 -5
  138. package/dist/templates/byline-examples/admin.config.ts +18 -21
  139. package/dist/templates/byline-examples/blocks/photo-block.ts +3 -2
  140. package/dist/templates/byline-examples/clients.server.ts +77 -0
  141. package/dist/templates/byline-examples/collection-types.contract.ts +67 -0
  142. package/dist/templates/byline-examples/collections/docs/hooks.ts +30 -20
  143. package/dist/templates/byline-examples/collections/docs/schema.ts +18 -18
  144. package/dist/templates/byline-examples/collections/index.ts +22 -0
  145. package/dist/templates/byline-examples/collections/media/components/media-list-view.tsx +11 -6
  146. package/dist/templates/byline-examples/collections/media/schema.ts +16 -0
  147. package/dist/templates/byline-examples/collections/news/schema.ts +17 -3
  148. package/dist/templates/byline-examples/collections/news-categories/schema.ts +13 -0
  149. package/dist/templates/byline-examples/collections/pages/schema.ts +13 -0
  150. package/dist/templates/byline-examples/collections/server-hooks.ts +8 -0
  151. package/dist/templates/byline-examples/fields/ai-text.ts +1 -1
  152. package/dist/templates/byline-examples/fields/ai-textarea.ts +1 -1
  153. package/dist/templates/byline-examples/fields/lexical-richtext-ai.tsx +3 -3
  154. package/dist/templates/byline-examples/generated/collection-types.ts +139 -0
  155. package/dist/templates/byline-examples/i18n.ts +3 -3
  156. package/dist/templates/byline-examples/locales.ts +9 -9
  157. package/dist/templates/byline-examples/public.ts +12 -0
  158. package/dist/templates/byline-examples/routes.ts +7 -6
  159. package/dist/templates/byline-examples/scripts/backfill-version-locales.ts +1 -1
  160. package/dist/templates/byline-examples/scripts/generate-types.ts +81 -0
  161. package/dist/templates/byline-examples/scripts/import-docs.ts +11 -12
  162. package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.test.node.ts +93 -233
  163. package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.ts +2 -2
  164. package/dist/templates/byline-examples/scripts/lib/strip-leading-h1.test.node.ts +0 -4
  165. package/dist/templates/byline-examples/scripts/re-anchor.ts +3 -3
  166. package/dist/templates/byline-examples/scripts/regenerate-media.ts +6 -5
  167. package/dist/templates/byline-examples/server.config.ts +6 -15
  168. package/dist/templates/host/vite.config.ts +2 -0
  169. package/dist/templates/routes/_byline/admin/collections/$collection/$id/api.tsx +1 -6
  170. package/dist/templates/routes/_byline/admin/collections/$collection/$id/history.tsx +1 -7
  171. package/dist/templates/routes/_byline/admin/collections/$collection/$id/index.tsx +1 -6
  172. package/dist/templates/routes/_byline/route.lazy.tsx +1 -2
  173. package/dist/templates/routes/_byline/route.tsx +1 -1
  174. package/dist/templates/ui-byline/blocks/photo-block/index.tsx +18 -23
  175. package/dist/templates/ui-byline/blocks/richtext-block/index.tsx +1 -1
  176. package/dist/templates/ui-byline/components/admonition/index.tsx +11 -8
  177. package/dist/templates/ui-byline/components/code/code.tsx +6 -4
  178. package/dist/templates/ui-byline/components/link/lang-link.tsx +4 -8
  179. package/dist/templates/ui-byline/components/link/link-field.tsx +3 -2
  180. package/dist/templates/ui-byline/components/link/link-lexical.tsx +9 -8
  181. package/dist/templates/ui-byline/render-blocks.tsx +38 -38
  182. package/dist/templates/ui-byline/types/content.ts +25 -0
  183. package/dist/templates/ui-byline/types/i18n.ts +4 -8
  184. package/dist/test-helpers.d.ts +5 -0
  185. package/dist/test-helpers.d.ts.map +1 -0
  186. package/dist/test-helpers.js +52 -0
  187. package/dist/test-helpers.js.map +1 -0
  188. package/dist/types.d.ts +21 -1
  189. package/dist/types.d.ts.map +1 -1
  190. package/dist/ui-template-contract.test.d.ts +2 -0
  191. package/dist/ui-template-contract.test.d.ts.map +1 -0
  192. package/dist/ui-template-contract.test.js +130 -0
  193. package/dist/ui-template-contract.test.js.map +1 -0
  194. package/dist/workspace-root.test.d.ts +2 -0
  195. package/dist/workspace-root.test.d.ts.map +1 -0
  196. package/dist/workspace-root.test.js +307 -0
  197. package/dist/workspace-root.test.js.map +1 -0
  198. package/package.json +9 -1
@@ -5,16 +5,14 @@ import type React from 'react'
5
5
  import { Container, FadeInLift } from '@byline/ui/react'
6
6
  import cx from 'classnames'
7
7
 
8
- import type { PhotoBlockData } from '~/blocks/photo-block'
9
- import type { MediaFields } from '~/collections/media/schema'
10
-
11
8
  import { ResponsiveImage } from '@/ui/byline/components/responsive-image'
12
9
  import { LexicalRichText } from '@/ui/byline/components/richtext-lexical'
10
+ import type { PopulatedPhotoBlockData } from '@/ui/byline/types/content'
13
11
  import type { Locale } from '@/ui/byline/types/i18n'
14
12
 
15
13
  interface Props {
16
14
  id: string
17
- block: PhotoBlockData
15
+ block: PopulatedPhotoBlockData
18
16
  lng: Locale
19
17
  constrainedLayout?: boolean
20
18
  className?: string
@@ -29,27 +27,26 @@ export function PhotoBlock({
29
27
  }: Props): React.JSX.Element | null {
30
28
  const { photo, alt, caption, display } = block
31
29
 
32
- // The `photo` relation must be populated (`populate: { photo: '*' }`) for
33
- // the front-end to render an image. When populated, the value is a
34
- // `PopulatedRelationValue` envelope — `{ ...rel, _resolved: true,
35
- // document: <MediaFields> }`. Bail out if the relation is missing or
36
- // unpopulated.
37
- if (photo == null || (photo as { _resolved?: boolean })._resolved !== true) {
38
- return null
39
- }
40
- const media = (photo as unknown as { document: MediaFields }).document
30
+ // Missing, unresolved, and cycle-suppressed relations have no document.
31
+ const media = photo?.document?.fields
32
+ if (media == null) return null
41
33
 
42
34
  const Comp = display === 'full_width' ? 'div' : Container
43
35
 
44
- // TODO: caption is a Lexical richText field — type properly once the
45
- // Lexical node shape is modelled in @byline/core.
46
- const captionDoc = caption as Record<string, any> | undefined
36
+ const captionRoot =
37
+ caption != null && !Array.isArray(caption) && typeof caption === 'object'
38
+ ? caption.root
39
+ : undefined
40
+ const captionChildren =
41
+ captionRoot != null && !Array.isArray(captionRoot) && typeof captionRoot === 'object'
42
+ ? captionRoot.children
43
+ : undefined
47
44
 
48
45
  return (
49
46
  <Comp
50
47
  id={id}
51
48
  className={cx(
52
- 'px-0 pt-4',
49
+ 'px-0',
53
50
  {
54
51
  'lg:max-w-[920px] xl:max-w-[920px] 2xl:max-w-[920px] mx-auto': display === 'default',
55
52
  },
@@ -65,12 +62,10 @@ export function PhotoBlock({
65
62
  className="photo-block"
66
63
  imgClassName="photo-block--photo"
67
64
  />
68
- {captionDoc != null && (
69
- <Container
70
- className={cx('bg-white dark:bg-canvas-900 py-2', { 'px-0': constrainedLayout })}
71
- >
72
- <div className="photo-block--caption">
73
- <LexicalRichText lng={lng} nodes={captionDoc?.root?.children} />
65
+ {captionChildren != null && (
66
+ <Container className={cx('py-2', { 'px-0': constrainedLayout })}>
67
+ <div className="photo-block--caption [&_p]:m-0 muted text-[1rem]">
68
+ <LexicalRichText lng={lng} nodes={captionChildren} />
74
69
  </div>
75
70
  </Container>
76
71
  )}
@@ -3,7 +3,7 @@
3
3
  import { Container } from '@byline/ui/react'
4
4
  import cx from 'classnames'
5
5
 
6
- import type { RichTextBlockData } from '~/blocks/richtext-block'
6
+ import type { RichTextBlockData } from '~/generated/collection-types.js'
7
7
 
8
8
  import { LexicalRichText } from '@/ui/byline/components/richtext-lexical'
9
9
  import type { Locale } from '@/ui/byline/types/i18n'
@@ -1,4 +1,4 @@
1
- 'use clients'
1
+ 'use client'
2
2
 
3
3
  import type React from 'react'
4
4
 
@@ -28,13 +28,16 @@ export function AdmonitionSerializer({
28
28
  options: SerializeOptions
29
29
  }): React.JSX.Element {
30
30
  const intent = intentMap[node.admonitionType as keyof typeof intentMap]
31
+ // Admonitions are ElementNodes, so their body is stored directly in children.
31
32
  return (
32
- <Alert title={node.title} intent={intent} close={false} className="rounded-none">
33
- {node?.content?.editorState?.root?.children != null ? (
34
- serialize({ nodes: node?.content?.editorState?.root?.children, lng, options })
35
- ) : (
36
- <span>Content not found for admonition.</span>
37
- )}
38
- </Alert>
33
+ <div className="not-prose pt-6 pb-8">
34
+ <Alert title={node.title} intent={intent} close={false} className="not-prose my-0">
35
+ {node.children != null ? (
36
+ serialize({ nodes: node.children, lng, options })
37
+ ) : (
38
+ <span>Content not found for admonition.</span>
39
+ )}
40
+ </Alert>
41
+ </div>
39
42
  )
40
43
  }
@@ -10,14 +10,16 @@ interface CodeProps extends CodeIntrinsicProps {
10
10
  className?: string
11
11
  title?: string
12
12
  code: string
13
- language?: string
13
+ language?: string | null
14
14
  }
15
15
 
16
- export function Code({ code, className, language = 'jsx' }: CodeProps): React.JSX.Element {
16
+ export function Code({ code, className, language }: CodeProps): React.JSX.Element {
17
+ // Prism calls `.toLowerCase()` on this value, so null and empty legacy values need a fallback.
18
+ const resolvedLanguage = language != null && language.length > 0 ? language : 'typescript'
17
19
  return (
18
- <Highlight theme={themes.oneDark} code={code} language={language}>
20
+ <Highlight theme={themes.oneDark} code={code} language={resolvedLanguage}>
19
21
  {({ tokens, getLineProps, getTokenProps }) => (
20
- <div className="code scroller group overflow-y-auto rounded border border-theme-600 relative">
22
+ <div className="code scroller group overflow-y-auto rounded border border-theme-600 relative mb-4">
21
23
  <CopyButton
22
24
  variant="outlined"
23
25
  intent="primary"
@@ -1,17 +1,13 @@
1
1
  /**
2
2
  * Locale-aware Link STUB.
3
3
  *
4
- * The reference webapp ships a real `LangLink` that injects an optional
5
- * `{-$lng}` path parameter onto TanStack Router's `<Link>` so that the
6
- * current locale survives client-side navigation. This stub is a thin
7
- * pass-through — it accepts the `lng` prop but does NOT prefix URLs.
4
+ * This stub is a thin pass-through: it accepts the `lng` prop but does not
5
+ * prefix URLs. A localized app can replace the body with a wrapper that
6
+ * injects its locale path parameter into TanStack Router's `<Link>`.
8
7
  *
9
8
  * If your app is single-locale, this is fine as-is.
10
9
  *
11
- * If you have a multi-locale strategy, replace the body with your own
12
- * Link wrapper. The reference implementation lives at
13
- * apps/webapp/src/i18n/components/lang-link.tsx
14
- * in https://github.com/Byline-CMS/bylinecms.dev.
10
+ * If you have a multi-locale strategy, replace the body with your own Link wrapper.
15
11
  */
16
12
 
17
13
  import type React from 'react'
@@ -84,14 +84,15 @@ function getHref(args: LinkFieldAttributes): string {
84
84
  }
85
85
 
86
86
  const hrefIsLocal = ['tel:', 'mailto:', '/'].some((prefix) => href.startsWith(prefix))
87
- if (!hrefIsLocal) {
87
+ // Relative custom hrefs are valid but cannot be passed to URL without a base.
88
+ if (!hrefIsLocal && /^[a-z][a-z\d+.-]*:/i.test(href)) {
88
89
  try {
89
90
  const objectURL = new URL(href)
90
91
  if (objectURL.origin === publicWebsiteUrl) {
91
92
  href = objectURL.href.replace(publicWebsiteUrl, '')
92
93
  }
93
94
  } catch (e) {
94
- console.error(`Failed to format url: ${href}`, e) // eslint-disable-line no-console
95
+ console.error(`Failed to format url: ${href}`, e)
95
96
  }
96
97
  }
97
98
 
@@ -7,8 +7,6 @@ import cx from 'classnames'
7
7
  import { LangLink } from '@/ui/byline/components/link/lang-link'
8
8
  import type { Locale } from '@/ui/byline/types/i18n'
9
9
 
10
- // import { getPublicWebsiteUrl } from '@/utils/utils.framework.ts'
11
-
12
10
  interface BaseLinkAttributes {
13
11
  newTab?: boolean
14
12
  nofollow?: boolean
@@ -134,13 +132,16 @@ function getHref(args: LinkAttributes): string {
134
132
  }
135
133
 
136
134
  if (!isLocalHref(href)) {
137
- try {
138
- const objectURL = new URL(href)
139
- if (objectURL.origin === publicWebsiteUrl) {
140
- href = objectURL.href.replace(publicWebsiteUrl, '')
135
+ // Relative custom hrefs are valid but cannot be passed to URL without a base.
136
+ if (/^[a-z][a-z\d+.-]*:/i.test(href)) {
137
+ try {
138
+ const objectURL = new URL(href)
139
+ if (objectURL.origin === publicWebsiteUrl) {
140
+ href = objectURL.href.replace(publicWebsiteUrl, '')
141
+ }
142
+ } catch (e) {
143
+ console.error(`Failed to format url: ${href}`, e)
141
144
  }
142
- } catch (e) {
143
- console.error(`Failed to format url: ${href}`, e) // eslint-disable-line no-console
144
145
  }
145
146
  }
146
147
 
@@ -1,45 +1,13 @@
1
- import type { BlocksUnion } from '@byline/core'
2
1
  import { Section } from '@byline/ui/react'
3
2
 
4
- import { PhotoBlock as PhotoBlockDef } from '~/blocks/photo-block'
5
- import { RichTextBlock as RichTextBlockDef } from '~/blocks/richtext-block'
6
-
7
3
  import { PhotoBlock } from '@/ui/byline/blocks/photo-block'
8
4
  import { RichTextBlock } from '@/ui/byline/blocks/richtext-block'
9
- import { toKebabCase } from '@/ui/utils/to-kebab-case'
10
- import type { Locale } from '@/i18n/i18n-config'
11
-
12
- const Blocks = [PhotoBlockDef, RichTextBlockDef] as const
13
-
14
- export type AnyBlock = BlocksUnion<typeof Blocks>
15
-
16
- // Mapped type ensures every AnyBlock['_type'] has a registered component.
17
- // TypeScript errors here if Blocks gains a new type without a matching entry.
18
- type BlockRegistry = {
19
- [K in AnyBlock['_type']]: React.ComponentType<{
20
- id: string
21
- block: Extract<AnyBlock, { _type: K }>
22
- lng: Locale
23
- constrainedLayout?: boolean
24
- }>
25
- }
26
-
27
- const blockComponents: BlockRegistry = {
28
- photoBlock: PhotoBlock,
29
- richTextBlock: RichTextBlock,
30
- }
31
-
32
- // Loose alias for the call site — BlockRegistry enforces correctness at
33
- // definition time; TypeScript can't infer the correlation during the map loop.
34
- type AnyBlockComponent = React.ComponentType<{
35
- id: string
36
- block: AnyBlock
37
- lng: Locale
38
- constrainedLayout?: boolean
39
- }>
5
+ import { toKebabCase } from '@/ui/byline/utils/to-kebab-case'
6
+ import type { PopulatedContentBlock } from '@/ui/byline/types/content'
7
+ import type { Locale } from '@/ui/byline/types/i18n'
40
8
 
41
9
  interface Props {
42
- blocks: AnyBlock[] | undefined | null
10
+ blocks: PopulatedContentBlock[] | undefined | null
43
11
  lng: Locale
44
12
  constrainedLayout?: boolean
45
13
  }
@@ -54,13 +22,45 @@ export function RenderBlocks({
54
22
  return (
55
23
  <>
56
24
  {blocks.map((block) => {
57
- const Block = blockComponents[block._type] as AnyBlockComponent
25
+ const content = renderBlock(block, lng, constrainedLayout)
26
+ if (content == null) return null
27
+
58
28
  return (
59
29
  <Section className={toKebabCase(block._type)} key={block._id}>
60
- <Block id={block._id} block={block} lng={lng} constrainedLayout={constrainedLayout} />
30
+ {content}
61
31
  </Section>
62
32
  )
63
33
  })}
64
34
  </>
65
35
  )
66
36
  }
37
+
38
+ function renderBlock(
39
+ block: PopulatedContentBlock,
40
+ lng: Locale,
41
+ constrainedLayout: boolean
42
+ ): React.JSX.Element | null {
43
+ switch (block._type) {
44
+ case 'photoBlock':
45
+ return (
46
+ <PhotoBlock id={block._id} block={block} lng={lng} constrainedLayout={constrainedLayout} />
47
+ )
48
+ case 'richTextBlock':
49
+ return (
50
+ <RichTextBlock
51
+ id={block._id}
52
+ block={block}
53
+ lng={lng}
54
+ constrainedLayout={constrainedLayout}
55
+ />
56
+ )
57
+ default:
58
+ return reportUnsupportedBlock(block)
59
+ }
60
+ }
61
+
62
+ function reportUnsupportedBlock(block: never): null {
63
+ const type = (block as { _type?: unknown })._type
64
+ console.error(`Unsupported content block type: "${String(type)}"`)
65
+ return null
66
+ }
@@ -0,0 +1,25 @@
1
+ import type { WithPopulated } from '@byline/client'
2
+
3
+ import type { DocsFields, MediaFields, PagesFields } from '~/generated/collection-types.js'
4
+
5
+ type ContentBlockOf<Fields extends { content?: unknown }> =
6
+ NonNullable<Fields['content']> extends Array<infer Block> ? Block : never
7
+ type ContentBlock = ContentBlockOf<DocsFields> | ContentBlockOf<PagesFields>
8
+ type PhotoBlockData = Extract<ContentBlock, { _type: 'photoBlock' }>
9
+ type PopulatePhotoBlock<Block> = Block extends PhotoBlockData
10
+ ? WithPopulated<Block, 'photo', MediaFields>
11
+ : Block
12
+ type PopulatePhotoBlockContent<Content> =
13
+ Content extends Array<infer Block> ? Array<PopulatePhotoBlock<Block>> : Content
14
+
15
+ /** Photo block shape returned by reads that populate the `photo` relation. */
16
+ export type PopulatedPhotoBlockData = WithPopulated<PhotoBlockData, 'photo', MediaFields>
17
+
18
+ export type PopulatedContentBlock = PopulatePhotoBlock<ContentBlock>
19
+
20
+ /** Overlay populated photo blocks while preserving each consumer's content union. */
21
+ export type WithPopulatedPhotoBlockContent<Fields> = {
22
+ [Key in keyof Fields]: Key extends 'content'
23
+ ? PopulatePhotoBlockContent<Fields[Key]>
24
+ : Fields[Key]
25
+ }
@@ -1,14 +1,10 @@
1
1
  /**
2
2
  * `Locale` type used throughout the ui/byline tree as a `lng` prop type.
3
3
  *
4
- * The reference webapp ships its own `i18n-config.ts` that narrows this
5
- * to a literal union of supported locales (`'en' | 'fr' | ...`). This
6
- * stub keeps the type as `string` so the install builds without
7
- * requiring you to wire up an i18n config.
4
+ * This portable default stays `string` so the scaffold builds without an
5
+ * application-specific i18n module.
8
6
  *
9
- * If your app has its own i18n setup, replace this file with one that
10
- * re-exports your narrower `Locale` type, e.g.
11
- *
12
- * export type { Locale } from '@/i18n/i18n-config'
7
+ * If your app has its own i18n setup, replace this alias with its narrower
8
+ * locale union or re-export that setup's `Locale` type from this file.
13
9
  */
14
10
  export type Locale = string
@@ -0,0 +1,5 @@
1
+ import { Context } from './context.js';
2
+ import type { Answers } from './types.js';
3
+ export declare function createTestContext(answers?: Answers): Context;
4
+ export declare function createTestContextAt(cwd: string, answers?: Answers): Context;
5
+ //# sourceMappingURL=test-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAGtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AA6BzC,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,OAAY,GAAG,OAAO,CAGhE;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,OAAY,GAAG,OAAO,CAe/E"}
@@ -0,0 +1,52 @@
1
+ import { mkdtempSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { Context } from './context.js';
5
+ import { StateStore } from './state.js';
6
+ const noop = () => { };
7
+ const logger = { info: noop, warn: noop, error: noop, success: noop, step: noop, raw: noop };
8
+ const prompter = {
9
+ async text({ defaultValue }) {
10
+ return defaultValue ?? '';
11
+ },
12
+ async password() {
13
+ return 'password';
14
+ },
15
+ async select({ options }) {
16
+ const first = options[0];
17
+ if (!first)
18
+ throw new Error('test prompt has no options');
19
+ return first.value;
20
+ },
21
+ async confirm({ defaultValue }) {
22
+ return defaultValue ?? true;
23
+ },
24
+ spinner: () => ({ start: noop, stop: noop }),
25
+ intro: noop,
26
+ outro: noop,
27
+ note: noop,
28
+ cancel(message) {
29
+ throw new Error(message);
30
+ },
31
+ };
32
+ export function createTestContext(answers = {}) {
33
+ const cwd = mkdtempSync(join(tmpdir(), 'byline-cli-test-'));
34
+ return createTestContextAt(cwd, answers);
35
+ }
36
+ export function createTestContextAt(cwd, answers = {}) {
37
+ const state = new StateStore(cwd);
38
+ state.patchAnswers(answers);
39
+ return new Context({
40
+ cwd,
41
+ apply: true,
42
+ dryRun: false,
43
+ yes: true,
44
+ reset: false,
45
+ resetConfirmed: false,
46
+ cliFlags: {},
47
+ logger,
48
+ prompter,
49
+ state,
50
+ });
51
+ }
52
+ //# sourceMappingURL=test-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAKvC,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAA;AACrB,MAAM,MAAM,GAAW,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;AACpG,MAAM,QAAQ,GAAa;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE;QACzB,OAAO,YAAY,IAAI,EAAE,CAAA;IAC3B,CAAC;IACD,KAAK,CAAC,QAAQ;QACZ,OAAO,UAAU,CAAA;IACnB,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE;QACtB,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;QACxB,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAA;QACzD,OAAO,KAAK,CAAC,KAAK,CAAA;IACpB,CAAC;IACD,KAAK,CAAC,OAAO,CAAC,EAAE,YAAY,EAAE;QAC5B,OAAO,YAAY,IAAI,IAAI,CAAA;IAC7B,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC5C,KAAK,EAAE,IAAI;IACX,KAAK,EAAE,IAAI;IACX,IAAI,EAAE,IAAI;IACV,MAAM,CAAC,OAAO;QACZ,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAA;IAC1B,CAAC;CACF,CAAA;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAO,GAAY,EAAE;IACrD,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAA;IAC3D,OAAO,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;AAC1C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAW,EAAE,OAAO,GAAY,EAAE;IACpE,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAA;IACjC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IAC3B,OAAO,IAAI,OAAO,CAAC;QACjB,GAAG;QACH,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,KAAK;QACb,GAAG,EAAE,IAAI;QACT,KAAK,EAAE,KAAK;QACZ,cAAc,EAAE,KAAK;QACrB,QAAQ,EAAE,EAAE;QACZ,MAAM;QACN,QAAQ;QACR,KAAK;KACN,CAAC,CAAA;AACJ,CAAC"}
package/dist/types.d.ts CHANGED
@@ -6,7 +6,7 @@ export type DbStrategy = 'existing' | 'docker';
6
6
  export interface FileWrite {
7
7
  path: string;
8
8
  contents: string;
9
- mode?: 'create' | 'overwrite' | 'patch';
9
+ mode?: 'create' | 'overwrite' | 'patch' | 'delete';
10
10
  before?: string;
11
11
  }
12
12
  export interface ShellCommand {
@@ -18,7 +18,26 @@ export interface Plan {
18
18
  writes: FileWrite[];
19
19
  commands: ShellCommand[];
20
20
  notes: string[];
21
+ preconditions?: PlanPrecondition[];
21
22
  }
23
+ export interface TreeSnapshotEntry {
24
+ path: string;
25
+ type: 'directory' | 'file' | 'other';
26
+ contents?: string;
27
+ }
28
+ export type PlanPrecondition = {
29
+ type: 'file';
30
+ path: string;
31
+ contents: string | null;
32
+ } | {
33
+ type: 'tree';
34
+ path: string;
35
+ entries: TreeSnapshotEntry[];
36
+ } | {
37
+ type: 'value';
38
+ key: string;
39
+ value: string;
40
+ };
22
41
  export interface PhaseResult {
23
42
  state: PhaseState;
24
43
  notes?: string[];
@@ -38,6 +57,7 @@ export interface Answers {
38
57
  dbName?: string;
39
58
  dbUser?: string;
40
59
  adminPath?: string;
60
+ signInPath?: string;
41
61
  uiDir?: string;
42
62
  examples?: boolean;
43
63
  importDocs?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,MAAM,MAAM,OAAO,GACf,WAAW,GACX,SAAS,GACT,MAAM,GACN,IAAI,GACJ,SAAS,GACT,KAAK,GACL,MAAM,GACN,UAAU,GACV,YAAY,GACZ,WAAW,GACX,MAAM,GACN,QAAQ,GACR,IAAI,GACJ,QAAQ,CAAA;AAEZ,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAA;AAEnE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;AAE5D,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAA;AAE9C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAA;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,IAAI;IACnB,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,UAAU,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,OAAO,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,SAAS,GAAG,MAAM,CAAA;IAC/B,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IACzC,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CACtD;AAED,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,EAAE,CAAC,EAAE,cAAc,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,CAAC,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,OAAO,EAAE,CAAA;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC,CAAA;CACxE"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ui-template-contract.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-template-contract.test.d.ts","sourceRoot":"","sources":["../src/ui-template-contract.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget, SyntaxKind } from 'ts-morph';
4
+ import { describe, expect, it } from 'vitest';
5
+ const templates = resolve(dirname(fileURLToPath(import.meta.url)), 'templates');
6
+ const project = new Project({ skipAddingFilesFromTsConfig: true });
7
+ const generated = project.addSourceFileAtPath(resolve(templates, 'byline-examples/generated/collection-types.ts'));
8
+ const contentTypes = project.addSourceFileAtPath(resolve(templates, 'ui-byline/types/content.ts'));
9
+ const renderer = project.addSourceFileAtPath(resolve(templates, 'ui-byline/render-blocks.tsx'));
10
+ describe('public block renderer templates', () => {
11
+ it('covers every generated Docs and Pages block discriminant', () => {
12
+ const blockNames = new Set([
13
+ ...contentBlockNames('DocsFields'),
14
+ ...contentBlockNames('PagesFields'),
15
+ ]);
16
+ const expectedTypes = [...blockNames].map(blockDiscriminant).sort();
17
+ const switchStatement = renderer
18
+ .getDescendantsOfKind(SyntaxKind.SwitchStatement)
19
+ .find((node) => node.getExpression().getText() === 'block._type');
20
+ const renderedTypes = switchStatement
21
+ ?.getCaseBlock()
22
+ .getClauses()
23
+ .flatMap((clause) => {
24
+ if (!clause.isKind(SyntaxKind.CaseClause))
25
+ return [];
26
+ const expression = clause.getExpression();
27
+ return expression.isKind(SyntaxKind.StringLiteral) ? [expression.getLiteralValue()] : [];
28
+ })
29
+ .sort();
30
+ expect(renderedTypes).toEqual(expectedTypes);
31
+ });
32
+ it('combines both consumers and preserves consumer-specific content in the overlay', () => {
33
+ expect(contentTypes.getTypeAliasOrThrow('ContentBlock').getTypeNodeOrThrow().getText()).toBe('ContentBlockOf<DocsFields> | ContentBlockOf<PagesFields>');
34
+ expect(contentTypes.getTypeAlias('WithPopulatedPhotoBlockContent')).toBeDefined();
35
+ });
36
+ it('typechecks the populated union and consumer-specific overlay contract', () => {
37
+ const typeProject = new Project({
38
+ useInMemoryFileSystem: true,
39
+ compilerOptions: {
40
+ strict: true,
41
+ noEmit: true,
42
+ module: ModuleKind.ESNext,
43
+ moduleResolution: ModuleResolutionKind.Bundler,
44
+ target: ScriptTarget.ES2024,
45
+ },
46
+ });
47
+ typeProject.createSourceFile('/client.ts', `
48
+ export interface PopulatedRelation<T> { document?: T }
49
+ export type WithPopulated<F, K extends keyof F, Target> = {
50
+ [P in keyof F]: P extends K ? PopulatedRelation<Target> : F[P]
51
+ }
52
+ `);
53
+ typeProject.createSourceFile('/generated.ts', generated.getFullText().replace(/import type \{[\s\S]*?\} from '@byline\/core'/, `type JsonValue = unknown
54
+ type RelatedDocumentValue = { documentId: string }
55
+ type StoredFileValue = { key: string }`));
56
+ typeProject.createSourceFile('/content.ts', contentTypes
57
+ .getFullText()
58
+ .replace("from '@byline/client'", "from './client.js'")
59
+ .replace("from '~/generated/collection-types.js'", "from './generated.js'"));
60
+ typeProject.createSourceFile('/contract.ts', `
61
+ import type { WithPopulated } from './client.js'
62
+ import type { DocsFields, MediaFields, PagesFields } from './generated.js'
63
+ import type {
64
+ PopulatedContentBlock,
65
+ WithPopulatedPhotoBlockContent,
66
+ } from './content.js'
67
+
68
+ type Equal<Left, Right> =
69
+ (<Value>() => Value extends Left ? 1 : 2) extends
70
+ (<Value>() => Value extends Right ? 1 : 2) ? true : false
71
+ type Assert<Value extends true> = Value
72
+ type ConsumerBlock =
73
+ | NonNullable<DocsFields['content']>[number]
74
+ | NonNullable<PagesFields['content']>[number]
75
+ type PhotoBlock = Extract<ConsumerBlock, { _type: 'photoBlock' }>
76
+ type ExpectedBlock =
77
+ | Exclude<ConsumerBlock, PhotoBlock>
78
+ | WithPopulated<PhotoBlock, 'photo', MediaFields>
79
+ type PagesOnlyBlock = { _id: string; _type: 'pagesOnlyBlock'; heading: string }
80
+ type ExtendedPages = Omit<PagesFields, 'content'> & {
81
+ content?: Array<NonNullable<PagesFields['content']>[number] | PagesOnlyBlock>
82
+ }
83
+ type PopulatedExtendedBlock = NonNullable<
84
+ WithPopulatedPhotoBlockContent<ExtendedPages>['content']
85
+ >[number]
86
+
87
+ type PopulatedUnionContract = Assert<Equal<PopulatedContentBlock, ExpectedBlock>>
88
+ type ConsumerOverlayContract = Assert<
89
+ Equal<Extract<PopulatedExtendedBlock, { _type: 'pagesOnlyBlock' }>, PagesOnlyBlock>
90
+ >
91
+ `);
92
+ expect(typeProject.getPreEmitDiagnostics().map((diagnostic) => diagnostic.getMessageText())).toEqual([]);
93
+ });
94
+ it('reports unsupported runtime blocks and returns null', () => {
95
+ const fallback = renderer.getFunctionOrThrow('reportUnsupportedBlock');
96
+ expect(fallback
97
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
98
+ .some((call) => call.getExpression().getText() === 'console.error')).toBe(true);
99
+ expect(fallback
100
+ .getDescendantsOfKind(SyntaxKind.ReturnStatement)
101
+ .some((statement) => statement.getExpression()?.isKind(SyntaxKind.NullKeyword))).toBe(true);
102
+ });
103
+ });
104
+ function contentBlockNames(collection) {
105
+ const content = generated
106
+ .getTypeAliasOrThrow(collection)
107
+ .getDescendantsOfKind(SyntaxKind.PropertySignature)
108
+ .find((property) => property.getName() === 'content');
109
+ const matches = content
110
+ ?.getTypeNodeOrThrow()
111
+ .getText()
112
+ .matchAll(/\b([A-Z]\w*BlockData)\b/g);
113
+ return [...(matches ?? [])].map((match) => match[1]).filter((name) => name !== undefined);
114
+ }
115
+ function blockDiscriminant(blockName) {
116
+ const type = generated.getTypeAliasOrThrow(blockName);
117
+ const discriminant = type
118
+ .getDescendantsOfKind(SyntaxKind.PropertySignature)
119
+ .find((property) => property.getName() === '_type')
120
+ ?.getTypeNodeOrThrow();
121
+ if (!discriminant?.isKind(SyntaxKind.LiteralType)) {
122
+ throw new Error(`Generated block ${blockName} has no literal _type`);
123
+ }
124
+ const literal = discriminant.getLiteral();
125
+ if (!literal.isKind(SyntaxKind.StringLiteral)) {
126
+ throw new Error(`Generated block ${blockName} has a non-string _type`);
127
+ }
128
+ return literal.getLiteralValue();
129
+ }
130
+ //# sourceMappingURL=ui-template-contract.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-template-contract.test.js","sourceRoot":"","sources":["../src/ui-template-contract.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=workspace-root.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-root.test.d.ts","sourceRoot":"","sources":["../src/workspace-root.test.ts"],"names":[],"mappings":""}