@byline/cli 3.21.0 → 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 (179) hide show
  1. package/README.md +29 -3
  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 +3 -2
  97. package/dist/phases/scaffold.d.ts.map +1 -1
  98. package/dist/phases/scaffold.js +268 -91
  99. package/dist/phases/scaffold.js.map +1 -1
  100. package/dist/phases/scaffold.test.js +171 -22
  101. package/dist/phases/scaffold.test.js.map +1 -1
  102. package/dist/phases/ui.d.ts +4 -1
  103. package/dist/phases/ui.d.ts.map +1 -1
  104. package/dist/phases/ui.js +51 -59
  105. package/dist/phases/ui.js.map +1 -1
  106. package/dist/phases/wire/index.d.ts.map +1 -1
  107. package/dist/phases/wire/index.js +10 -6
  108. package/dist/phases/wire/index.js.map +1 -1
  109. package/dist/phases/wire/shared.d.ts +4 -1
  110. package/dist/phases/wire/shared.d.ts.map +1 -1
  111. package/dist/phases/wire/vite-config.d.ts.map +1 -1
  112. package/dist/phases/wire/vite-config.js +69 -20
  113. package/dist/phases/wire/vite-config.js.map +1 -1
  114. package/dist/phases/wire/vite-config.test.d.ts +2 -0
  115. package/dist/phases/wire/vite-config.test.d.ts.map +1 -0
  116. package/dist/phases/wire/vite-config.test.js +97 -0
  117. package/dist/phases/wire/vite-config.test.js.map +1 -0
  118. package/dist/prompts.d.ts +1 -0
  119. package/dist/prompts.d.ts.map +1 -1
  120. package/dist/prompts.js +7 -2
  121. package/dist/prompts.js.map +1 -1
  122. package/dist/runner.js +1 -1
  123. package/dist/runner.js.map +1 -1
  124. package/dist/templates/byline/clients.server.ts +77 -0
  125. package/dist/templates/byline/collection-types.contract.ts +25 -0
  126. package/dist/templates/byline/collections/server-hooks.ts +7 -0
  127. package/dist/templates/byline/routes.ts +7 -6
  128. package/dist/templates/byline/server.config.ts +3 -1
  129. package/dist/templates/byline-examples/admin.config.ts +7 -12
  130. package/dist/templates/byline-examples/blocks/photo-block.ts +3 -2
  131. package/dist/templates/byline-examples/clients.server.ts +77 -0
  132. package/dist/templates/byline-examples/collection-types.contract.ts +25 -0
  133. package/dist/templates/byline-examples/collections/docs/hooks.ts +23 -13
  134. package/dist/templates/byline-examples/collections/docs/schema.ts +2 -11
  135. package/dist/templates/byline-examples/collections/media/components/media-list-view.tsx +11 -6
  136. package/dist/templates/byline-examples/collections/media/schema.ts +3 -0
  137. package/dist/templates/byline-examples/collections/news/schema.ts +4 -3
  138. package/dist/templates/byline-examples/collections/server-hooks.ts +8 -0
  139. package/dist/templates/byline-examples/fields/ai-text.ts +1 -1
  140. package/dist/templates/byline-examples/fields/ai-textarea.ts +1 -1
  141. package/dist/templates/byline-examples/fields/lexical-richtext-ai.tsx +3 -3
  142. package/dist/templates/byline-examples/routes.ts +7 -6
  143. package/dist/templates/byline-examples/scripts/backfill-version-locales.ts +1 -1
  144. package/dist/templates/byline-examples/scripts/import-docs.ts +11 -12
  145. package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.test.node.ts +93 -233
  146. package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.ts +2 -2
  147. package/dist/templates/byline-examples/scripts/lib/strip-leading-h1.test.node.ts +0 -4
  148. package/dist/templates/byline-examples/scripts/re-anchor.ts +3 -3
  149. package/dist/templates/byline-examples/scripts/regenerate-media.ts +6 -5
  150. package/dist/templates/byline-examples/server.config.ts +5 -4
  151. package/dist/templates/host/vite.config.ts +2 -0
  152. package/dist/templates/routes/_byline/route.lazy.tsx +1 -2
  153. package/dist/templates/routes/_byline/route.tsx +1 -1
  154. package/dist/templates/ui-byline/blocks/photo-block/index.tsx +18 -22
  155. package/dist/templates/ui-byline/components/admonition/index.tsx +11 -8
  156. package/dist/templates/ui-byline/components/code/code.tsx +6 -4
  157. package/dist/templates/ui-byline/components/link/lang-link.tsx +4 -8
  158. package/dist/templates/ui-byline/components/link/link-field.tsx +3 -2
  159. package/dist/templates/ui-byline/components/link/link-lexical.tsx +9 -8
  160. package/dist/templates/ui-byline/render-blocks.tsx +38 -34
  161. package/dist/templates/ui-byline/types/content.ts +25 -0
  162. package/dist/templates/ui-byline/types/i18n.ts +4 -8
  163. package/dist/test-helpers.d.ts +5 -0
  164. package/dist/test-helpers.d.ts.map +1 -0
  165. package/dist/test-helpers.js +52 -0
  166. package/dist/test-helpers.js.map +1 -0
  167. package/dist/types.d.ts +21 -1
  168. package/dist/types.d.ts.map +1 -1
  169. package/dist/ui-template-contract.test.d.ts +2 -0
  170. package/dist/ui-template-contract.test.d.ts.map +1 -0
  171. package/dist/ui-template-contract.test.js +130 -0
  172. package/dist/ui-template-contract.test.js.map +1 -0
  173. package/dist/workspace-root.test.d.ts +2 -0
  174. package/dist/workspace-root.test.d.ts.map +1 -0
  175. package/dist/workspace-root.test.js +307 -0
  176. package/dist/workspace-root.test.js.map +1 -0
  177. package/package.json +9 -1
  178. package/dist/templates/byline/client.server.ts +0 -32
  179. package/dist/templates/byline-examples/client.server.ts +0 -32
