@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
|
@@ -23,21 +23,28 @@
|
|
|
23
23
|
* The script orchestrates the same two-step flow the admin UI uses
|
|
24
24
|
* for an existing document — upload (createDocument: false) followed by
|
|
25
25
|
* a document update — but as a single "one-shot" Client SDK pass per
|
|
26
|
-
* media item. Variants and the original written under fresh paths
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* media item. Variants and the original are written under fresh paths.
|
|
27
|
+
* Previous files are retained because immutable historical versions — or an
|
|
28
|
+
* older published version while the current version is draft — may still
|
|
29
|
+
* reference them. Storage reclamation requires a separate reference-aware
|
|
30
|
+
* garbage collector.
|
|
29
31
|
*
|
|
30
32
|
* Bypass-MIME images (SVG / GIF) are passed through untouched — Sharp
|
|
31
33
|
* does not produce variants for them, so the only effect is a new
|
|
32
34
|
* storagePath on a re-stored copy.
|
|
33
35
|
*
|
|
34
|
-
* Status preservation: `updateDocument`
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* Status preservation: `updateDocument` stamps new versions with the
|
|
37
|
+
* workflow's default status (`'draft'` for media). The script wraps that
|
|
38
|
+
* update and a direct restoration of the captured status in one database
|
|
39
|
+
* transaction. This is a deliberate maintenance-only use of adapter
|
|
40
|
+
* commands: an archived item is never temporarily published, and callers
|
|
41
|
+
* cannot observe the intermediate draft. Run without concurrent editorial
|
|
42
|
+
* writes to the media collection.
|
|
43
|
+
*
|
|
44
|
+
* Safety: the generated variant names must exactly match the current schema
|
|
45
|
+
* before the document is changed. A failed upload/update is compensated by
|
|
46
|
+
* deleting the newly-written files, failures are collected per document, and
|
|
47
|
+
* the process exits non-zero after attempting the remaining items.
|
|
41
48
|
*
|
|
42
49
|
* Currently requires the local storage provider — pulls bytes back via
|
|
43
50
|
* the provider's `uploadDir`. Variant generation itself is provider-
|
|
@@ -53,17 +60,22 @@ import fs from 'node:fs'
|
|
|
53
60
|
import path from 'node:path'
|
|
54
61
|
|
|
55
62
|
import { createSuperAdminContext } from '@byline/auth'
|
|
56
|
-
import {
|
|
63
|
+
import { createBylineClient } from '@byline/client'
|
|
57
64
|
import {
|
|
58
65
|
type FieldUploadContext,
|
|
59
66
|
getCollectionDefinition,
|
|
60
67
|
getServerConfig,
|
|
61
|
-
getWorkflowStatuses,
|
|
62
|
-
type StoredFileValue,
|
|
63
68
|
} from '@byline/core'
|
|
64
69
|
import { extractImageMeta, generateImageVariants, isBypassMimeType } from '@byline/core/image'
|
|
65
70
|
import { uploadField as coreUploadField } from '@byline/core/services'
|
|
66
71
|
|
|
72
|
+
import type { MediaFields } from '@byline/generated-types'
|
|
73
|
+
import {
|
|
74
|
+
assertCompleteVariantSet,
|
|
75
|
+
replaceMediaVersionPreservingStatus,
|
|
76
|
+
storedFilePaths,
|
|
77
|
+
} from './regenerate-media-operation.js'
|
|
78
|
+
|
|
67
79
|
const COLLECTION_PATH = 'media'
|
|
68
80
|
const FIELD_NAME = 'image'
|
|
69
81
|
|
|
@@ -102,13 +114,13 @@ async function run(): Promise<void> {
|
|
|
102
114
|
// Snapshot the full set up-front. Each update bumps `updated_at` and
|
|
103
115
|
// reorders the default sort, so paging through a moving window would
|
|
104
116
|
// either skip or re-visit rows. Capture `status` so we can restore
|
|
105
|
-
// each doc to its original workflow slot
|
|
106
|
-
//
|
|
117
|
+
// each doc to its original workflow slot inside the replacement
|
|
118
|
+
// transaction.
|
|
107
119
|
const allDocs: {
|
|
108
120
|
id: string
|
|
109
121
|
path: string
|
|
110
122
|
status: string
|
|
111
|
-
fields:
|
|
123
|
+
fields: MediaFields
|
|
112
124
|
}[] = []
|
|
113
125
|
const pageSize = 100
|
|
114
126
|
for (let page = 1; ; page++) {
|
|
@@ -123,7 +135,7 @@ async function run(): Promise<void> {
|
|
|
123
135
|
id: d.id,
|
|
124
136
|
path: d.path,
|
|
125
137
|
status: d.status,
|
|
126
|
-
fields: d.fields
|
|
138
|
+
fields: d.fields,
|
|
127
139
|
})
|
|
128
140
|
}
|
|
129
141
|
if (result.docs.length < pageSize) break
|
|
@@ -150,113 +162,138 @@ async function run(): Promise<void> {
|
|
|
150
162
|
},
|
|
151
163
|
}
|
|
152
164
|
|
|
165
|
+
const imageField = definition.fields.find((field) => field.name === FIELD_NAME)
|
|
166
|
+
if (imageField == null || (imageField.type !== 'image' && imageField.type !== 'file')) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`regenerate-media: '${COLLECTION_PATH}.${FIELD_NAME}' is not an image/file field.`
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
if (imageField.upload == null) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`regenerate-media: '${COLLECTION_PATH}.${FIELD_NAME}' has no upload configuration.`
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
153
177
|
let processed = 0
|
|
154
178
|
let skipped = 0
|
|
179
|
+
const failures: Error[] = []
|
|
155
180
|
|
|
156
181
|
for (const doc of allDocs) {
|
|
157
|
-
const image = doc.fields[FIELD_NAME]
|
|
182
|
+
const image = doc.fields[FIELD_NAME]
|
|
158
183
|
if (image == null || !image.storagePath) {
|
|
159
184
|
console.log(` - skip ${doc.id} (${doc.path}) — no image value`)
|
|
160
185
|
skipped += 1
|
|
161
186
|
continue
|
|
162
187
|
}
|
|
163
188
|
|
|
189
|
+
const oldPaths = storedFilePaths(image)
|
|
164
190
|
const sourceAbsolutePath = path.join(uploadDir, image.storagePath)
|
|
165
191
|
let buffer: Buffer
|
|
166
192
|
try {
|
|
167
193
|
buffer = await fs.promises.readFile(sourceAbsolutePath)
|
|
168
194
|
} catch (err) {
|
|
169
195
|
console.error(
|
|
170
|
-
` !
|
|
196
|
+
` ! fail ${doc.id} (${doc.path}) — failed to read original at ${sourceAbsolutePath}:`,
|
|
171
197
|
err
|
|
172
198
|
)
|
|
173
|
-
|
|
199
|
+
failures.push(asDocumentError(doc.id, doc.path, err))
|
|
174
200
|
continue
|
|
175
201
|
}
|
|
176
202
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
203
|
+
let freshPaths = new Set<string>()
|
|
204
|
+
try {
|
|
205
|
+
const uploadResult = await coreUploadField(
|
|
206
|
+
{ ...baseUploadCtx, requestContext },
|
|
207
|
+
{
|
|
208
|
+
buffer,
|
|
209
|
+
originalFilename: image.originalFilename || image.filename,
|
|
210
|
+
mimeType: image.mimeType,
|
|
211
|
+
fileSize: buffer.byteLength,
|
|
212
|
+
shouldCreateDocument: false,
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
freshPaths = storedFilePaths(uploadResult.storedFile)
|
|
190
216
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// the slot the doc originally occupied.
|
|
196
|
-
await restoreStatus(handle, definition, doc.id, doc.status)
|
|
217
|
+
const expectedVariantNames = isBypassMimeType(image.mimeType)
|
|
218
|
+
? []
|
|
219
|
+
: (imageField.upload.sizes ?? []).map((size) => size.name)
|
|
220
|
+
assertCompleteVariantSet(uploadResult.storedFile, expectedVariantNames)
|
|
197
221
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
]
|
|
205
|
-
for (const orphan of orphans) {
|
|
206
|
-
try {
|
|
207
|
-
await storage.delete(orphan)
|
|
208
|
-
} catch (err) {
|
|
209
|
-
console.warn(` ! failed to delete orphan ${orphan}:`, err)
|
|
222
|
+
const collisions = [...freshPaths].filter((storagePath) => oldPaths.has(storagePath))
|
|
223
|
+
if (collisions.length > 0) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`regenerate-media: upload hooks reused existing storage path(s): ${collisions.join(', ')}. ` +
|
|
226
|
+
'Fresh paths are required for an atomic replacement.'
|
|
227
|
+
)
|
|
210
228
|
}
|
|
211
|
-
}
|
|
212
229
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
230
|
+
const nextFields = { ...doc.fields, [FIELD_NAME]: uploadResult.storedFile }
|
|
231
|
+
await replaceMediaVersionPreservingStatus({
|
|
232
|
+
db: config.db,
|
|
233
|
+
definition,
|
|
234
|
+
collectionId,
|
|
235
|
+
handle,
|
|
236
|
+
documentId: doc.id,
|
|
237
|
+
fields: nextFields,
|
|
238
|
+
targetStatus: doc.status,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const variantSummary =
|
|
242
|
+
uploadResult.storedFile.variants?.map((v) => `${v.name}:${v.format ?? '?'}`).join(', ') ??
|
|
243
|
+
'(none)'
|
|
244
|
+
console.log(
|
|
245
|
+
` - ${doc.id} (${doc.path}) → ${uploadResult.storedFile.storagePath} [${variantSummary}]`
|
|
246
|
+
)
|
|
247
|
+
processed += 1
|
|
248
|
+
} catch (err) {
|
|
249
|
+
// The document transaction did not commit. Remove every fresh path we
|
|
250
|
+
// know about, but never delete a path that belonged to the old value — a
|
|
251
|
+
// deterministic beforeStore hook may already have overwritten it.
|
|
252
|
+
const rollbackPaths = new Set([...freshPaths].filter((storagePath) => !oldPaths.has(storagePath)))
|
|
253
|
+
const cleanupErrors = await deletePathsBestEffort(storage, rollbackPaths)
|
|
254
|
+
const failure = asDocumentError(doc.id, doc.path, err)
|
|
255
|
+
failures.push(
|
|
256
|
+
cleanupErrors.length === 0
|
|
257
|
+
? failure
|
|
258
|
+
: new AggregateError(
|
|
259
|
+
[failure, ...cleanupErrors],
|
|
260
|
+
`regenerate-media: operation and rollback cleanup failed for '${doc.path}'`
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
console.error(` ! fail ${doc.id} (${doc.path}):`, err)
|
|
264
|
+
}
|
|
220
265
|
}
|
|
221
266
|
|
|
222
267
|
console.log(
|
|
223
|
-
`regenerate-media: done. processed=${processed}, skipped=${skipped},
|
|
268
|
+
`regenerate-media: done. processed=${processed}, skipped=${skipped}, ` +
|
|
269
|
+
`failed=${failures.length}, total=${allDocs.length}.`
|
|
224
270
|
)
|
|
271
|
+
if (failures.length > 0) {
|
|
272
|
+
throw new AggregateError(failures, `regenerate-media: ${failures.length} document(s) failed`)
|
|
273
|
+
}
|
|
225
274
|
}
|
|
226
275
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
*/
|
|
241
|
-
async function restoreStatus(
|
|
242
|
-
handle: CollectionHandle,
|
|
243
|
-
definition: Parameters<typeof getWorkflowStatuses>[0],
|
|
244
|
-
documentId: string,
|
|
245
|
-
targetStatus: string
|
|
246
|
-
): Promise<void> {
|
|
247
|
-
const statuses = getWorkflowStatuses(definition).map((s) => s.name)
|
|
248
|
-
const targetIndex = statuses.indexOf(targetStatus)
|
|
249
|
-
if (targetIndex === -1) {
|
|
250
|
-
throw new Error(
|
|
251
|
-
`restoreStatus: status '${targetStatus}' is not declared on collection ` +
|
|
252
|
-
`'${definition.path}' (declared: ${statuses.join(', ')}).`
|
|
253
|
-
)
|
|
254
|
-
}
|
|
255
|
-
// The new version is always at index 0 (workflow default). Anything at
|
|
256
|
-
// index > 0 needs that many forward transitions.
|
|
257
|
-
for (let i = 1; i <= targetIndex; i++) {
|
|
258
|
-
await handle.changeStatus(documentId, statuses[i] as string)
|
|
276
|
+
async function deletePathsBestEffort(
|
|
277
|
+
storage: NonNullable<ReturnType<typeof getServerConfig>['storage']>,
|
|
278
|
+
storagePaths: Iterable<string>
|
|
279
|
+
): Promise<Error[]> {
|
|
280
|
+
const errors: Error[] = []
|
|
281
|
+
for (const storagePath of storagePaths) {
|
|
282
|
+
try {
|
|
283
|
+
await storage.delete(storagePath)
|
|
284
|
+
} catch (err) {
|
|
285
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
286
|
+
errors.push(error)
|
|
287
|
+
console.warn(` ! failed to delete ${storagePath}:`, error)
|
|
288
|
+
}
|
|
259
289
|
}
|
|
290
|
+
return errors
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function asDocumentError(documentId: string, documentPath: string, err: unknown): Error {
|
|
294
|
+
return new Error(`regenerate-media: '${documentPath}' (${documentId}) failed`, {
|
|
295
|
+
cause: err,
|
|
296
|
+
})
|
|
260
297
|
}
|
|
261
298
|
|
|
262
299
|
run()
|
|
@@ -28,8 +28,10 @@ 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 '@byline/client/server'
|
|
32
|
+
import { registerTanstackStartHostBridge } from '@byline/host-tanstack-start/integrations/host-bridge'
|
|
32
33
|
import { collections } from './collections/index.js'
|
|
34
|
+
import { serverHooks } from './collections/server-hooks.js'
|
|
33
35
|
import { i18n } from './i18n.js'
|
|
34
36
|
import { DEFAULT_SERVER_URL, routes } from './routes.js'
|
|
35
37
|
|
|
@@ -134,11 +136,18 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
|
|
|
134
136
|
signingSecret,
|
|
135
137
|
})
|
|
136
138
|
|
|
139
|
+
// Register the TanStack Start `HostRequestBridge` before any
|
|
140
|
+
// request-bound client getter (`@byline/client/server`) can run —
|
|
141
|
+
// structural at server boot rather than dependent on which host
|
|
142
|
+
// module happens to load first.
|
|
143
|
+
registerTanstackStartHostBridge()
|
|
144
|
+
|
|
137
145
|
const core = await initBylineCore<AdminStore>({
|
|
138
146
|
serverURL,
|
|
139
147
|
i18n,
|
|
140
148
|
routes,
|
|
141
149
|
collections,
|
|
150
|
+
hooks: serverHooks,
|
|
142
151
|
db,
|
|
143
152
|
adminStore,
|
|
144
153
|
// Site-wide default storage provider — used by any upload collection
|
|
@@ -163,8 +172,7 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
|
|
|
163
172
|
}),
|
|
164
173
|
// S3-compatible alternative (AWS S3 / Cloudflare R2 / MinIO). Replace
|
|
165
174
|
// 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`).
|
|
175
|
+
// the corresponding `BYLINE_STORAGE_S3_*` entries to your `.env.local`.
|
|
168
176
|
//
|
|
169
177
|
// On AWS with an IAM role / instance profile, omit `accessKeyId` and
|
|
170
178
|
// `secretAccessKey` so the SDK resolves credentials via its default
|
|
@@ -204,11 +212,11 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
|
|
|
204
212
|
//
|
|
205
213
|
// We pass `getAdminBylineClient` (not the public client) because
|
|
206
214
|
// admin server fns are the only call sites that read documents in
|
|
207
|
-
// the admin
|
|
215
|
+
// the admin host today — the populate phase inherits whichever
|
|
208
216
|
// actor the request resolved. A future public-facing host would
|
|
209
217
|
// register its own client whose `requestContext` factory resolves
|
|
210
|
-
// an end-user actor instead. See
|
|
211
|
-
//
|
|
218
|
+
// an end-user actor instead. See `packages/client/src/server/
|
|
219
|
+
// clients.ts` (`@byline/client/server`) for how the admin client is built.
|
|
212
220
|
//
|
|
213
221
|
// Why a getter, not a value: `getAdminBylineClient()` reads the
|
|
214
222
|
// server config singleton, which is only populated *after*
|
|
@@ -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',
|
|
@@ -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
|
|
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:
|
|
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
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
{
|
|
68
|
-
<Container
|
|
69
|
-
className=
|
|
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
|
)}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Container } from '@byline/ui/react'
|
|
4
4
|
import cx from 'classnames'
|
|
5
5
|
|
|
6
|
-
import type { RichTextBlockData } from '
|
|
6
|
+
import type { RichTextBlockData } from '@byline/generated-types'
|
|
7
7
|
|
|
8
8
|
import { LexicalRichText } from '@/ui/byline/components/richtext-lexical'
|
|
9
9
|
import type { Locale } from '@/ui/byline/types/i18n'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
'use
|
|
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
|
-
<
|
|
33
|
-
{node
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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={
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|