@byline/cli 3.21.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +24 -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 +175 -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/collection-types.contract.ts +26 -1
- package/dist/templates/byline/collections/index.ts +1 -1
- package/dist/templates/byline/collections/server-hooks.ts +7 -0
- package/dist/templates/byline/generated/collection-types.ts +14 -6
- package/dist/templates/byline/routes.ts +7 -6
- package/dist/templates/byline/server.config.ts +10 -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/collection-types.contract.ts +26 -1
- 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/index.ts +1 -1
- 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/generated/collection-types.ts +138 -130
- 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 +10 -11
- 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-operation.ts +112 -0
- package/dist/templates/byline-examples/scripts/regenerate-media.ts +129 -92
- package/dist/templates/byline-examples/server.config.ts +14 -6
- 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/blocks/richtext-block/index.tsx +1 -1
- 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 +150 -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 +311 -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
|
@@ -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
|
*/
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maintenance helpers for `regenerate-media.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the executable so the safety-critical behaviour can be
|
|
5
|
+
* unit tested without loading the application server config or touching real
|
|
6
|
+
* storage.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CollectionHandle } from '@byline/client'
|
|
10
|
+
import {
|
|
11
|
+
getDefaultStatus,
|
|
12
|
+
getWorkflowStatuses,
|
|
13
|
+
type CollectionDefinition,
|
|
14
|
+
type IDbAdapter,
|
|
15
|
+
type StoredFileValue,
|
|
16
|
+
} from '@byline/core'
|
|
17
|
+
|
|
18
|
+
export function storedFilePaths(value: StoredFileValue): Set<string> {
|
|
19
|
+
return new Set([
|
|
20
|
+
value.storagePath,
|
|
21
|
+
...(value.variants ?? [])
|
|
22
|
+
.map((variant) => variant.storagePath)
|
|
23
|
+
.filter((storagePath): storagePath is string => Boolean(storagePath)),
|
|
24
|
+
])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reject a partial image-processor result before it replaces a complete
|
|
29
|
+
* persisted value. Core uploads deliberately tolerate an individual variant
|
|
30
|
+
* failure for interactive requests; this maintenance operation must be
|
|
31
|
+
* stricter because it is about bringing every stored asset up to the schema.
|
|
32
|
+
*/
|
|
33
|
+
export function assertCompleteVariantSet(
|
|
34
|
+
storedFile: StoredFileValue,
|
|
35
|
+
expectedVariantNames: readonly string[]
|
|
36
|
+
): void {
|
|
37
|
+
const expected = [...expectedVariantNames].sort()
|
|
38
|
+
const actual = (storedFile.variants ?? []).map((variant) => variant.name).sort()
|
|
39
|
+
if (
|
|
40
|
+
expected.length !== actual.length ||
|
|
41
|
+
expected.some((variantName, index) => variantName !== actual[index])
|
|
42
|
+
) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`regenerate-media: incomplete variant set for '${storedFile.storagePath}' ` +
|
|
45
|
+
`(expected: ${expected.join(', ') || 'none'}; generated: ${actual.join(', ') || 'none'}).`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Write the regenerated fields and restore the captured workflow status as
|
|
52
|
+
* one transaction.
|
|
53
|
+
*
|
|
54
|
+
* `CollectionHandle.update()` creates a new immutable version at the
|
|
55
|
+
* collection's default status. For this maintenance operation, changing that
|
|
56
|
+
* newly-created row to the captured status inside the same transaction is not
|
|
57
|
+
* an editorial transition: no caller can observe the intermediate draft, and
|
|
58
|
+
* an archived item never passes through published. The final version row and
|
|
59
|
+
* its normal `document.updated` activity entry retain the resulting status;
|
|
60
|
+
* the explicit status mutation is additionally audited as a system action.
|
|
61
|
+
*
|
|
62
|
+
* Direct adapter commands are intentional here and should not be copied into
|
|
63
|
+
* ordinary application writes. Scripts are the supported escape hatch for
|
|
64
|
+
* migrations that must preserve metadata while still using the lifecycle for
|
|
65
|
+
* the content update itself.
|
|
66
|
+
*/
|
|
67
|
+
export async function replaceMediaVersionPreservingStatus(params: {
|
|
68
|
+
db: IDbAdapter
|
|
69
|
+
definition: CollectionDefinition
|
|
70
|
+
collectionId: string
|
|
71
|
+
handle: Pick<CollectionHandle, 'update'>
|
|
72
|
+
documentId: string
|
|
73
|
+
fields: Record<string, any>
|
|
74
|
+
targetStatus: string
|
|
75
|
+
}): Promise<{ documentVersionId: string }> {
|
|
76
|
+
const statuses = getWorkflowStatuses(params.definition).map((status) => status.name)
|
|
77
|
+
if (!statuses.includes(params.targetStatus)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`regenerate-media: status '${params.targetStatus}' is not declared on collection ` +
|
|
80
|
+
`'${params.definition.path}' (declared: ${statuses.join(', ')}).`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const defaultStatus = getDefaultStatus(params.definition)
|
|
85
|
+
return params.db.withTransaction(async () => {
|
|
86
|
+
const result = await params.handle.update(params.documentId, params.fields)
|
|
87
|
+
if (params.targetStatus === defaultStatus) return result
|
|
88
|
+
|
|
89
|
+
await params.db.commands.documents.setDocumentStatus({
|
|
90
|
+
document_version_id: result.documentVersionId,
|
|
91
|
+
status: params.targetStatus,
|
|
92
|
+
})
|
|
93
|
+
if (params.targetStatus === 'published') {
|
|
94
|
+
await params.db.commands.documents.archivePublishedVersions({
|
|
95
|
+
document_id: params.documentId,
|
|
96
|
+
excludeVersionId: result.documentVersionId,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
await params.db.commands.audit.append({
|
|
100
|
+
documentId: params.documentId,
|
|
101
|
+
collectionId: params.collectionId,
|
|
102
|
+
actorId: null,
|
|
103
|
+
actorRealm: 'system',
|
|
104
|
+
action: 'document.status.changed',
|
|
105
|
+
field: 'status',
|
|
106
|
+
before: defaultStatus,
|
|
107
|
+
after: params.targetStatus,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
})
|
|
112
|
+
}
|