@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.
- package/README.md +29 -3
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +10 -14
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/setup-checks.d.ts +9 -0
- package/dist/commands/setup-checks.d.ts.map +1 -1
- package/dist/commands/setup-checks.js +43 -18
- package/dist/commands/setup-checks.js.map +1 -1
- package/dist/commands/setup.js +1 -1
- package/dist/commands/setup.js.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -0
- package/dist/context.js.map +1 -1
- package/dist/lib/dependency-version.d.ts +12 -0
- package/dist/lib/dependency-version.d.ts.map +1 -0
- package/dist/lib/dependency-version.js +101 -0
- package/dist/lib/dependency-version.js.map +1 -0
- package/dist/lib/planned-writes.d.ts +8 -0
- package/dist/lib/planned-writes.d.ts.map +1 -0
- package/dist/lib/planned-writes.js +177 -0
- package/dist/lib/planned-writes.js.map +1 -0
- package/dist/lib/planned-writes.test.d.ts +2 -0
- package/dist/lib/planned-writes.test.d.ts.map +1 -0
- package/dist/lib/planned-writes.test.js +113 -0
- package/dist/lib/planned-writes.test.js.map +1 -0
- package/dist/lib/release-policy.d.ts +12 -0
- package/dist/lib/release-policy.d.ts.map +1 -0
- package/dist/lib/release-policy.js +20 -0
- package/dist/lib/release-policy.js.map +1 -0
- package/dist/lib/release-policy.test.d.ts +2 -0
- package/dist/lib/release-policy.test.d.ts.map +1 -0
- package/dist/lib/release-policy.test.js +36 -0
- package/dist/lib/release-policy.test.js.map +1 -0
- package/dist/lib/route-config.d.ts +40 -0
- package/dist/lib/route-config.d.ts.map +1 -0
- package/dist/lib/route-config.js +322 -0
- package/dist/lib/route-config.js.map +1 -0
- package/dist/lib/static-config.d.ts +20 -0
- package/dist/lib/static-config.d.ts.map +1 -0
- package/dist/lib/static-config.js +444 -0
- package/dist/lib/static-config.js.map +1 -0
- package/dist/lib/static-config.test.d.ts +2 -0
- package/dist/lib/static-config.test.d.ts.map +1 -0
- package/dist/lib/static-config.test.js +105 -0
- package/dist/lib/static-config.test.js.map +1 -0
- package/dist/lib/template-hook-boundary.test.node.d.ts +2 -0
- package/dist/lib/template-hook-boundary.test.node.d.ts.map +1 -0
- package/dist/lib/template-hook-boundary.test.node.js +30 -0
- package/dist/lib/template-hook-boundary.test.node.js.map +1 -0
- package/dist/lib/template-path.d.ts +5 -0
- package/dist/lib/template-path.d.ts.map +1 -0
- package/dist/lib/template-path.js +9 -0
- package/dist/lib/template-path.js.map +1 -0
- package/dist/lib/workspace-root.d.ts +28 -0
- package/dist/lib/workspace-root.d.ts.map +1 -0
- package/dist/lib/workspace-root.js +205 -0
- package/dist/lib/workspace-root.js.map +1 -0
- package/dist/manifest/deps.d.ts +1 -1
- package/dist/manifest/deps.d.ts.map +1 -1
- package/dist/manifest/deps.js +18 -8
- package/dist/manifest/deps.js.map +1 -1
- package/dist/phases/deps.d.ts +14 -1
- package/dist/phases/deps.d.ts.map +1 -1
- package/dist/phases/deps.js +312 -137
- package/dist/phases/deps.js.map +1 -1
- package/dist/phases/deps.test.d.ts +2 -0
- package/dist/phases/deps.test.d.ts.map +1 -0
- package/dist/phases/deps.test.js +343 -0
- package/dist/phases/deps.test.js.map +1 -0
- package/dist/phases/preflight.d.ts.map +1 -1
- package/dist/phases/preflight.js +59 -11
- package/dist/phases/preflight.js.map +1 -1
- package/dist/phases/prompts.d.ts +2 -0
- package/dist/phases/prompts.d.ts.map +1 -1
- package/dist/phases/prompts.js +51 -2
- package/dist/phases/prompts.js.map +1 -1
- package/dist/phases/routes-ui.test.d.ts +2 -0
- package/dist/phases/routes-ui.test.d.ts.map +1 -0
- package/dist/phases/routes-ui.test.js +766 -0
- package/dist/phases/routes-ui.test.js.map +1 -0
- package/dist/phases/routes.d.ts +4 -1
- package/dist/phases/routes.d.ts.map +1 -1
- package/dist/phases/routes.js +610 -122
- package/dist/phases/routes.js.map +1 -1
- package/dist/phases/scaffold-smoke.test.d.ts +2 -0
- package/dist/phases/scaffold-smoke.test.d.ts.map +1 -0
- package/dist/phases/scaffold-smoke.test.js +168 -0
- package/dist/phases/scaffold-smoke.test.js.map +1 -0
- package/dist/phases/scaffold.d.ts +3 -2
- package/dist/phases/scaffold.d.ts.map +1 -1
- package/dist/phases/scaffold.js +268 -91
- package/dist/phases/scaffold.js.map +1 -1
- package/dist/phases/scaffold.test.js +171 -22
- package/dist/phases/scaffold.test.js.map +1 -1
- package/dist/phases/ui.d.ts +4 -1
- package/dist/phases/ui.d.ts.map +1 -1
- package/dist/phases/ui.js +51 -59
- package/dist/phases/ui.js.map +1 -1
- package/dist/phases/wire/index.d.ts.map +1 -1
- package/dist/phases/wire/index.js +10 -6
- package/dist/phases/wire/index.js.map +1 -1
- package/dist/phases/wire/shared.d.ts +4 -1
- package/dist/phases/wire/shared.d.ts.map +1 -1
- package/dist/phases/wire/vite-config.d.ts.map +1 -1
- package/dist/phases/wire/vite-config.js +69 -20
- package/dist/phases/wire/vite-config.js.map +1 -1
- package/dist/phases/wire/vite-config.test.d.ts +2 -0
- package/dist/phases/wire/vite-config.test.d.ts.map +1 -0
- package/dist/phases/wire/vite-config.test.js +97 -0
- package/dist/phases/wire/vite-config.test.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +7 -2
- package/dist/prompts.js.map +1 -1
- package/dist/runner.js +1 -1
- package/dist/runner.js.map +1 -1
- package/dist/templates/byline/clients.server.ts +77 -0
- package/dist/templates/byline/collection-types.contract.ts +25 -0
- package/dist/templates/byline/collections/server-hooks.ts +7 -0
- package/dist/templates/byline/routes.ts +7 -6
- package/dist/templates/byline/server.config.ts +3 -1
- package/dist/templates/byline-examples/admin.config.ts +7 -12
- package/dist/templates/byline-examples/blocks/photo-block.ts +3 -2
- package/dist/templates/byline-examples/clients.server.ts +77 -0
- package/dist/templates/byline-examples/collection-types.contract.ts +25 -0
- package/dist/templates/byline-examples/collections/docs/hooks.ts +23 -13
- package/dist/templates/byline-examples/collections/docs/schema.ts +2 -11
- package/dist/templates/byline-examples/collections/media/components/media-list-view.tsx +11 -6
- package/dist/templates/byline-examples/collections/media/schema.ts +3 -0
- package/dist/templates/byline-examples/collections/news/schema.ts +4 -3
- package/dist/templates/byline-examples/collections/server-hooks.ts +8 -0
- package/dist/templates/byline-examples/fields/ai-text.ts +1 -1
- package/dist/templates/byline-examples/fields/ai-textarea.ts +1 -1
- package/dist/templates/byline-examples/fields/lexical-richtext-ai.tsx +3 -3
- package/dist/templates/byline-examples/routes.ts +7 -6
- package/dist/templates/byline-examples/scripts/backfill-version-locales.ts +1 -1
- package/dist/templates/byline-examples/scripts/import-docs.ts +11 -12
- package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.test.node.ts +93 -233
- package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.ts +2 -2
- package/dist/templates/byline-examples/scripts/lib/strip-leading-h1.test.node.ts +0 -4
- package/dist/templates/byline-examples/scripts/re-anchor.ts +3 -3
- package/dist/templates/byline-examples/scripts/regenerate-media.ts +6 -5
- package/dist/templates/byline-examples/server.config.ts +5 -4
- package/dist/templates/host/vite.config.ts +2 -0
- package/dist/templates/routes/_byline/route.lazy.tsx +1 -2
- package/dist/templates/routes/_byline/route.tsx +1 -1
- package/dist/templates/ui-byline/blocks/photo-block/index.tsx +18 -22
- package/dist/templates/ui-byline/components/admonition/index.tsx +11 -8
- package/dist/templates/ui-byline/components/code/code.tsx +6 -4
- package/dist/templates/ui-byline/components/link/lang-link.tsx +4 -8
- package/dist/templates/ui-byline/components/link/link-field.tsx +3 -2
- package/dist/templates/ui-byline/components/link/link-lexical.tsx +9 -8
- package/dist/templates/ui-byline/render-blocks.tsx +38 -34
- package/dist/templates/ui-byline/types/content.ts +25 -0
- package/dist/templates/ui-byline/types/i18n.ts +4 -8
- package/dist/test-helpers.d.ts +5 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +52 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/types.d.ts +21 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui-template-contract.test.d.ts +2 -0
- package/dist/ui-template-contract.test.d.ts.map +1 -0
- package/dist/ui-template-contract.test.js +130 -0
- package/dist/ui-template-contract.test.js.map +1 -0
- package/dist/workspace-root.test.d.ts +2 -0
- package/dist/workspace-root.test.d.ts.map +1 -0
- package/dist/workspace-root.test.js +307 -0
- package/dist/workspace-root.test.js.map +1 -0
- package/package.json +9 -1
- package/dist/templates/byline/client.server.ts +0 -32
- package/dist/templates/byline-examples/client.server.ts +0 -32
|
@@ -12,11 +12,6 @@
|
|
|
12
12
|
// pnpm tsx byline/scripts/import-docs.ts ../../docs/**/*.md --verbose
|
|
13
13
|
// pnpm tsx byline/scripts/import-docs.ts '../../docs/*.md' --dry-run --verbose
|
|
14
14
|
// pnpm tsx byline/scripts/import-docs.ts ../../docs/**/*.md
|
|
15
|
-
//
|
|
16
|
-
// Run tests...
|
|
17
|
-
//
|
|
18
|
-
// pnpm vitest run --mode=node byline/scripts/lib/
|
|
19
|
-
|
|
20
15
|
/**
|
|
21
16
|
* Import markdown files into the `docs` collection.
|
|
22
17
|
*
|
|
@@ -59,7 +54,7 @@ import { glob } from 'node:fs/promises'
|
|
|
59
54
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
60
55
|
|
|
61
56
|
import { createSuperAdminContext } from '@byline/auth'
|
|
62
|
-
import { type CollectionHandle, createBylineClient } from '@byline/client'
|
|
57
|
+
import { type BylineClient, type CollectionHandle, createBylineClient } from '@byline/client'
|
|
63
58
|
import { getCollectionDefinition, getServerConfig, slugify } from '@byline/core'
|
|
64
59
|
|
|
65
60
|
import { type DocFrontmatter, parseDocFile } from './lib/frontmatter.js'
|
|
@@ -67,12 +62,16 @@ import { type MdastToLexicalWarning, mdastToLexical } from './lib/mdast-to-lexic
|
|
|
67
62
|
import { parseBodyToMdast } from './lib/parse-markdown.js'
|
|
68
63
|
import { type DocLinkRewriteWarning, rewriteDocLinks } from './lib/rewrite-doc-links.js'
|
|
69
64
|
import { stripLeadingH1IfMatches } from './lib/strip-leading-h1.js'
|
|
65
|
+
import type { CollectionFieldsByPath, DocsFields } from '../generated/collection-types.js'
|
|
70
66
|
|
|
71
67
|
const DOCS_COLLECTION = 'docs'
|
|
72
68
|
const MEDIA_COLLECTION = 'media'
|
|
73
69
|
const DOCS_URL_PREFIX = '/docs'
|
|
74
70
|
const DEFAULT_IMPORT_STATUS = 'published'
|
|
75
71
|
|
|
72
|
+
type AppBylineClient = BylineClient<CollectionFieldsByPath>
|
|
73
|
+
type DocsHandle = CollectionHandle<DocsFields>
|
|
74
|
+
|
|
76
75
|
interface Flags {
|
|
77
76
|
dryRun: boolean
|
|
78
77
|
verbose: boolean
|
|
@@ -135,7 +134,7 @@ interface ResolvedFeatureImage {
|
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
async function resolveFeatureImage(
|
|
138
|
-
client:
|
|
137
|
+
client: AppBylineClient,
|
|
139
138
|
path: string
|
|
140
139
|
): Promise<ResolvedFeatureImage | null> {
|
|
141
140
|
const mediaCollectionId = await client.resolveCollectionId(MEDIA_COLLECTION)
|
|
@@ -186,7 +185,7 @@ function derivePath(frontmatter: DocFrontmatter, locale: string): string {
|
|
|
186
185
|
* workflow doesn't include the target or when already at/past it.
|
|
187
186
|
*/
|
|
188
187
|
async function walkToStatus(
|
|
189
|
-
handle:
|
|
188
|
+
handle: DocsHandle,
|
|
190
189
|
documentId: string,
|
|
191
190
|
workflowStatuses: readonly { name: string }[],
|
|
192
191
|
currentStatus: string,
|
|
@@ -230,8 +229,8 @@ function buildSourcePathMap(files: string[], defaultLocale: string): Map<string,
|
|
|
230
229
|
|
|
231
230
|
async function processFile(
|
|
232
231
|
filePath: string,
|
|
233
|
-
client:
|
|
234
|
-
handle:
|
|
232
|
+
client: AppBylineClient,
|
|
233
|
+
handle: DocsHandle,
|
|
235
234
|
flags: Flags,
|
|
236
235
|
pathMap: Map<string, string>
|
|
237
236
|
): Promise<ProcessResult> {
|
|
@@ -340,7 +339,7 @@ function parentIndexFile(filePath: string): string {
|
|
|
340
339
|
* processed in sorted order, so prefix order carries straight through.
|
|
341
340
|
*/
|
|
342
341
|
async function placeTreeFromDirectories(
|
|
343
|
-
handle:
|
|
342
|
+
handle: DocsHandle,
|
|
344
343
|
results: ProcessResult[]
|
|
345
344
|
): Promise<void> {
|
|
346
345
|
const placeable = results.filter(
|
|
@@ -392,7 +391,7 @@ async function run(): Promise<void> {
|
|
|
392
391
|
|
|
393
392
|
const config = getServerConfig()
|
|
394
393
|
const requestContext = createSuperAdminContext({ id: 'import-docs-script' })
|
|
395
|
-
const client = createBylineClient({ config, requestContext })
|
|
394
|
+
const client = createBylineClient<CollectionFieldsByPath>({ config, requestContext })
|
|
396
395
|
const handle = client.collection(DOCS_COLLECTION)
|
|
397
396
|
|
|
398
397
|
// Pre-pass: map each source file to the path its imported doc will
|
|
@@ -24,312 +24,172 @@ describe('mdastToLexical', () => {
|
|
|
24
24
|
expect(state.root.children[0]).toMatchObject({ type: 'paragraph', children: [] })
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
test('
|
|
28
|
-
const
|
|
29
|
-
expect(
|
|
27
|
+
test('maps plain paragraphs and every heading depth', () => {
|
|
28
|
+
const paragraph = convert('hello world').state.root.children[0]
|
|
29
|
+
expect(paragraph).toMatchObject({
|
|
30
30
|
type: 'paragraph',
|
|
31
31
|
children: [{ type: 'text', text: 'hello world', format: 0 }],
|
|
32
32
|
})
|
|
33
|
+
const headings = convert('# h1\n\n## h2\n\n###### h6').state.root.children
|
|
34
|
+
expect(headings.map((node) => (node as { tag: string }).tag)).toEqual(['h1', 'h2', 'h6'])
|
|
33
35
|
})
|
|
34
36
|
|
|
35
|
-
test('
|
|
36
|
-
const { state } = convert('#
|
|
37
|
-
expect(state.root.children).toHaveLength(3)
|
|
37
|
+
test('maps headings and composed inline formatting', () => {
|
|
38
|
+
const { state } = convert('# heading\n\n***both*** and `code`')
|
|
38
39
|
expect(state.root.children[0]).toMatchObject({ type: 'heading', tag: 'h1' })
|
|
39
|
-
|
|
40
|
-
expect(state.root.children[2]).toMatchObject({ type: 'heading', tag: 'h6' })
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('bold + italic compose into a single text node with bitmask format=3', () => {
|
|
44
|
-
const { state } = convert('***both***')
|
|
45
|
-
const paragraph = state.root.children[0] as unknown as {
|
|
40
|
+
const paragraph = state.root.children[1] as unknown as {
|
|
46
41
|
children: Array<{ format: number; text: string }>
|
|
47
42
|
}
|
|
48
|
-
expect(paragraph.children).
|
|
49
|
-
expect(paragraph.children
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
test('inline code carries the IS_CODE bit', () => {
|
|
53
|
-
const { state } = convert('a `b` c')
|
|
54
|
-
const paragraph = state.root.children[0] as unknown as {
|
|
55
|
-
children: Array<{ format: number; text: string }>
|
|
56
|
-
}
|
|
57
|
-
// 'a ', code 'b', ' c'
|
|
58
|
-
expect(paragraph.children.map((c) => ({ text: c.text, format: c.format }))).toEqual([
|
|
59
|
-
{ text: 'a ', format: 0 },
|
|
60
|
-
{ text: 'b', format: 1 << 4 },
|
|
61
|
-
{ text: ' c', format: 0 },
|
|
62
|
-
])
|
|
43
|
+
expect(paragraph.children).toContainEqual(expect.objectContaining({ text: 'both', format: 3 }))
|
|
44
|
+
expect(paragraph.children).toContainEqual(
|
|
45
|
+
expect.objectContaining({ text: 'code', format: 1 << 4 })
|
|
46
|
+
)
|
|
63
47
|
})
|
|
64
48
|
|
|
65
|
-
test('
|
|
49
|
+
test('preserves GFM strikethrough', () => {
|
|
66
50
|
const { state } = convert('a ~~b~~ c')
|
|
67
51
|
const paragraph = state.root.children[0] as unknown as {
|
|
68
52
|
children: Array<{ format: number; text: string }>
|
|
69
53
|
}
|
|
70
|
-
|
|
71
|
-
expect(struck?.format).toBe(1 << 2)
|
|
54
|
+
expect(paragraph.children.find((child) => child.text === 'b')?.format).toBe(1 << 2)
|
|
72
55
|
})
|
|
73
56
|
|
|
74
|
-
test('unordered
|
|
75
|
-
const
|
|
76
|
-
const list = state.root.children[0] as unknown as {
|
|
77
|
-
type: string
|
|
57
|
+
test('maps unordered, ordered, and nested lists', () => {
|
|
58
|
+
const unordered = convert('- a\n - a1\n- b').state.root.children[0] as unknown as {
|
|
78
59
|
listType: string
|
|
79
60
|
tag: string
|
|
80
|
-
children: Array<{
|
|
61
|
+
children: Array<{ children: Array<{ type: string }> }>
|
|
81
62
|
}
|
|
82
|
-
expect(
|
|
83
|
-
expect(
|
|
84
|
-
expect(list.children[0]).toMatchObject({
|
|
85
|
-
type: 'listitem',
|
|
86
|
-
children: [{ type: 'text', text: 'one' }],
|
|
87
|
-
})
|
|
88
|
-
})
|
|
63
|
+
expect(unordered).toMatchObject({ listType: 'bullet', tag: 'ul' })
|
|
64
|
+
expect(unordered.children[0].children.some((child) => child.type === 'list')).toBe(true)
|
|
89
65
|
|
|
90
|
-
|
|
91
|
-
const { state } = convert('3. a\n4. b')
|
|
92
|
-
const list = state.root.children[0] as unknown as {
|
|
66
|
+
const ordered = convert('3. a\n4. b').state.root.children[0] as unknown as {
|
|
93
67
|
listType: string
|
|
94
68
|
tag: string
|
|
95
69
|
start: number
|
|
96
70
|
children: Array<{ value: number }>
|
|
97
71
|
}
|
|
98
|
-
expect(
|
|
99
|
-
expect(
|
|
100
|
-
expect(list.children[1].value).toBe(4)
|
|
72
|
+
expect(ordered).toMatchObject({ listType: 'number', tag: 'ol', start: 3 })
|
|
73
|
+
expect(ordered.children.map((child) => child.value)).toEqual([3, 4])
|
|
101
74
|
})
|
|
102
75
|
|
|
103
|
-
test('
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
children: Array<{
|
|
76
|
+
test('maps links and fenced code language aliases', () => {
|
|
77
|
+
const linkParagraph = convert('[label](https://example.com)').state.root
|
|
78
|
+
.children[0] as unknown as {
|
|
79
|
+
children: Array<{ attributes: { url: string; newTab: boolean } }>
|
|
107
80
|
}
|
|
108
|
-
|
|
109
|
-
expect(firstItemChildren.some((c) => c.type === 'list')).toBe(true)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test('link emits link node with custom attributes envelope', () => {
|
|
113
|
-
const { state } = convert('[label](https://example.com)')
|
|
114
|
-
const paragraph = state.root.children[0] as unknown as {
|
|
115
|
-
children: Array<{
|
|
116
|
-
type: string
|
|
117
|
-
attributes?: { linkType?: string; url?: string; newTab?: boolean }
|
|
118
|
-
children?: Array<{ text: string }>
|
|
119
|
-
}>
|
|
120
|
-
}
|
|
121
|
-
const link = paragraph.children[0]
|
|
122
|
-
expect(link.type).toBe('link')
|
|
123
|
-
expect(link.attributes).toMatchObject({
|
|
124
|
-
linkType: 'custom',
|
|
81
|
+
expect(linkParagraph.children[0].attributes).toMatchObject({
|
|
125
82
|
url: 'https://example.com',
|
|
126
83
|
newTab: true,
|
|
127
84
|
})
|
|
128
|
-
expect(link.children?.[0]).toMatchObject({ text: 'label' })
|
|
129
|
-
})
|
|
130
85
|
|
|
131
|
-
|
|
132
|
-
const { state } = convert('```ts\nconst x = 1\nconst y = 2\n```')
|
|
133
|
-
const code = state.root.children[0] as unknown as {
|
|
134
|
-
type: string
|
|
86
|
+
const code = convert('```ts\nconst x = 1\n```').state.root.children[0] as unknown as {
|
|
135
87
|
language: string
|
|
136
88
|
children: Array<{ type: string; text?: string }>
|
|
137
89
|
}
|
|
138
|
-
expect(code).
|
|
90
|
+
expect(code.language).toBe('typescript')
|
|
139
91
|
expect(code.children[0]).toMatchObject({ type: 'code-highlight', text: 'const x = 1' })
|
|
140
|
-
expect(code.children[1]).toMatchObject({ type: 'linebreak' })
|
|
141
|
-
expect(code.children[2]).toMatchObject({ type: 'code-highlight', text: 'const y = 2' })
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
test('thematic break emits horizontalrule', () => {
|
|
145
|
-
const { state } = convert('---')
|
|
146
|
-
expect(state.root.children[0]).toMatchObject({ type: 'horizontalrule' })
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
test('blockquote flattens single paragraph into inline children', () => {
|
|
150
|
-
const { state } = convert('> hello')
|
|
151
|
-
const quote = state.root.children[0] as unknown as {
|
|
152
|
-
type: string
|
|
153
|
-
children: Array<{ type: string; text?: string }>
|
|
154
|
-
}
|
|
155
|
-
expect(quote.type).toBe('quote')
|
|
156
|
-
expect(quote.children).toEqual([expect.objectContaining({ type: 'text', text: 'hello' })])
|
|
157
92
|
})
|
|
158
93
|
|
|
159
|
-
test('
|
|
160
|
-
|
|
161
|
-
expect(
|
|
162
|
-
|
|
163
|
-
|
|
94
|
+
test('maps blockquotes, rules, and image warnings', () => {
|
|
95
|
+
expect(convert('> hello').state.root.children[0]).toMatchObject({ type: 'quote' })
|
|
96
|
+
expect(convert('---').state.root.children[0]).toMatchObject({ type: 'horizontalrule' })
|
|
97
|
+
expect(convert('').warnings).toContainEqual(
|
|
98
|
+
expect.objectContaining({ kind: 'dropped-image' })
|
|
99
|
+
)
|
|
164
100
|
})
|
|
165
101
|
|
|
166
|
-
test('GFM
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
expect(warnings).toEqual([])
|
|
170
|
-
|
|
171
|
-
const table = state.root.children[0] as unknown as {
|
|
172
|
-
type: string
|
|
102
|
+
test('maps GFM tables with header state and inline formatting', () => {
|
|
103
|
+
const table = convert('| col |\n| - |\n| **bold** |').state.root
|
|
104
|
+
.children[0] as unknown as {
|
|
173
105
|
children: Array<{
|
|
174
|
-
type: string
|
|
175
106
|
children: Array<{
|
|
176
|
-
type: string
|
|
177
107
|
headerState: number
|
|
178
|
-
children: Array<{
|
|
108
|
+
children: Array<{ children: Array<{ text: string; format: number }> }>
|
|
179
109
|
}>
|
|
180
110
|
}>
|
|
181
111
|
}
|
|
182
|
-
expect(table.
|
|
183
|
-
expect(table.children).
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
expect(headerRow.children[0]).toMatchObject({ type: 'tablecell', headerState: 1 })
|
|
189
|
-
expect(headerRow.children[1]).toMatchObject({ type: 'tablecell', headerState: 1 })
|
|
190
|
-
expect(bodyRow.children[0]).toMatchObject({ type: 'tablecell', headerState: 0 })
|
|
191
|
-
expect(bodyRow.children[1]).toMatchObject({ type: 'tablecell', headerState: 0 })
|
|
192
|
-
|
|
193
|
-
// Inline cell content is wrapped in a paragraph.
|
|
194
|
-
const firstHeaderCell = headerRow.children[0]
|
|
195
|
-
expect(firstHeaderCell.children[0].type).toBe('paragraph')
|
|
196
|
-
expect(firstHeaderCell.children[0].children[0]).toMatchObject({ text: 'a' })
|
|
197
|
-
expect(bodyRow.children[1].children[0].children[0]).toMatchObject({ text: '2' })
|
|
112
|
+
expect(table.children[0].children[0].headerState).toBe(1)
|
|
113
|
+
expect(table.children[1].children[0].headerState).toBe(0)
|
|
114
|
+
expect(table.children[1].children[0].children[0].children[0]).toMatchObject({
|
|
115
|
+
text: 'bold',
|
|
116
|
+
format: 1,
|
|
117
|
+
})
|
|
198
118
|
})
|
|
199
119
|
|
|
200
|
-
test('empty
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const table = state.root.children[0] as unknown as {
|
|
120
|
+
test('gives empty table cells a structurally valid empty paragraph', () => {
|
|
121
|
+
const table = convert('| a | b |\n| - | - |\n| | 2 |').state.root
|
|
122
|
+
.children[0] as unknown as {
|
|
204
123
|
children: Array<{
|
|
205
124
|
children: Array<{ children: Array<{ type: string; children: unknown[] }> }>
|
|
206
125
|
}>
|
|
207
126
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
test('inline formatting inside cells is preserved', () => {
|
|
213
|
-
const md = '| col |\n| - |\n| **bold** |\n'
|
|
214
|
-
const { state } = convert(md)
|
|
215
|
-
const table = state.root.children[0] as unknown as {
|
|
216
|
-
children: Array<{
|
|
217
|
-
children: Array<{
|
|
218
|
-
children: Array<{ children: Array<{ text: string; format: number }> }>
|
|
219
|
-
}>
|
|
220
|
-
}>
|
|
221
|
-
}
|
|
222
|
-
const bodyCellInline = table.children[1].children[0].children[0].children[0]
|
|
223
|
-
expect(bodyCellInline).toMatchObject({ text: 'bold', format: 1 })
|
|
127
|
+
expect(table.children[1].children[0].children[0]).toMatchObject({
|
|
128
|
+
type: 'paragraph',
|
|
129
|
+
children: [],
|
|
130
|
+
})
|
|
224
131
|
})
|
|
225
132
|
|
|
226
|
-
test('code fence
|
|
133
|
+
test('normalizes common code fence aliases and preserves known names', () => {
|
|
227
134
|
const cases: Array<[string, string]> = [
|
|
228
135
|
['ts', 'typescript'],
|
|
229
136
|
['js', 'javascript'],
|
|
230
137
|
['sh', 'bash'],
|
|
231
138
|
['yml', 'yaml'],
|
|
232
|
-
['tsx', 'tsx'],
|
|
139
|
+
['tsx', 'tsx'],
|
|
233
140
|
]
|
|
234
141
|
for (const [input, expected] of cases) {
|
|
235
|
-
const
|
|
236
|
-
|
|
142
|
+
const code = convert(`\`\`\`${input}\nx\n\`\`\``).state.root.children[0] as unknown as {
|
|
143
|
+
language: string
|
|
144
|
+
}
|
|
237
145
|
expect(code.language).toBe(expected)
|
|
238
146
|
}
|
|
239
147
|
})
|
|
240
148
|
|
|
241
|
-
test('hard-wrapped paragraph
|
|
242
|
-
const
|
|
243
|
-
'In a world where AI produces content fast,\ninto dozens of languages,\nwhy does a CMS matter?'
|
|
244
|
-
const { state } = convert(md)
|
|
245
|
-
const paragraph = state.root.children[0] as unknown as {
|
|
149
|
+
test('collapses hard-wrapped paragraph newlines', () => {
|
|
150
|
+
const paragraph = convert('first line,\nsecond line.').state.root.children[0] as unknown as {
|
|
246
151
|
children: Array<{ text: string }>
|
|
247
152
|
}
|
|
248
|
-
|
|
249
|
-
// <br> nodes and no embedded \n.
|
|
250
|
-
expect(paragraph.children).toHaveLength(1)
|
|
251
|
-
expect(paragraph.children[0].text).toBe(
|
|
252
|
-
'In a world where AI produces content fast, into dozens of languages, why does a CMS matter?'
|
|
253
|
-
)
|
|
153
|
+
expect(paragraph.children[0].text).toBe('first line, second line.')
|
|
254
154
|
})
|
|
255
155
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
expect(adm).toMatchObject({ type: 'admonition', admonitionType: 'note', title: 'Heads up' })
|
|
269
|
-
expect(adm.children).toHaveLength(2)
|
|
270
|
-
expect(adm.children[0]).toMatchObject({ type: 'paragraph' })
|
|
271
|
-
expect(adm.children[0].children[0].text).toBe('First line.')
|
|
272
|
-
expect(adm.children[1].children[0].text).toBe('Second line.')
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
test('inline formatting + links survive inside an admonition body', () => {
|
|
276
|
-
const md = [
|
|
277
|
-
':::warning[Careful]',
|
|
278
|
-
'This is **bold** and a [link](https://x.io).',
|
|
279
|
-
':::',
|
|
280
|
-
].join('\n')
|
|
281
|
-
const { state } = convert(md)
|
|
282
|
-
const adm = state.root.children[0] as unknown as {
|
|
283
|
-
admonitionType: string
|
|
284
|
-
children: Array<{ children: Array<{ type: string; text?: string; format?: number }> }>
|
|
285
|
-
}
|
|
286
|
-
expect(adm.admonitionType).toBe('warning')
|
|
287
|
-
const inline = adm.children[0].children
|
|
288
|
-
expect(inline).toContainEqual(expect.objectContaining({ text: 'bold', format: 1 }))
|
|
289
|
-
expect(inline).toContainEqual(expect.objectContaining({ type: 'link' }))
|
|
290
|
-
})
|
|
156
|
+
test('maps admonitions and ignores colon-bearing prose', () => {
|
|
157
|
+
const admonition = convert(':::warning[Careful]\nThis is **bold**.\n:::').state.root
|
|
158
|
+
.children[0] as unknown as {
|
|
159
|
+
admonitionType: string
|
|
160
|
+
title: string
|
|
161
|
+
children: Array<{ children: Array<{ text?: string; format?: number }> }>
|
|
162
|
+
}
|
|
163
|
+
expect(admonition).toMatchObject({ admonitionType: 'warning', title: 'Careful' })
|
|
164
|
+
expect(admonition.children[0].children).toContainEqual(
|
|
165
|
+
expect.objectContaining({ text: 'bold', format: 1 })
|
|
166
|
+
)
|
|
291
167
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
})
|
|
168
|
+
const prose = convert('Run at 9:30 with a 1:2:3 ratio and note:foo.')
|
|
169
|
+
expect(prose.warnings).toEqual([])
|
|
170
|
+
expect(prose.state.root.children[0]).toMatchObject({ type: 'paragraph' })
|
|
171
|
+
})
|
|
297
172
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
admonitionType: string
|
|
302
|
-
children: Array<{ type: string; children: unknown[] }>
|
|
303
|
-
}
|
|
304
|
-
expect(adm.admonitionType).toBe('danger')
|
|
305
|
-
expect(adm.children).toEqual([expect.objectContaining({ type: 'paragraph', children: [] })])
|
|
306
|
-
})
|
|
173
|
+
test('supports untitled, empty, and source-ordered admonitions', () => {
|
|
174
|
+
const untitled = convert(':::tip\nA quick tip.\n:::').state.root.children[0]
|
|
175
|
+
expect(untitled).toMatchObject({ admonitionType: 'tip', title: '' })
|
|
307
176
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
'heading',
|
|
313
|
-
'admonition',
|
|
314
|
-
'paragraph',
|
|
315
|
-
])
|
|
316
|
-
})
|
|
177
|
+
const empty = convert(':::danger[Stop]\n:::').state.root.children[0] as unknown as {
|
|
178
|
+
children: Array<{ type: string; children: unknown[] }>
|
|
179
|
+
}
|
|
180
|
+
expect(empty.children).toEqual([expect.objectContaining({ type: 'paragraph', children: [] })])
|
|
317
181
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
expect(paragraph.children[0].text).toBe('Run at 9:30 with a 1:2:3 ratio and note:foo.')
|
|
326
|
-
})
|
|
182
|
+
const ordered = convert('# Title\n\n:::note[Note]\nBody.\n:::\n\nAfter.').state.root.children
|
|
183
|
+
expect(ordered.map((node) => (node as { type: string }).type)).toEqual([
|
|
184
|
+
'heading',
|
|
185
|
+
'admonition',
|
|
186
|
+
'paragraph',
|
|
187
|
+
])
|
|
188
|
+
})
|
|
327
189
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
expect(state.root.children[0]).toMatchObject({ type: 'code' })
|
|
333
|
-
})
|
|
190
|
+
test('keeps admonition-looking fences inside code blocks as code', () => {
|
|
191
|
+
const state = convert('```\n:::note[Not an admonition]\nstill code\n:::\n```').state
|
|
192
|
+
expect(state.root.children).toHaveLength(1)
|
|
193
|
+
expect(state.root.children[0]).toMatchObject({ type: 'code' })
|
|
334
194
|
})
|
|
335
195
|
})
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Pure, headless-editor-free conversion. The output JSON is shaped to
|
|
13
13
|
* match what the runtime serializer in
|
|
14
|
-
* `
|
|
14
|
+
* `src/ui/byline/components/richtext-lexical/serialize`
|
|
15
15
|
* expects to render (heading.tag, list.tag + list.listType, code
|
|
16
16
|
* children of type 'code-highlight' interspersed with 'linebreak',
|
|
17
17
|
* link.attributes, etc.).
|
|
@@ -47,7 +47,7 @@ import type {
|
|
|
47
47
|
import type { AdmonitionDirective } from './parse-markdown.js'
|
|
48
48
|
|
|
49
49
|
// Lexical inline format bits — kept in sync with
|
|
50
|
-
//
|
|
50
|
+
// src/ui/byline/components/richtext-lexical/serialize/richtext-node-formats.ts
|
|
51
51
|
const IS_BOLD = 1
|
|
52
52
|
const IS_ITALIC = 1 << 1
|
|
53
53
|
const IS_STRIKETHROUGH = 1 << 2
|
|
@@ -33,10 +33,6 @@ describe('stripLeadingH1IfMatches', () => {
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
test('match flattens inline formatting in the H1', () => {
|
|
36
|
-
// mdast-util-to-string drops backticks / emphasis markers from the
|
|
37
|
-
// H1's inline structure. Frontmatter titles are plain prose, so a
|
|
38
|
-
// body H1 of '# Client SDK (`@byline/client`)' compares equal to a
|
|
39
|
-
// frontmatter title of 'Client SDK (@byline/client)'.
|
|
40
36
|
const root = parse('# Client SDK (`@byline/client`)\n\nbody')
|
|
41
37
|
const out = stripLeadingH1IfMatches(root, 'Client SDK (@byline/client)')
|
|
42
38
|
expect(out.children).toHaveLength(1)
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
* listed — that list is the outstanding-translation backlog. Re-run as
|
|
17
17
|
* translation progresses; it is idempotent (each doc is its own transaction).
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* pnpm tsx byline/scripts/re-anchor.ts --to fr
|
|
20
|
+
* pnpm tsx byline/scripts/re-anchor.ts --to fr --collection pages
|
|
21
|
+
* pnpm tsx byline/scripts/re-anchor.ts --to fr --dry-run
|
|
22
22
|
*
|
|
23
23
|
* See docs/07-internationalization/index.md.
|
|
24
24
|
*/
|
|
@@ -59,11 +59,12 @@ import {
|
|
|
59
59
|
getCollectionDefinition,
|
|
60
60
|
getServerConfig,
|
|
61
61
|
getWorkflowStatuses,
|
|
62
|
-
type StoredFileValue,
|
|
63
62
|
} from '@byline/core'
|
|
64
63
|
import { extractImageMeta, generateImageVariants, isBypassMimeType } from '@byline/core/image'
|
|
65
64
|
import { uploadField as coreUploadField } from '@byline/core/services'
|
|
66
65
|
|
|
66
|
+
import type { CollectionFieldsByPath, MediaFields } from '../generated/collection-types.js'
|
|
67
|
+
|
|
67
68
|
const COLLECTION_PATH = 'media'
|
|
68
69
|
const FIELD_NAME = 'image'
|
|
69
70
|
|
|
@@ -92,7 +93,7 @@ async function run(): Promise<void> {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
const requestContext = createSuperAdminContext({ id: 'regenerate-media-script' })
|
|
95
|
-
const client = createBylineClient({ config, requestContext })
|
|
96
|
+
const client = createBylineClient<CollectionFieldsByPath>({ config, requestContext })
|
|
96
97
|
|
|
97
98
|
const { id: collectionId, version: collectionVersion } =
|
|
98
99
|
await client.resolveCollectionRecord(COLLECTION_PATH)
|
|
@@ -108,7 +109,7 @@ async function run(): Promise<void> {
|
|
|
108
109
|
id: string
|
|
109
110
|
path: string
|
|
110
111
|
status: string
|
|
111
|
-
fields:
|
|
112
|
+
fields: MediaFields
|
|
112
113
|
}[] = []
|
|
113
114
|
const pageSize = 100
|
|
114
115
|
for (let page = 1; ; page++) {
|
|
@@ -123,7 +124,7 @@ async function run(): Promise<void> {
|
|
|
123
124
|
id: d.id,
|
|
124
125
|
path: d.path,
|
|
125
126
|
status: d.status,
|
|
126
|
-
fields: d.fields
|
|
127
|
+
fields: d.fields,
|
|
127
128
|
})
|
|
128
129
|
}
|
|
129
130
|
if (result.docs.length < pageSize) break
|
|
@@ -154,7 +155,7 @@ async function run(): Promise<void> {
|
|
|
154
155
|
let skipped = 0
|
|
155
156
|
|
|
156
157
|
for (const doc of allDocs) {
|
|
157
|
-
const image = doc.fields[FIELD_NAME]
|
|
158
|
+
const image = doc.fields[FIELD_NAME]
|
|
158
159
|
if (image == null || !image.storagePath) {
|
|
159
160
|
console.log(` - skip ${doc.id} (${doc.path}) — no image value`)
|
|
160
161
|
skipped += 1
|
|
@@ -28,8 +28,9 @@ import {
|
|
|
28
28
|
import { migrate, postgresSearch } from '@byline/search-postgres'
|
|
29
29
|
import { localStorageProvider } from '@byline/storage-local'
|
|
30
30
|
|
|
31
|
-
import { getAdminBylineClient } from './
|
|
31
|
+
import { getAdminBylineClient } from './clients.server.js'
|
|
32
32
|
import { collections } from './collections/index.js'
|
|
33
|
+
import { serverHooks } from './collections/server-hooks.js'
|
|
33
34
|
import { i18n } from './i18n.js'
|
|
34
35
|
import { DEFAULT_SERVER_URL, routes } from './routes.js'
|
|
35
36
|
|
|
@@ -139,6 +140,7 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
|
|
|
139
140
|
i18n,
|
|
140
141
|
routes,
|
|
141
142
|
collections,
|
|
143
|
+
hooks: serverHooks,
|
|
142
144
|
db,
|
|
143
145
|
adminStore,
|
|
144
146
|
// Site-wide default storage provider — used by any upload collection
|
|
@@ -163,8 +165,7 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
|
|
|
163
165
|
}),
|
|
164
166
|
// S3-compatible alternative (AWS S3 / Cloudflare R2 / MinIO). Replace
|
|
165
167
|
// the `localStorageProvider` block above with the call below and add
|
|
166
|
-
// the corresponding `BYLINE_STORAGE_S3_*` entries to your `.env.local
|
|
167
|
-
// (see `apps/webapp/.env.local.example`).
|
|
168
|
+
// the corresponding `BYLINE_STORAGE_S3_*` entries to your `.env.local`.
|
|
168
169
|
//
|
|
169
170
|
// On AWS with an IAM role / instance profile, omit `accessKeyId` and
|
|
170
171
|
// `secretAccessKey` so the SDK resolves credentials via its default
|
|
@@ -204,7 +205,7 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
|
|
|
204
205
|
//
|
|
205
206
|
// We pass `getAdminBylineClient` (not the public client) because
|
|
206
207
|
// admin server fns are the only call sites that read documents in
|
|
207
|
-
// the admin
|
|
208
|
+
// the admin host today — the populate phase inherits whichever
|
|
208
209
|
// actor the request resolved. A future public-facing host would
|
|
209
210
|
// register its own client whose `requestContext` factory resolves
|
|
210
211
|
// an end-user actor instead. See `@byline/host-tanstack-start/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { devtools } from '@tanstack/devtools-vite'
|
|
2
|
+
import { bylineClientHookBoundary } from '@byline/host-tanstack-start/vite'
|
|
2
3
|
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
3
4
|
|
|
4
5
|
import tailwindcss from '@tailwindcss/vite'
|
|
@@ -158,6 +159,7 @@ const config = defineConfig({
|
|
|
158
159
|
},
|
|
159
160
|
plugins: [
|
|
160
161
|
browserAsyncHooksAlias(),
|
|
162
|
+
bylineClientHookBoundary(),
|
|
161
163
|
devtools(),
|
|
162
164
|
nitro({
|
|
163
165
|
preset: 'node',
|