@@ -24,8 +24,7 @@
24
24
  * Both call `defineClientConfig` idempotently.
25
25
  *
26
26
  * If you also want to use the Byline UI components on your public site,
27
- * import the same stylesheets from your front-end's pathless layout
28
- * instead (e.g. `_front-end/route.tsx`).
27
+ * import the same stylesheets from your public layout route as well.
29
28
  */
30
29
 
31
30
  import { createLazyFileRoute, Outlet } from '@tanstack/react-router'
@@ -9,7 +9,7 @@
9
9
  /**
10
10
  * Pathless route definition for everything Byline-rendered (the admin
11
11
  * shell and the sign-in page). The `_byline` prefix doesn't contribute
12
- * to URL paths — `/_byline/admin/...` resolves to `/admin/...` and
12
+ * to URL paths — by default, `/_byline/admin/...` resolves to `/admin/...` and
13
13
  * `/_byline/sign-in` resolves to `/sign-in`.
14
14
  *
15
15
  * This file is intentionally light. The router needs the route definition
@@ -5,15 +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 { MediaFields, PhotoBlockData } from '~/generated/collection-types.js'
9
-
10
8
  import { ResponsiveImage } from '@/ui/byline/components/responsive-image'
11
9
  import { LexicalRichText } from '@/ui/byline/components/richtext-lexical'
10
+ import type { PopulatedPhotoBlockData } from '@/ui/byline/types/content'
12
11
  import type { Locale } from '@/ui/byline/types/i18n'
13
12
 
14
13
  interface Props {
15
14
  id: string
16
- block: PhotoBlockData
15
+ block: PopulatedPhotoBlockData
17
16
  lng: Locale
18
17
  constrainedLayout?: boolean
19
18
  className?: string
@@ -28,27 +27,26 @@ export function PhotoBlock({
28
27
  }: Props): React.JSX.Element | null {
29
28
  const { photo, alt, caption, display } = block
30
29
 
31
- // The `photo` relation must be populated (`populate: { photo: '*' }`) for
32
- // the front-end to render an image. When populated, the value is a
33
- // `PopulatedRelationValue` envelope — `{ ...rel, _resolved: true,
34
- // document: <MediaFields> }`. Bail out if the relation is missing or
35
- // unpopulated.
36
- if (photo == null || (photo as { _resolved?: boolean })._resolved !== true) {
37
- return null
38
- }
39
- 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
40
33
 
41
34
  const Comp = display === 'full_width' ? 'div' : Container
42
35
 
43
- // TODO: caption is a Lexical richText field — type properly once the
44
- // Lexical node shape is modelled in @byline/core.
45
- 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
46
44
 
47
45
  return (
48
46
  <Comp
49
47
  id={id}
50
48
  className={cx(
51
- 'px-0 pt-4',
49
+ 'px-0',
52
50
  {
53
51
  'lg:max-w-[920px] xl:max-w-[920px] 2xl:max-w-[920px] mx-auto': display === 'default',
54
52
  },
@@ -64,12 +62,10 @@ export function PhotoBlock({
64
62
  className="photo-block"
65
63
  imgClassName="photo-block--photo"
66
64
  />
67
- {captionDoc != null && (
68
- <Container
69
- className={cx('bg-white dark:bg-canvas-900 py-2', { 'px-0': constrainedLayout })}
70
- >
71
- <div className="photo-block--caption">
72
- <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} />
73
69
  </div>
74
70
  </Container>
75
71
  )}
@@ -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,41 +1,13 @@
1
1
  import { Section } from '@byline/ui/react'
2
2
 
3
- import type { PhotoBlockData, RichTextBlockData } from '~/generated/collection-types.js'
4
-
5
3
  import { PhotoBlock } from '@/ui/byline/blocks/photo-block'
6
4
  import { RichTextBlock } from '@/ui/byline/blocks/richtext-block'
7
- import { toKebabCase } from '@/ui/utils/to-kebab-case'
8
- import type { Locale } from '@/i18n/i18n-config'
9
-
10
- export type AnyBlock = PhotoBlockData | RichTextBlockData
11
-
12
- // Mapped type ensures every AnyBlock['_type'] has a registered component.
13
- // TypeScript errors here if AnyBlock gains a new type without a matching entry.
14
- type BlockRegistry = {
15
- [K in AnyBlock['_type']]: React.ComponentType<{
16
- id: string
17
- block: Extract<AnyBlock, { _type: K }>
18
- lng: Locale
19
- constrainedLayout?: boolean
20
- }>
21
- }
22
-
23
- const blockComponents: BlockRegistry = {
24
- photoBlock: PhotoBlock,
25
- richTextBlock: RichTextBlock,
26
- }
27
-
28
- // Loose alias for the call site — BlockRegistry enforces correctness at
29
- // definition time; TypeScript can't infer the correlation during the map loop.
30
- type AnyBlockComponent = React.ComponentType<{
31
- id: string
32
- block: AnyBlock
33
- lng: Locale
34
- constrainedLayout?: boolean
35
- }>
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'
36
8
 
37
9
  interface Props {
38
- blocks: AnyBlock[] | undefined | null
10
+ blocks: PopulatedContentBlock[] | undefined | null
39
11
  lng: Locale
40
12
  constrainedLayout?: boolean
41
13
  }
@@ -50,13 +22,45 @@ export function RenderBlocks({
50
22
  return (
51
23
  <>
52
24
  {blocks.map((block) => {
53
- const Block = blockComponents[block._type] as AnyBlockComponent
25
+ const content = renderBlock(block, lng, constrainedLayout)
26
+ if (content == null) return null
27
+
54
28
  return (
55
29
  <Section className={toKebabCase(block._type)} key={block._id}>
56
- <Block id={block._id} block={block} lng={lng} constrainedLayout={constrainedLayout} />
30
+ {content}
57
31
  </Section>
58
32
  )
59
33
  })}
60
34
  </>
61
35
  )
62
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":""}