@byline/host-tanstack-start 4.15.0 → 4.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/admin-shell/chrome/admin-menu-drawer.d.ts +2 -1
  2. package/dist/admin-shell/chrome/admin-menu-drawer.js +17 -5
  3. package/dist/admin-shell/chrome/dashboard.js +41 -5
  4. package/dist/admin-shell/chrome/dashboard.test.d.ts +8 -0
  5. package/dist/admin-shell/collections/api.d.ts +2 -2
  6. package/dist/admin-shell/collections/create.d.ts +2 -2
  7. package/dist/admin-shell/collections/create.js +1 -0
  8. package/dist/admin-shell/collections/edit.d.ts +2 -2
  9. package/dist/admin-shell/collections/edit.js +1 -0
  10. package/dist/admin-shell/collections/history.d.ts +4 -12
  11. package/dist/admin-shell/collections/history.js +112 -303
  12. package/dist/admin-shell/collections/reindex-button.d.ts +1 -1
  13. package/dist/admin-shell/collections/reindex-button.js +5 -1
  14. package/dist/admin-shell/collections/restore-version-modal.d.ts +4 -4
  15. package/dist/admin-shell/collections/restore-version-modal.js +8 -24
  16. package/dist/admin-shell/collections/version-history.d.ts +72 -0
  17. package/dist/admin-shell/collections/version-history.js +259 -0
  18. package/dist/admin-shell/singletons/history.d.ts +19 -0
  19. package/dist/admin-shell/singletons/history.js +170 -0
  20. package/dist/admin-shell/singletons/history.test.d.ts +8 -0
  21. package/dist/admin-shell/singletons/preview-link.d.ts +12 -0
  22. package/dist/admin-shell/singletons/preview-link.js +54 -0
  23. package/dist/admin-shell/singletons/view-menu.d.ts +13 -0
  24. package/dist/admin-shell/singletons/view-menu.js +116 -0
  25. package/dist/admin-shell/singletons/view.d.ts +18 -0
  26. package/dist/admin-shell/singletons/view.js +259 -0
  27. package/dist/admin-shell/singletons/view.test.d.ts +8 -0
  28. package/dist/integrations/analytics-agent.d.ts +20 -0
  29. package/dist/integrations/analytics-agent.js +57 -0
  30. package/dist/integrations/analytics-events.d.ts +51 -0
  31. package/dist/integrations/analytics-events.js +129 -0
  32. package/dist/integrations/api-utils.d.ts +18 -4
  33. package/dist/integrations/api-utils.js +10 -3
  34. package/dist/integrations/serve-analytics-agent.d.ts +8 -0
  35. package/dist/integrations/serve-analytics-agent.js +11 -0
  36. package/dist/routes/analytics-range.d.ts +12 -0
  37. package/dist/routes/analytics-range.js +35 -0
  38. package/dist/routes/create-admin-analytics-route.d.ts +8 -0
  39. package/dist/routes/create-admin-analytics-route.js +108 -0
  40. package/dist/routes/create-admin-dashboard-route.js +2 -2
  41. package/dist/routes/create-admin-layout-route.js +10 -4
  42. package/dist/routes/create-collection-api-route.js +3 -3
  43. package/dist/routes/create-collection-create-route.js +4 -3
  44. package/dist/routes/create-collection-edit-route.js +4 -3
  45. package/dist/routes/create-collection-history-route.js +4 -3
  46. package/dist/routes/create-collection-list-route.js +4 -3
  47. package/dist/routes/create-singleton-history-route.d.ts +8 -0
  48. package/dist/routes/create-singleton-history-route.js +99 -0
  49. package/dist/routes/create-singleton-route.d.ts +35 -0
  50. package/dist/routes/create-singleton-route.js +130 -0
  51. package/dist/routes/get-multi-collection-definition.d.ts +10 -0
  52. package/dist/routes/get-multi-collection-definition.js +6 -0
  53. package/dist/routes/get-singleton-definition.d.ts +10 -0
  54. package/dist/routes/get-singleton-definition.js +6 -0
  55. package/dist/routes/index.d.ts +3 -0
  56. package/dist/routes/index.js +3 -0
  57. package/dist/server-fns/actors.d.ts +16 -0
  58. package/dist/server-fns/actors.js +25 -0
  59. package/dist/server-fns/admin-document-presentation.d.ts +30 -0
  60. package/dist/server-fns/admin-document-presentation.js +32 -0
  61. package/dist/server-fns/analytics/index.d.ts +9 -0
  62. package/dist/server-fns/analytics/index.js +2 -0
  63. package/dist/server-fns/analytics/maintenance.d.ts +22 -0
  64. package/dist/server-fns/analytics/maintenance.js +42 -0
  65. package/dist/server-fns/analytics/queries.d.ts +25 -0
  66. package/dist/server-fns/analytics/queries.js +54 -0
  67. package/dist/server-fns/collections/actors.d.ts +1 -10
  68. package/dist/server-fns/collections/actors.js +1 -25
  69. package/dist/server-fns/collections/get.js +9 -26
  70. package/dist/server-fns/collections/reorder.js +1 -1
  71. package/dist/server-fns/collections/scheduled-publication.js +2 -2
  72. package/dist/server-fns/collections/upload.js +3 -3
  73. package/dist/server-fns/collections/utils.d.ts +1 -7
  74. package/dist/server-fns/collections/utils.js +1 -4
  75. package/dist/server-fns/serialise.d.ts +14 -0
  76. package/dist/server-fns/serialise.js +4 -0
  77. package/dist/server-fns/singleton-document-read.d.ts +20 -0
  78. package/dist/server-fns/singleton-document-read.js +10 -0
  79. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +12 -0
  80. package/dist/server-fns/singletons/cancel-scheduled-publish.js +7 -0
  81. package/dist/server-fns/singletons/change-status.d.ts +14 -0
  82. package/dist/server-fns/singletons/change-status.js +7 -0
  83. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +14 -0
  84. package/dist/server-fns/singletons/confirm-scheduled-publish.js +9 -0
  85. package/dist/server-fns/singletons/copy-to-locale.d.ts +18 -0
  86. package/dist/server-fns/singletons/copy-to-locale.js +11 -0
  87. package/dist/server-fns/singletons/find-by-version.d.ts +16 -0
  88. package/dist/server-fns/singletons/find-by-version.js +9 -0
  89. package/dist/server-fns/singletons/get-scheduled-publish.d.ts +12 -0
  90. package/dist/server-fns/singletons/get-scheduled-publish.js +7 -0
  91. package/dist/server-fns/singletons/get.d.ts +24 -0
  92. package/dist/server-fns/singletons/get.js +6 -0
  93. package/dist/server-fns/singletons/history.d.ts +31 -0
  94. package/dist/server-fns/singletons/history.js +22 -0
  95. package/dist/server-fns/singletons/index.d.ts +19 -0
  96. package/dist/server-fns/singletons/index.js +12 -0
  97. package/dist/server-fns/singletons/restore-version.d.ts +14 -0
  98. package/dist/server-fns/singletons/restore-version.js +7 -0
  99. package/dist/server-fns/singletons/schedule-publish.d.ts +16 -0
  100. package/dist/server-fns/singletons/schedule-publish.js +10 -0
  101. package/dist/server-fns/singletons/unpublish.d.ts +12 -0
  102. package/dist/server-fns/singletons/unpublish.js +7 -0
  103. package/dist/server-fns/singletons/update.d.ts +14 -0
  104. package/dist/server-fns/singletons/update.js +14 -0
  105. package/dist/vite/analytics-dev.d.ts +22 -0
  106. package/dist/vite/analytics-dev.js +28 -0
  107. package/dist/vite/index.d.ts +1 -0
  108. package/dist/vite/index.js +1 -0
  109. package/dist/vitest-jsdom.test.d.ts +8 -0
  110. package/package.json +28 -16
  111. package/src/admin-shell/chrome/admin-menu-drawer.tsx +19 -5
  112. package/src/admin-shell/chrome/dashboard.test.tsx +199 -0
  113. package/src/admin-shell/chrome/dashboard.tsx +43 -9
  114. package/src/admin-shell/collections/api.tsx +2 -2
  115. package/src/admin-shell/collections/create.tsx +6 -2
  116. package/src/admin-shell/collections/edit.tsx +6 -2
  117. package/src/admin-shell/collections/history.module.css +1 -1
  118. package/src/admin-shell/collections/history.tsx +162 -442
  119. package/src/admin-shell/collections/reindex-button.tsx +4 -2
  120. package/src/admin-shell/collections/restore-version-modal.tsx +19 -25
  121. package/src/admin-shell/collections/version-history.tsx +349 -0
  122. package/src/admin-shell/singletons/history.test.tsx +407 -0
  123. package/src/admin-shell/singletons/history.tsx +185 -0
  124. package/src/admin-shell/singletons/preview-link.tsx +75 -0
  125. package/src/admin-shell/singletons/view-menu.tsx +136 -0
  126. package/src/admin-shell/singletons/view.test.tsx +400 -0
  127. package/src/admin-shell/singletons/view.tsx +351 -0
  128. package/src/integrations/analytics-agent.tsx +91 -0
  129. package/src/integrations/analytics-events.test.node.ts +245 -0
  130. package/src/integrations/analytics-events.ts +224 -0
  131. package/src/integrations/api-utils.ts +34 -9
  132. package/src/integrations/serve-analytics-agent.test.node.ts +20 -0
  133. package/src/integrations/serve-analytics-agent.ts +19 -0
  134. package/src/routes/analytics-range.ts +55 -0
  135. package/src/routes/create-admin-analytics-route.test.node.ts +46 -0
  136. package/src/routes/create-admin-analytics-route.tsx +109 -0
  137. package/src/routes/create-admin-dashboard-route.test.node.ts +70 -0
  138. package/src/routes/create-admin-dashboard-route.tsx +10 -2
  139. package/src/routes/create-admin-layout-route.tsx +15 -8
  140. package/src/routes/create-collection-api-route.tsx +4 -4
  141. package/src/routes/create-collection-create-route.tsx +5 -8
  142. package/src/routes/create-collection-edit-route.tsx +5 -4
  143. package/src/routes/create-collection-history-route.tsx +5 -8
  144. package/src/routes/create-collection-list-route.tsx +5 -8
  145. package/src/routes/create-singleton-history-route.test.node.ts +148 -0
  146. package/src/routes/create-singleton-history-route.tsx +116 -0
  147. package/src/routes/create-singleton-route.test.node.ts +172 -0
  148. package/src/routes/create-singleton-route.tsx +176 -0
  149. package/src/routes/get-multi-collection-definition.ts +16 -0
  150. package/src/routes/get-singleton-definition.ts +16 -0
  151. package/src/routes/index.ts +3 -0
  152. package/src/server-fns/actors.ts +43 -0
  153. package/src/server-fns/admin-document-presentation.ts +78 -0
  154. package/src/server-fns/analytics/index.ts +23 -0
  155. package/src/server-fns/analytics/maintenance.ts +54 -0
  156. package/src/server-fns/analytics/queries.ts +86 -0
  157. package/src/server-fns/collections/actors.ts +1 -52
  158. package/src/server-fns/collections/get.ts +10 -49
  159. package/src/server-fns/collections/reorder.ts +1 -1
  160. package/src/server-fns/collections/scheduled-publication.ts +7 -1
  161. package/src/server-fns/collections/upload-singleton.test.node.ts +164 -0
  162. package/src/server-fns/collections/upload.ts +7 -3
  163. package/src/server-fns/collections/utils.ts +1 -9
  164. package/src/server-fns/serialise.ts +17 -0
  165. package/src/server-fns/singleton-document-read.ts +32 -0
  166. package/src/server-fns/singletons/cancel-scheduled-publish.ts +19 -0
  167. package/src/server-fns/singletons/change-status.ts +19 -0
  168. package/src/server-fns/singletons/confirm-scheduled-publish.ts +23 -0
  169. package/src/server-fns/singletons/copy-to-locale.ts +32 -0
  170. package/src/server-fns/singletons/find-by-version.ts +23 -0
  171. package/src/server-fns/singletons/get-scheduled-publish.ts +19 -0
  172. package/src/server-fns/singletons/get.ts +32 -0
  173. package/src/server-fns/singletons/history.ts +41 -0
  174. package/src/server-fns/singletons/index.ts +20 -0
  175. package/src/server-fns/singletons/restore-version.ts +19 -0
  176. package/src/server-fns/singletons/schedule-publish.ts +24 -0
  177. package/src/server-fns/singletons/singletons.test.node.ts +298 -0
  178. package/src/server-fns/singletons/unpublish.ts +19 -0
  179. package/src/server-fns/singletons/update.ts +31 -0
  180. package/src/vite/analytics-dev.test.node.ts +126 -0
  181. package/src/vite/analytics-dev.ts +56 -0
  182. package/src/vite/index.ts +1 -0
  183. package/src/vitest-jsdom.test.tsx +27 -0
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ export * from './cancel-scheduled-publish.js'
10
+ export * from './change-status.js'
11
+ export * from './confirm-scheduled-publish.js'
12
+ export * from './copy-to-locale.js'
13
+ export * from './find-by-version.js'
14
+ export * from './get.js'
15
+ export * from './get-scheduled-publish.js'
16
+ export * from './history.js'
17
+ export * from './restore-version.js'
18
+ export * from './schedule-publish.js'
19
+ export * from './unpublish.js'
20
+ export * from './update.js'
@@ -0,0 +1,19 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { createServerFn } from '@tanstack/react-start'
10
+
11
+ import { getAdminBylineClient } from '@byline/client/server'
12
+
13
+ import { serialise } from '../serialise.js'
14
+
15
+ export const restoreSingletonVersion = createServerFn({ method: 'POST' })
16
+ .validator((input: { singleton: string; versionId: string }) => input)
17
+ .handler(async ({ data }) =>
18
+ serialise(await getAdminBylineClient().singleton(data.singleton).restoreVersion(data.versionId))
19
+ )
@@ -0,0 +1,24 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { createServerFn } from '@tanstack/react-start'
10
+
11
+ import { getAdminBylineClient } from '@byline/client/server'
12
+
13
+ import { serialise } from '../serialise.js'
14
+
15
+ export const scheduleSingletonPublish = createServerFn({ method: 'POST' })
16
+ .validator((input: { singleton: string; publishAt: string; expectedVersionId: string }) => input)
17
+ .handler(async ({ data }) =>
18
+ serialise(
19
+ await getAdminBylineClient().singleton(data.singleton).schedulePublish({
20
+ publishAt: data.publishAt,
21
+ expectedVersionId: data.expectedVersionId,
22
+ })
23
+ )
24
+ )
@@ -0,0 +1,298 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
10
+
11
+ const mocks = vi.hoisted(() => ({
12
+ getAdminBylineClient: vi.fn(),
13
+ resolveActorLabels: vi.fn(),
14
+ }))
15
+
16
+ vi.mock('@tanstack/react-start', () => ({
17
+ createServerFn: () => {
18
+ let validate = (input: unknown) => input
19
+ const chain = {
20
+ validator(validator: (input: unknown) => unknown) {
21
+ validate = validator
22
+ return chain
23
+ },
24
+ handler(handler: (options: { data: any }) => Promise<unknown>) {
25
+ return async (options: { data: unknown }) => handler({ data: validate(options.data) })
26
+ },
27
+ }
28
+ return chain
29
+ },
30
+ }))
31
+
32
+ vi.mock('@byline/client/server', () => ({
33
+ getAdminBylineClient: mocks.getAdminBylineClient,
34
+ }))
35
+
36
+ vi.mock('../actors.js', () => ({
37
+ resolveActorLabels: mocks.resolveActorLabels,
38
+ }))
39
+
40
+ import { cancelSingletonScheduledPublish } from './cancel-scheduled-publish.js'
41
+ import { changeSingletonStatus } from './change-status.js'
42
+ import { confirmSingletonScheduledPublish } from './confirm-scheduled-publish.js'
43
+ import { copySingletonToLocale } from './copy-to-locale.js'
44
+ import { findSingletonByVersion } from './find-by-version.js'
45
+ import { getSingleton } from './get.js'
46
+ import { getSingletonScheduledPublish } from './get-scheduled-publish.js'
47
+ import { getSingletonHistory } from './history.js'
48
+ import { restoreSingletonVersion } from './restore-version.js'
49
+ import { scheduleSingletonPublish } from './schedule-publish.js'
50
+ import { unpublishSingleton } from './unpublish.js'
51
+ import { updateSingleton } from './update.js'
52
+
53
+ type ServerFunction = (options: { data: any }) => Promise<any>
54
+
55
+ const invoke = (serverFunction: unknown, data: Record<string, any>) =>
56
+ (serverFunction as ServerFunction)({ data })
57
+
58
+ const handle = {
59
+ get: vi.fn(),
60
+ update: vi.fn(),
61
+ changeStatus: vi.fn(),
62
+ unpublish: vi.fn(),
63
+ schedulePublish: vi.fn(),
64
+ confirmScheduledPublish: vi.fn(),
65
+ cancelScheduledPublish: vi.fn(),
66
+ getScheduledPublish: vi.fn(),
67
+ history: vi.fn(),
68
+ findByVersion: vi.fn(),
69
+ restoreVersion: vi.fn(),
70
+ copyToLocale: vi.fn(),
71
+ }
72
+
73
+ const client = { singleton: vi.fn(() => handle) }
74
+
75
+ describe('singleton server functions', () => {
76
+ beforeEach(() => {
77
+ vi.clearAllMocks()
78
+ mocks.getAdminBylineClient.mockReturnValue(client)
79
+ mocks.resolveActorLabels.mockResolvedValue({ actor: { label: 'Ada Editor' } })
80
+ })
81
+
82
+ it('delegates every operation to the singleton handle for the requested path', async () => {
83
+ const cases = [
84
+ {
85
+ serverFunction: getSingleton,
86
+ method: 'get',
87
+ input: { singleton: 'site-settings', locale: 'fr' },
88
+ args: [
89
+ {
90
+ locale: 'fr',
91
+ populate: undefined,
92
+ depth: undefined,
93
+ status: 'any',
94
+ onMissingLocale: 'empty',
95
+ lenient: true,
96
+ },
97
+ ],
98
+ result: { title: 'Site' },
99
+ },
100
+ {
101
+ serverFunction: updateSingleton,
102
+ method: 'update',
103
+ input: {
104
+ singleton: 'site-settings',
105
+ data: { title: 'Changed' },
106
+ locale: 'fr',
107
+ expectedVersionId: 'version-1',
108
+ },
109
+ args: [{ title: 'Changed' }, { locale: 'fr', expectedVersionId: 'version-1' }],
110
+ result: { versionId: 'version-2' },
111
+ },
112
+ {
113
+ serverFunction: changeSingletonStatus,
114
+ method: 'changeStatus',
115
+ input: { singleton: 'site-settings', status: 'published' },
116
+ args: ['published'],
117
+ result: { newStatus: 'published' },
118
+ },
119
+ {
120
+ serverFunction: unpublishSingleton,
121
+ method: 'unpublish',
122
+ input: { singleton: 'site-settings' },
123
+ args: [],
124
+ result: { archivedCount: 1 },
125
+ },
126
+ {
127
+ serverFunction: scheduleSingletonPublish,
128
+ method: 'schedulePublish',
129
+ input: {
130
+ singleton: 'site-settings',
131
+ publishAt: '2026-09-01T10:00:00Z',
132
+ expectedVersionId: 'version-2',
133
+ },
134
+ args: [
135
+ {
136
+ publishAt: '2026-09-01T10:00:00Z',
137
+ expectedVersionId: 'version-2',
138
+ },
139
+ ],
140
+ result: { state: 'armed' },
141
+ },
142
+ {
143
+ serverFunction: confirmSingletonScheduledPublish,
144
+ method: 'confirmScheduledPublish',
145
+ input: { singleton: 'site-settings', expectedVersionId: 'version-3' },
146
+ args: [{ expectedVersionId: 'version-3' }],
147
+ result: { state: 'armed' },
148
+ },
149
+ {
150
+ serverFunction: cancelSingletonScheduledPublish,
151
+ method: 'cancelScheduledPublish',
152
+ input: { singleton: 'site-settings' },
153
+ args: [],
154
+ result: { state: 'cancelled' },
155
+ },
156
+ {
157
+ serverFunction: getSingletonScheduledPublish,
158
+ method: 'getScheduledPublish',
159
+ input: { singleton: 'site-settings' },
160
+ args: [],
161
+ result: { state: 'armed' },
162
+ },
163
+ {
164
+ serverFunction: getSingletonHistory,
165
+ method: 'history',
166
+ input: {
167
+ singleton: 'site-settings',
168
+ params: { locale: 'all', page: 2, page_size: 10, order: 'created_at', desc: true },
169
+ },
170
+ args: [{ locale: 'all', page: 2, pageSize: 10, order: 'created_at', desc: true }],
171
+ result: {
172
+ docs: [{ createdBy: 'actor', versionId: 'version-1' }],
173
+ meta: { total: 1, page: 2, pageSize: 10, totalPages: 1 },
174
+ },
175
+ actors: true,
176
+ },
177
+ {
178
+ serverFunction: findSingletonByVersion,
179
+ method: 'findByVersion',
180
+ input: { singleton: 'site-settings', versionId: 'version-1', locale: 'fr' },
181
+ args: ['version-1', { locale: 'fr' }],
182
+ result: { title: 'Historical' },
183
+ },
184
+ {
185
+ serverFunction: restoreSingletonVersion,
186
+ method: 'restoreVersion',
187
+ input: { singleton: 'site-settings', versionId: 'version-1' },
188
+ args: ['version-1'],
189
+ result: { versionId: 'version-4' },
190
+ },
191
+ {
192
+ serverFunction: copySingletonToLocale,
193
+ method: 'copyToLocale',
194
+ input: {
195
+ singleton: 'site-settings',
196
+ sourceLocale: 'en',
197
+ targetLocale: 'fr',
198
+ overwrite: true,
199
+ },
200
+ args: [{ sourceLocale: 'en', targetLocale: 'fr', overwrite: true }],
201
+ result: { versionId: 'version-5' },
202
+ },
203
+ ] as const
204
+
205
+ for (const testCase of cases) {
206
+ const method = handle[testCase.method]
207
+ method.mockResolvedValueOnce(testCase.result)
208
+
209
+ const result = await invoke(testCase.serverFunction, testCase.input)
210
+
211
+ expect(client.singleton).toHaveBeenLastCalledWith('site-settings')
212
+ expect(method).toHaveBeenCalledWith(...testCase.args)
213
+ expect(result).toEqual(
214
+ testCase.actors
215
+ ? { ...testCase.result, actors: { actor: { label: 'Ada Editor' } } }
216
+ : testCase.result
217
+ )
218
+ }
219
+ })
220
+
221
+ it('forwards the optimistic version to update unchanged', async () => {
222
+ handle.update.mockResolvedValueOnce({ versionId: 'version-next' })
223
+
224
+ await invoke(updateSingleton, {
225
+ singleton: 'site-settings',
226
+ data: { title: 'Changed' },
227
+ expectedVersionId: 'version-current',
228
+ })
229
+
230
+ expect(handle.update).toHaveBeenCalledWith(
231
+ { title: 'Changed' },
232
+ { locale: undefined, expectedVersionId: 'version-current' }
233
+ )
234
+ })
235
+
236
+ it('preserves authentication failures from the handle', async () => {
237
+ const error = { code: 'ERR_UNAUTHENTICATED', message: 'sign in required' }
238
+ handle.get.mockRejectedValueOnce(error)
239
+
240
+ await expect(invoke(getSingleton, { singleton: 'site-settings' })).rejects.toBe(error)
241
+ })
242
+
243
+ it('preserves the requested empty history envelope from an unmaterialised slot', async () => {
244
+ handle.history.mockResolvedValueOnce({
245
+ docs: [],
246
+ meta: { total: 0, page: 4, pageSize: 30, totalPages: 0 },
247
+ })
248
+ mocks.resolveActorLabels.mockResolvedValueOnce({})
249
+
250
+ await expect(
251
+ invoke(getSingletonHistory, {
252
+ singleton: 'site-settings',
253
+ params: { page: 4, page_size: 30 },
254
+ })
255
+ ).resolves.toEqual({
256
+ docs: [],
257
+ meta: { total: 0, page: 4, pageSize: 30, totalPages: 0 },
258
+ actors: {},
259
+ })
260
+ expect(handle.history).toHaveBeenCalledWith({
261
+ locale: 'en',
262
+ page: 4,
263
+ pageSize: 30,
264
+ order: undefined,
265
+ desc: undefined,
266
+ })
267
+ expect(mocks.resolveActorLabels).toHaveBeenCalledWith([])
268
+ })
269
+
270
+ it('keeps missing-slot and stale-write failures distinguishable', async () => {
271
+ const missing = { code: 'ERR_NOT_FOUND', message: 'slot is empty' }
272
+ const stale = { code: 'ERR_CONFLICT', message: 'version changed' }
273
+ handle.changeStatus.mockRejectedValueOnce(missing)
274
+ handle.update.mockRejectedValueOnce(stale)
275
+
276
+ await expect(
277
+ invoke(changeSingletonStatus, { singleton: 'site-settings', status: 'published' })
278
+ ).rejects.toMatchObject({ code: 'ERR_NOT_FOUND' })
279
+ await expect(
280
+ invoke(updateSingleton, {
281
+ singleton: 'site-settings',
282
+ data: { title: 'Changed' },
283
+ expectedVersionId: 'stale',
284
+ })
285
+ ).rejects.toMatchObject({ code: 'ERR_CONFLICT' })
286
+ })
287
+
288
+ it('serialises schedule dates at the transport boundary', async () => {
289
+ handle.getScheduledPublish.mockResolvedValueOnce({
290
+ state: 'armed',
291
+ publishAt: new Date('2026-09-01T10:00:00Z'),
292
+ })
293
+
294
+ await expect(
295
+ invoke(getSingletonScheduledPublish, { singleton: 'site-settings' })
296
+ ).resolves.toMatchObject({ publishAt: '2026-09-01T10:00:00.000Z' })
297
+ })
298
+ })
@@ -0,0 +1,19 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { createServerFn } from '@tanstack/react-start'
10
+
11
+ import { getAdminBylineClient } from '@byline/client/server'
12
+
13
+ import { serialise } from '../serialise.js'
14
+
15
+ export const unpublishSingleton = createServerFn({ method: 'POST' })
16
+ .validator((input: { singleton: string }) => input)
17
+ .handler(async ({ data }) =>
18
+ serialise(await getAdminBylineClient().singleton(data.singleton).unpublish())
19
+ )
@@ -0,0 +1,31 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { createServerFn } from '@tanstack/react-start'
10
+
11
+ import { getAdminBylineClient } from '@byline/client/server'
12
+
13
+ import { serialise } from '../serialise.js'
14
+
15
+ export interface UpdateSingletonInput {
16
+ singleton: string
17
+ data: Record<string, any>
18
+ locale?: string
19
+ expectedVersionId?: string
20
+ }
21
+
22
+ export const updateSingleton = createServerFn({ method: 'POST' })
23
+ .validator((input: UpdateSingletonInput) => input)
24
+ .handler(async ({ data }) => {
25
+ const { singleton, data: fields, locale, expectedVersionId } = data
26
+ const result = await getAdminBylineClient().singleton(singleton).update(fields, {
27
+ locale,
28
+ expectedVersionId,
29
+ })
30
+ return serialise(result)
31
+ })
@@ -0,0 +1,126 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { IncomingMessage, ServerResponse } from 'node:http'
10
+
11
+ import { ANALYTICS_AGENT_SOURCE } from '@byline/analytics-agent/source'
12
+ import type { Plugin } from 'vite'
13
+ import { describe, expect, it, vi } from 'vitest'
14
+
15
+ import { bylineAnalyticsDev } from './analytics-dev.js'
16
+
17
+ type Middleware = (
18
+ request: IncomingMessage,
19
+ response: ServerResponse,
20
+ next: (error?: unknown) => void
21
+ ) => void | Promise<void>
22
+
23
+ describe('bylineAnalyticsDev', () => {
24
+ it('directly serves browser script requests before Vite can transform them', async () => {
25
+ const handler = captureMiddleware(bylineAnalyticsDev())
26
+ const request = requestStub('GET', '/b.js?version=current', {
27
+ accept: '*/*',
28
+ 'sec-fetch-dest': 'script',
29
+ })
30
+ const { response, headers, end } = responseStub()
31
+ const next = vi.fn()
32
+
33
+ await handler(request, response, next)
34
+
35
+ expect(response.statusCode).toBe(200)
36
+ expect(headers).toEqual({
37
+ 'cache-control': 'public, max-age=3600, must-revalidate',
38
+ 'content-type': 'text/javascript; charset=utf-8',
39
+ 'x-content-type-options': 'nosniff',
40
+ })
41
+ expect(end).toHaveBeenCalledWith(ANALYTICS_AGENT_SOURCE)
42
+ expect(next).not.toHaveBeenCalled()
43
+ expect(request.headers).toEqual({
44
+ accept: '*/*',
45
+ 'sec-fetch-dest': 'script',
46
+ })
47
+ })
48
+
49
+ it('continues unrelated requests through the Vite middleware chain', async () => {
50
+ const handler = captureMiddleware(bylineAnalyticsDev())
51
+ const { response, end } = responseStub()
52
+ const next = vi.fn()
53
+
54
+ await handler(requestStub('GET', '/about'), response, next)
55
+ await handler(requestStub('POST', '/api/events'), response, next)
56
+
57
+ expect(next).toHaveBeenCalledTimes(2)
58
+ expect(end).not.toHaveBeenCalled()
59
+ })
60
+
61
+ it('supports a custom public agent path', async () => {
62
+ const handler = captureMiddleware(bylineAnalyticsDev({ agentPath: '/first-party.js' }))
63
+ const defaultResponse = responseStub()
64
+ const customResponse = responseStub()
65
+ const next = vi.fn()
66
+
67
+ await handler(requestStub('GET', '/b.js'), defaultResponse.response, next)
68
+ await handler(requestStub('GET', '/first-party.js'), customResponse.response, next)
69
+
70
+ expect(next).toHaveBeenCalledOnce()
71
+ expect(defaultResponse.end).not.toHaveBeenCalled()
72
+ expect(customResponse.end).toHaveBeenCalledWith(ANALYTICS_AGENT_SOURCE)
73
+ })
74
+ })
75
+
76
+ function captureMiddleware(plugin: Plugin): Middleware {
77
+ const handlers: Middleware[] = []
78
+ const configureServer = plugin.configureServer
79
+
80
+ expect(typeof configureServer).toBe('function')
81
+ if (typeof configureServer !== 'function') {
82
+ throw new Error('Expected a Vite configureServer hook')
83
+ }
84
+
85
+ configureServer.call(
86
+ {} as never,
87
+ {
88
+ middlewares: {
89
+ use(handler: Middleware) {
90
+ handlers.push(handler)
91
+ },
92
+ },
93
+ } as never
94
+ )
95
+
96
+ const handler = handlers[0]
97
+ expect(handler).toBeDefined()
98
+ if (handler == null) throw new Error('Expected a Vite middleware')
99
+ return handler
100
+ }
101
+
102
+ function requestStub(
103
+ method: string,
104
+ url: string,
105
+ headers: IncomingMessage['headers'] = {}
106
+ ): IncomingMessage {
107
+ return { method, url, headers } as IncomingMessage
108
+ }
109
+
110
+ function responseStub(): {
111
+ response: ServerResponse
112
+ headers: Record<string, string | number | readonly string[]>
113
+ end: ReturnType<typeof vi.fn>
114
+ } {
115
+ const headers: Record<string, string | number | readonly string[]> = {}
116
+ const end = vi.fn()
117
+ const response = {
118
+ statusCode: 0,
119
+ setHeader(name: string, value: string | number | readonly string[]) {
120
+ headers[name] = value
121
+ return response
122
+ },
123
+ end,
124
+ } as unknown as ServerResponse
125
+ return { response, headers, end }
126
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { Plugin } from 'vite'
10
+
11
+ import { serveAnalyticsAgent } from '../integrations/serve-analytics-agent.js'
12
+
13
+ /**
14
+ * Complete the first-party analytics transport during Vite development.
15
+ *
16
+ * Browser script requests carry `Sec-Fetch-Dest: script`, which Vite routes
17
+ * into its module pipeline before TanStack Start can serve the agent route.
18
+ * This plugin serves the agent directly. The application event route remains
19
+ * responsible for its development identity because POST server routes can
20
+ * bypass Vite's ordinary middleware chain. Production continues to use the
21
+ * application-owned agent route.
22
+ */
23
+ export function bylineAnalyticsDev(
24
+ options: {
25
+ /** Public path that serves the standalone agent. */
26
+ agentPath?: string
27
+ } = {}
28
+ ): Plugin {
29
+ const agentPath = options.agentPath ?? '/b.js'
30
+
31
+ return {
32
+ name: 'byline:analytics-dev',
33
+ apply: 'serve',
34
+ configureServer(server) {
35
+ server.middlewares.use(async (request, response, next) => {
36
+ const pathname = request.url?.split('?', 1)[0]
37
+
38
+ if (request.method === 'GET' && pathname === agentPath) {
39
+ try {
40
+ const agentResponse = serveAnalyticsAgent()
41
+ response.statusCode = agentResponse.status
42
+ agentResponse.headers.forEach((value, name) => {
43
+ response.setHeader(name, value)
44
+ })
45
+ response.end(await agentResponse.text())
46
+ } catch (error) {
47
+ next(error)
48
+ }
49
+ return
50
+ }
51
+
52
+ next()
53
+ })
54
+ },
55
+ }
56
+ }
package/src/vite/index.ts CHANGED
@@ -6,4 +6,5 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
+ export { bylineAnalyticsDev } from './analytics-dev.js'
9
10
  export { bylineClientHookBoundary } from './client-hook-boundary.js'
@@ -0,0 +1,27 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { act } from 'react'
10
+
11
+ import { createRoot } from 'react-dom/client'
12
+ import { expect, it } from 'vitest'
13
+
14
+ it('collects and renders React component tests in jsdom mode', () => {
15
+ const reactGlobals = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
16
+ reactGlobals.IS_REACT_ACT_ENVIRONMENT = true
17
+ const container = document.createElement('div')
18
+ document.body.append(container)
19
+ const root = createRoot(container)
20
+
21
+ act(() => root.render(<span data-testid="probe">jsdom ready</span>))
22
+ expect(container.querySelector('[data-testid="probe"]')?.textContent).toBe('jsdom ready')
23
+
24
+ act(() => root.unmount())
25
+ container.remove()
26
+ delete reactGlobals.IS_REACT_ACT_ENVIRONMENT
27
+ })