handoff-app 1.2.2-7 → 2.0.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/Changelog.md +37 -15
  2. package/README.md +372 -118
  3. package/dist/app-builder/build.d.ts +41 -2
  4. package/dist/app-builder/build.js +455 -18
  5. package/dist/app-builder/client-config.d.ts +15 -2
  6. package/dist/app-builder/client-config.js +43 -4
  7. package/dist/app-builder/config-diff/snapshot.d.ts +1 -6
  8. package/dist/app-builder/config-diff/snapshot.js +2 -9
  9. package/dist/app-builder/index.d.ts +1 -1
  10. package/dist/app-builder/index.js +3 -1
  11. package/dist/app-builder/nav-shell.d.ts +23 -0
  12. package/dist/app-builder/nav-shell.js +80 -0
  13. package/dist/app-builder/static-export.d.ts +17 -0
  14. package/dist/app-builder/static-export.js +181 -0
  15. package/dist/app-builder/vercel-output.d.ts +39 -0
  16. package/dist/app-builder/vercel-output.js +259 -0
  17. package/dist/app-builder/watchers/utils.d.ts +2 -0
  18. package/dist/app-builder/watchers.d.ts +14 -4
  19. package/dist/app-builder/watchers.js +123 -28
  20. package/dist/app-builder/websocket.js +1 -1
  21. package/dist/artifacts/index.d.ts +2 -0
  22. package/dist/artifacts/index.js +13 -0
  23. package/dist/artifacts/types.d.ts +136 -0
  24. package/dist/artifacts/types.js +24 -0
  25. package/dist/artifacts/url.d.ts +49 -0
  26. package/dist/artifacts/url.js +86 -0
  27. package/dist/cache/build-cache.js +48 -4
  28. package/dist/cli.js +0 -0
  29. package/dist/commands/build/app.js +1 -2
  30. package/dist/commands/build/index.d.ts +17 -0
  31. package/dist/commands/build/index.js +66 -0
  32. package/dist/commands/checkout/index.d.ts +24 -0
  33. package/dist/commands/checkout/index.js +71 -0
  34. package/dist/commands/db/migrate.d.ts +11 -0
  35. package/dist/commands/db/migrate.js +33 -0
  36. package/dist/commands/index.js +8 -0
  37. package/dist/commands/publish/index.d.ts +21 -0
  38. package/dist/commands/publish/index.js +68 -0
  39. package/dist/config/defaults.js +15 -1
  40. package/dist/config/loader.js +6 -1
  41. package/dist/config/normalizers/declaration.js +4 -1
  42. package/dist/config/normalizers/page.d.ts +18 -0
  43. package/dist/config/normalizers/page.js +34 -0
  44. package/dist/config/normalizers/pattern.js +7 -1
  45. package/dist/config/runtime.d.ts +4 -0
  46. package/dist/config/runtime.js +53 -5
  47. package/dist/index.d.ts +65 -1
  48. package/dist/index.js +123 -3
  49. package/dist/nav/index.d.ts +83 -0
  50. package/dist/nav/index.js +9 -0
  51. package/dist/nav/registry.d.ts +15 -0
  52. package/dist/nav/registry.js +42 -0
  53. package/dist/nav/resolver.d.ts +8 -0
  54. package/dist/nav/resolver.js +192 -0
  55. package/dist/nav/workspace.d.ts +18 -0
  56. package/dist/nav/workspace.js +103 -0
  57. package/dist/pipeline/styles.js +7 -35
  58. package/dist/pipeline/token-transformers.d.ts +45 -0
  59. package/dist/pipeline/token-transformers.js +71 -0
  60. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
  61. package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
  62. package/dist/registry/asset-storage/define.d.ts +10 -0
  63. package/dist/registry/asset-storage/define.js +13 -0
  64. package/dist/registry/asset-storage/resolve.d.ts +45 -0
  65. package/dist/registry/asset-storage/resolve.js +36 -0
  66. package/dist/registry/asset-storage/types.d.ts +51 -0
  67. package/dist/registry/asset-storage/types.js +9 -0
  68. package/dist/registry/assets/layout.d.ts +22 -0
  69. package/dist/registry/assets/layout.js +30 -0
  70. package/dist/registry/assets/sets.d.ts +18 -0
  71. package/dist/registry/assets/sets.js +44 -0
  72. package/dist/registry/assets/transfer.d.ts +45 -0
  73. package/dist/registry/assets/transfer.js +11 -0
  74. package/dist/registry/checkout/assets.d.ts +16 -0
  75. package/dist/registry/checkout/assets.js +150 -0
  76. package/dist/registry/checkout/index.d.ts +33 -0
  77. package/dist/registry/checkout/index.js +610 -0
  78. package/dist/registry/checkout/tokens.d.ts +15 -0
  79. package/dist/registry/checkout/tokens.js +171 -0
  80. package/dist/registry/client.d.ts +61 -0
  81. package/dist/registry/client.js +211 -0
  82. package/dist/registry/connection.d.ts +30 -0
  83. package/dist/registry/connection.js +36 -0
  84. package/dist/registry/db/adapter.d.ts +43 -0
  85. package/dist/registry/db/adapter.js +59 -0
  86. package/dist/registry/db/client.d.ts +35 -0
  87. package/dist/registry/db/client.js +94 -0
  88. package/dist/registry/db/index.d.ts +5 -0
  89. package/dist/registry/db/index.js +24 -0
  90. package/dist/registry/db/migrate.d.ts +23 -0
  91. package/dist/registry/db/migrate.js +64 -0
  92. package/dist/registry/db/schema.d.ts +4869 -0
  93. package/dist/registry/db/schema.js +347 -0
  94. package/dist/registry/path.d.ts +4 -0
  95. package/dist/registry/path.js +31 -0
  96. package/dist/registry/publish/artifacts.d.ts +16 -0
  97. package/dist/registry/publish/artifacts.js +132 -0
  98. package/dist/registry/publish/assets.d.ts +16 -0
  99. package/dist/registry/publish/assets.js +129 -0
  100. package/dist/registry/publish/errors.d.ts +5 -0
  101. package/dist/registry/publish/errors.js +26 -0
  102. package/dist/registry/publish/index.d.ts +27 -0
  103. package/dist/registry/publish/index.js +126 -0
  104. package/dist/registry/publish/package.d.ts +27 -0
  105. package/dist/registry/publish/package.js +257 -0
  106. package/dist/registry/publish/publish-build.d.ts +12 -0
  107. package/dist/registry/publish/publish-build.js +43 -0
  108. package/dist/registry/publish/tokens.d.ts +18 -0
  109. package/dist/registry/publish/tokens.js +112 -0
  110. package/dist/registry/tokens/sets.d.ts +59 -0
  111. package/dist/registry/tokens/sets.js +109 -0
  112. package/dist/registry/tokens/transfer.d.ts +48 -0
  113. package/dist/registry/tokens/transfer.js +10 -0
  114. package/dist/registry/transfer.d.ts +75 -0
  115. package/dist/registry/transfer.js +11 -0
  116. package/dist/store/filesystem-assets.d.ts +17 -0
  117. package/dist/store/filesystem-assets.js +147 -0
  118. package/dist/store/filesystem.d.ts +76 -0
  119. package/dist/store/filesystem.js +208 -0
  120. package/dist/store/index.d.ts +20 -0
  121. package/dist/store/index.js +38 -0
  122. package/dist/store/registry.d.ts +77 -0
  123. package/dist/store/registry.js +350 -0
  124. package/dist/store/source-files.d.ts +24 -0
  125. package/dist/store/source-files.js +147 -0
  126. package/dist/store/types.d.ts +185 -0
  127. package/dist/store/types.js +12 -0
  128. package/dist/transformers/plugins/csf-render.js +17 -8
  129. package/dist/transformers/plugins/handlebars-previews.js +10 -3
  130. package/dist/transformers/plugins/ssr-render.js +116 -49
  131. package/dist/transformers/preview/component/api.js +3 -1
  132. package/dist/transformers/preview/component/artifacts.js +8 -1
  133. package/dist/transformers/preview/component/builder.js +13 -5
  134. package/dist/transformers/preview/component/css.js +9 -1
  135. package/dist/transformers/preview/component/javascript.d.ts +9 -3
  136. package/dist/transformers/preview/component/javascript.js +45 -17
  137. package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
  138. package/dist/transformers/preview/component/shared-artifacts.js +92 -0
  139. package/dist/transformers/preview/component.js +5 -2
  140. package/dist/transformers/preview/pattern/builder.js +28 -9
  141. package/dist/transformers/preview/pattern/html.d.ts +13 -10
  142. package/dist/transformers/preview/pattern/html.js +62 -27
  143. package/dist/transformers/preview/types.d.ts +38 -0
  144. package/dist/transformers/utils/handlebars.d.ts +3 -0
  145. package/dist/transformers/utils/handlebars.js +19 -5
  146. package/dist/types/config.d.ts +118 -25
  147. package/dist/utils/menu-shell.d.ts +60 -0
  148. package/dist/utils/menu-shell.js +221 -0
  149. package/dist/utils/stable-stringify.d.ts +2 -0
  150. package/dist/utils/stable-stringify.js +8 -0
  151. package/docs/cli.md +4 -2
  152. package/drizzle/0000_init_registry_storage.sql +95 -0
  153. package/drizzle/0001_add_review_metadata.sql +2 -0
  154. package/drizzle/0002_add_pages.sql +28 -0
  155. package/drizzle/0003_add_tokens.sql +29 -0
  156. package/drizzle/0004_add_assets.sql +37 -0
  157. package/drizzle/meta/0000_snapshot.json +642 -0
  158. package/drizzle/meta/0001_snapshot.json +654 -0
  159. package/drizzle/meta/0002_snapshot.json +835 -0
  160. package/drizzle/meta/0003_snapshot.json +1023 -0
  161. package/drizzle/meta/0004_snapshot.json +1252 -0
  162. package/drizzle/meta/_journal.json +41 -0
  163. package/drizzle.config.ts +16 -0
  164. package/package.json +10 -1
  165. package/src/app/components/Component/Preview.tsx +3 -2
  166. package/src/app/components/Component/RegistryActions.tsx +150 -0
  167. package/src/app/components/Foundations/DisplayLogo.tsx +5 -3
  168. package/src/app/components/Layout/Header.tsx +21 -3
  169. package/src/app/components/Layout/Main.tsx +11 -3
  170. package/src/app/components/Navigation/MainNav.tsx +4 -2
  171. package/src/app/components/Navigation/MobileNav.tsx +4 -2
  172. package/src/app/components/Navigation/SideNav.tsx +46 -11
  173. package/src/app/components/NotFound.tsx +24 -0
  174. package/src/app/components/context/NavProvider.tsx +71 -0
  175. package/src/app/components/old/Header.tsx +3 -3
  176. package/src/app/components/util/index.ts +203 -380
  177. package/src/app/components/util/useCollectionAssets.ts +125 -0
  178. package/src/app/components/util/useFoundationTokens.ts +67 -0
  179. package/src/app/generated/default-pages.json +1 -0
  180. package/src/app/generated/nav-shell.json +1 -0
  181. package/src/app/lib/api/query.ts +6 -0
  182. package/src/app/lib/api/redact.ts +46 -0
  183. package/src/app/lib/asset-storage.ts +81 -0
  184. package/src/app/lib/docs-api/artifacts.ts +116 -0
  185. package/src/app/lib/docs-api/backend.ts +151 -0
  186. package/src/app/lib/docs-api/errors.ts +52 -0
  187. package/src/app/lib/docs-api/handler.ts +34 -0
  188. package/src/app/lib/docs-api/index.ts +7 -0
  189. package/src/app/lib/docs-api/records.ts +189 -0
  190. package/src/app/lib/docs-api/registry.ts +130 -0
  191. package/src/app/lib/docs-api/runtime-config.ts +173 -0
  192. package/src/app/lib/docs-api/serve.ts +37 -0
  193. package/src/app/lib/docs-api/token-detail.ts +19 -0
  194. package/src/app/lib/registry-api/allowlist.ts +163 -0
  195. package/src/app/lib/registry-api/asset-transfer.ts +363 -0
  196. package/src/app/lib/registry-api/auth.ts +68 -0
  197. package/src/app/lib/registry-api/entity-routes.ts +197 -0
  198. package/src/app/lib/registry-api/errors.ts +51 -0
  199. package/src/app/lib/registry-api/files.ts +135 -0
  200. package/src/app/lib/registry-api/handler.ts +96 -0
  201. package/src/app/lib/registry-api/index.ts +4 -0
  202. package/src/app/lib/registry-api/meta.ts +87 -0
  203. package/src/app/lib/registry-api/revalidate.ts +36 -0
  204. package/src/app/lib/registry-api/store.ts +348 -0
  205. package/src/app/lib/registry-api/token-transfer.ts +231 -0
  206. package/src/app/lib/registry-api/transfer.ts +513 -0
  207. package/src/app/lib/registry-api/validation.ts +74 -0
  208. package/src/app/lib/registry-connection.ts +58 -0
  209. package/src/app/lib/utils.ts +14 -0
  210. package/src/app/next.config.mjs +72 -13
  211. package/src/app/pages/[...slug]/index.tsx +74 -20
  212. package/src/app/pages/_app.tsx +5 -2
  213. package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
  214. package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
  215. package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
  216. package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
  217. package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
  218. package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
  219. package/src/app/pages/api/docs/components/[id].ts +24 -0
  220. package/src/app/pages/api/docs/components.json.ts +12 -0
  221. package/src/app/pages/api/docs/nav.json.ts +48 -0
  222. package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
  223. package/src/app/pages/api/docs/patterns/[id].ts +24 -0
  224. package/src/app/pages/api/docs/patterns.json.ts +12 -0
  225. package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
  226. package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
  227. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
  228. package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
  229. package/src/app/pages/api/registry/components/[id].ts +20 -0
  230. package/src/app/pages/api/registry/components/index.ts +19 -0
  231. package/src/app/pages/api/registry/health.ts +20 -0
  232. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
  233. package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
  234. package/src/app/pages/api/registry/patterns/[id].ts +20 -0
  235. package/src/app/pages/api/registry/patterns/index.ts +19 -0
  236. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
  237. package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
  238. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
  239. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
  240. package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
  241. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
  242. package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
  243. package/src/app/pages/assets/fonts.tsx +41 -17
  244. package/src/app/pages/assets/index.tsx +2 -2
  245. package/src/app/pages/assets/logos.tsx +2 -2
  246. package/src/app/pages/foundations/colors.tsx +18 -8
  247. package/src/app/pages/foundations/effects.tsx +18 -8
  248. package/src/app/pages/foundations/icons/[name]/index.tsx +26 -14
  249. package/src/app/pages/foundations/icons/index.tsx +13 -9
  250. package/src/app/pages/foundations/index.tsx +1 -1
  251. package/src/app/pages/foundations/logo.tsx +7 -4
  252. package/src/app/pages/foundations/typography.tsx +19 -9
  253. package/src/app/pages/index.tsx +1 -1
  254. package/src/app/pages/system/component/[component]/index.tsx +87 -17
  255. package/src/app/pages/system/index.tsx +13 -4
  256. package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
  257. package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
  258. package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
  259. package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
  260. package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
  261. package/src/app/tsconfig.json +2 -3
  262. package/src/app-builder/build.ts +555 -11
  263. package/src/app-builder/client-config.ts +61 -4
  264. package/src/app-builder/config-diff/snapshot.ts +1 -11
  265. package/src/app-builder/index.ts +1 -1
  266. package/src/app-builder/nav-shell.ts +74 -0
  267. package/src/app-builder/static-export.ts +215 -0
  268. package/src/app-builder/vercel-output.ts +297 -0
  269. package/src/app-builder/watchers/utils.ts +2 -0
  270. package/src/app-builder/watchers.ts +149 -25
  271. package/src/app-builder/websocket.ts +2 -2
  272. package/src/artifacts/README.md +14 -0
  273. package/src/artifacts/index.ts +17 -0
  274. package/src/artifacts/types.ts +146 -0
  275. package/src/artifacts/url.ts +90 -0
  276. package/src/cache/build-cache.ts +47 -2
  277. package/src/commands/build/app.ts +1 -2
  278. package/src/commands/build/index.ts +61 -0
  279. package/src/commands/checkout/index.ts +66 -0
  280. package/src/commands/db/migrate.ts +25 -0
  281. package/src/commands/index.ts +8 -0
  282. package/src/commands/publish/index.ts +63 -0
  283. package/src/config/defaults.ts +15 -0
  284. package/src/config/loader.ts +10 -1
  285. package/src/config/normalizers/declaration.ts +3 -0
  286. package/src/config/normalizers/page.ts +45 -0
  287. package/src/config/normalizers/pattern.ts +4 -1
  288. package/src/config/runtime.ts +48 -2
  289. package/src/index.ts +126 -3
  290. package/src/nav/index.ts +94 -0
  291. package/src/nav/registry.ts +54 -0
  292. package/src/nav/resolver.ts +217 -0
  293. package/src/nav/workspace.ts +87 -0
  294. package/src/pipeline/styles.ts +4 -35
  295. package/src/pipeline/token-transformers.ts +97 -0
  296. package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
  297. package/src/registry/asset-storage/define.ts +15 -0
  298. package/src/registry/asset-storage/resolve.ts +72 -0
  299. package/src/registry/asset-storage/types.ts +46 -0
  300. package/src/registry/assets/layout.ts +38 -0
  301. package/src/registry/assets/sets.ts +48 -0
  302. package/src/registry/assets/transfer.ts +50 -0
  303. package/src/registry/checkout/assets.ts +126 -0
  304. package/src/registry/checkout/index.ts +596 -0
  305. package/src/registry/checkout/tokens.ts +152 -0
  306. package/src/registry/client.ts +234 -0
  307. package/src/registry/connection.ts +50 -0
  308. package/src/registry/db/adapter.ts +74 -0
  309. package/src/registry/db/client.ts +88 -0
  310. package/src/registry/db/index.ts +35 -0
  311. package/src/registry/db/migrate.ts +56 -0
  312. package/src/registry/db/schema.ts +444 -0
  313. package/src/registry/path.ts +26 -0
  314. package/src/registry/publish/artifacts.ts +147 -0
  315. package/src/registry/publish/assets.ts +129 -0
  316. package/src/registry/publish/errors.ts +34 -0
  317. package/src/registry/publish/index.ts +128 -0
  318. package/src/registry/publish/package.ts +272 -0
  319. package/src/registry/publish/publish-build.ts +51 -0
  320. package/src/registry/publish/tokens.ts +119 -0
  321. package/src/registry/tokens/sets.ts +132 -0
  322. package/src/registry/tokens/transfer.ts +53 -0
  323. package/src/registry/transfer.ts +82 -0
  324. package/src/store/filesystem-assets.ts +152 -0
  325. package/src/store/filesystem.ts +237 -0
  326. package/src/store/index.ts +62 -0
  327. package/src/store/registry.ts +334 -0
  328. package/src/store/source-files.ts +155 -0
  329. package/src/store/types.ts +210 -0
  330. package/src/transformers/plugins/csf-render.ts +31 -7
  331. package/src/transformers/plugins/handlebars-previews.ts +22 -3
  332. package/src/transformers/plugins/ssr-render.ts +145 -57
  333. package/src/transformers/preview/component/api.ts +2 -1
  334. package/src/transformers/preview/component/artifacts.ts +10 -1
  335. package/src/transformers/preview/component/builder.ts +9 -2
  336. package/src/transformers/preview/component/css.ts +10 -1
  337. package/src/transformers/preview/component/javascript.ts +55 -20
  338. package/src/transformers/preview/component/shared-artifacts.ts +146 -0
  339. package/src/transformers/preview/component.ts +5 -2
  340. package/src/transformers/preview/pattern/builder.ts +33 -7
  341. package/src/transformers/preview/pattern/html.ts +71 -27
  342. package/src/transformers/preview/types.ts +42 -0
  343. package/src/transformers/utils/handlebars.ts +31 -5
  344. package/src/types/config.ts +123 -25
  345. package/src/utils/menu-shell.ts +241 -0
  346. package/src/utils/stable-stringify.ts +7 -0
  347. package/templates/default/package.json.tpl +1 -1
  348. package/handoff.config.js +0 -90
  349. package/src/app/components/ComponentSearch.tsx +0 -93
@@ -0,0 +1,34 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { redactSecrets } from '../api/redact';
3
+ import { resolveDocsBackend, type DocsBackend } from './backend';
4
+ import { ensureGet, sendDocsError } from './errors';
5
+
6
+ /** Write a successful docs read API JSON response, redacting any accidentally-stored secrets. */
7
+ export const sendDocsData = (res: NextApiResponse, status: number, data: unknown): void => {
8
+ res.status(status).json(redactSecrets(data));
9
+ };
10
+
11
+ /**
12
+ * Shared entry point for every `/api/docs/*` route.
13
+ *
14
+ * Applies the GET-only guard, resolves the mode-aware {@link DocsBackend}, and runs the route's
15
+ * logic against it — translating any thrown failure (e.g. a database that is unreachable in
16
+ * registry mode) into the docs read API's `unexpected_error` envelope. Routes stay thin and never
17
+ * need to know which backing serves them.
18
+ */
19
+ export const handleDocsRoute = async (
20
+ req: NextApiRequest,
21
+ res: NextApiResponse,
22
+ handler: (backend: DocsBackend) => Promise<void>
23
+ ): Promise<void> => {
24
+ if (!ensureGet(req, res)) {
25
+ return;
26
+ }
27
+ try {
28
+ const backend = await resolveDocsBackend();
29
+ await handler(backend);
30
+ } catch (error) {
31
+ console.error('Docs read API request failed.', error);
32
+ sendDocsError(res, 'unexpected_error', 'Unexpected docs read API error.');
33
+ }
34
+ };
@@ -0,0 +1,7 @@
1
+ /** Route facing docs API helpers. Backend and filesystem details stay in their own modules. */
2
+
3
+ export { ensureGet, idFromJsonParam, sendDocsError, type DocsErrorCode } from './errors';
4
+ export { serveArtifactBySegments } from './serve';
5
+ export { handleDocsRoute, sendDocsData } from './handler';
6
+ export { resolveDocsBackend } from './backend';
7
+ export { getServerRuntimeConfig, type ServerRuntimeConfig } from './runtime-config';
@@ -0,0 +1,189 @@
1
+ import fs from 'fs-extra';
2
+ import matter from 'gray-matter';
3
+ import path from 'path';
4
+ import { Types as CoreTypes } from 'handoff-core';
5
+ import { normalizePageDeclaration } from '@handoff/config/normalizers/page';
6
+ import { deriveTokenSets, emptyTokenDocument, setNameForId } from '@handoff/registry/tokens/sets';
7
+ import type { TokenArtifactResource } from '@handoff/store';
8
+ import type { ComponentListObject, PageListObject, PatternListObject } from '@handoff/transformers/preview/types';
9
+ import type { ArtifactBuildStatus } from '@handoff/artifacts/types';
10
+ import { getArtifactRoot } from './artifacts';
11
+ import type { TokenSetDetail, TokenSetListItem } from './backend';
12
+
13
+ /**
14
+ * Workspace-mode metadata reads for the docs read API.
15
+ *
16
+ * List/detail metadata is served from the generated `components.json`/`patterns.json` summaries and
17
+ * the per-entity `component/{id}.json`/`pattern/{id}.json` artifacts — the same files the build
18
+ * produces from the normalized store records. The shapes returned here (`ComponentListObject` /
19
+ * `PatternListObject`) are exactly what the store exposes, so registry mode can serve the
20
+ * same shapes at the same URLs from the database. HTML/asset artifacts are served separately
21
+ * through the canonical artifact route.
22
+ */
23
+
24
+ /** Detail metadata carries the entity record plus a build state derived from artifact presence. */
25
+ export type ComponentDetail = ComponentListObject & { build: { status: ArtifactBuildStatus } };
26
+ export type PatternDetail = PatternListObject & { build: { status: ArtifactBuildStatus } };
27
+ /** A page's normalized record plus its rendered markdown body. */
28
+ export type PageDetail = PageListObject & { content: string };
29
+
30
+ const readJsonFile = <T>(absolutePath: string): T | null => {
31
+ try {
32
+ if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
33
+ return null;
34
+ }
35
+ return JSON.parse(fs.readFileSync(absolutePath, 'utf8')) as T;
36
+ } catch {
37
+ return null;
38
+ }
39
+ };
40
+
41
+ /** Whether the entity's primary detail artifact has been generated. */
42
+ const detailArtifactExists = (relativePath: string): boolean => {
43
+ const absolutePath = path.resolve(getArtifactRoot(), relativePath);
44
+ return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
45
+ };
46
+
47
+ /**
48
+ * Build state for an entity in workspace mode. State is derived from artifact presence: `current`
49
+ * when the entity's detail artifact exists, otherwise `missing` (a metadata-only record that is
50
+ * still listable while its preview/asset routes report missing-artifact state).
51
+ */
52
+ const deriveBuildStatus = (detailRelativePath: string): ArtifactBuildStatus =>
53
+ detailArtifactExists(detailRelativePath) ? 'current' : 'missing';
54
+
55
+ export const listComponents = (): ComponentListObject[] =>
56
+ readJsonFile<ComponentListObject[]>(path.resolve(getArtifactRoot(), 'components.json')) ?? [];
57
+
58
+ export const listPatterns = (): PatternListObject[] =>
59
+ readJsonFile<PatternListObject[]>(path.resolve(getArtifactRoot(), 'patterns.json')) ?? [];
60
+
61
+ export const getComponentDetail = (id: string): ComponentDetail | null => {
62
+ const record = listComponents().find((component) => component.id === id);
63
+ if (!record) {
64
+ return null;
65
+ }
66
+ return { ...record, build: { status: deriveBuildStatus(path.join('component', `${id}.json`)) } };
67
+ };
68
+
69
+ export const getPatternDetail = (id: string): PatternDetail | null => {
70
+ const record = listPatterns().find((pattern) => pattern.id === id);
71
+ if (!record) {
72
+ return null;
73
+ }
74
+ return { ...record, build: { status: deriveBuildStatus(path.join('pattern', `${id}.json`)) } };
75
+ };
76
+
77
+ /**
78
+ * Workspace-mode page reads. These scan `<workingPath>/pages/` directly (there is no generated page
79
+ * summary). Vestigial in practice — workspace nav is baked and the catch-all reads markdown through
80
+ * `fetchDocPageMarkdown` — but implemented honestly so the {@link DocsBackend} contract holds.
81
+ */
82
+ const workingPagesRoot = (): string => path.resolve(process.env.HANDOFF_WORKING_PATH ?? '', 'pages');
83
+
84
+ /** Recursively collect page slug segments under `root` (every `.md` except `index.md`). */
85
+ const collectPageSlugs = (root: string, parts: string[] = []): string[][] => {
86
+ if (!fs.existsSync(root)) return [];
87
+ const out: string[][] = [];
88
+ for (const entry of fs.readdirSync(root)) {
89
+ const full = path.join(root, entry);
90
+ if (fs.statSync(full).isDirectory()) {
91
+ out.push(...collectPageSlugs(full, [...parts, entry]));
92
+ } else if (entry.endsWith('.md') && entry !== 'index.md') {
93
+ out.push([...parts, entry.replace(/\.md$/, '')]);
94
+ }
95
+ }
96
+ return out;
97
+ };
98
+
99
+ export const listPages = (): PageListObject[] => {
100
+ const root = workingPagesRoot();
101
+ return collectPageSlugs(root).map((segments) => {
102
+ const slug = segments.join('/');
103
+ const sourcePath = path.resolve(root, `${slug}.md`);
104
+ const { data } = matter(fs.readFileSync(sourcePath, 'utf8'));
105
+ return normalizePageDeclaration(data, { id: slug, routePath: `/${slug}`, sourcePath });
106
+ });
107
+ };
108
+
109
+ export const getPageDetail = (id: string): PageDetail | null => {
110
+ const root = workingPagesRoot();
111
+ const segments = collectPageSlugs(root).find((candidate) => candidate.join('/') === id);
112
+ if (!segments) {
113
+ return null;
114
+ }
115
+ const slug = segments.join('/');
116
+ const sourcePath = path.resolve(root, `${slug}.md`);
117
+ const { data, content } = matter(fs.readFileSync(sourcePath, 'utf8'));
118
+ return { ...normalizePageDeclaration(data, { id: slug, routePath: `/${slug}`, sourcePath }), content };
119
+ };
120
+
121
+ /**
122
+ * Workspace-mode token reads. The generated `tokens.json` and token style files are read straight
123
+ * from the export dir (`HANDOFF_EXPORT_PATH`, else `<cwd>/<HANDOFF_OUTPUT_DIR>`), mirroring the
124
+ * build-time `getTokens`/`fetchTokensString` readers so token behavior is identical to today's. The
125
+ * generated-file layout mirrors `fetchTokensString` (sass/types/sd/css) to preserve current
126
+ * semantics; custom transformer outputs are only surfaced through the registry backing.
127
+ */
128
+ const tokensExportRoot = (): string =>
129
+ process.env.HANDOFF_EXPORT_PATH
130
+ ? path.resolve(process.env.HANDOFF_EXPORT_PATH)
131
+ : path.resolve(process.cwd(), process.env.HANDOFF_OUTPUT_DIR ?? 'exported');
132
+
133
+ const readTokensDocument = (): CoreTypes.IDocumentationObject => {
134
+ const filePath = path.resolve(tokensExportRoot(), 'tokens.json');
135
+ if (!fs.existsSync(filePath)) {
136
+ return emptyTokenDocument();
137
+ }
138
+ try {
139
+ return JSON.parse(fs.readFileSync(filePath, 'utf8')) as CoreTypes.IDocumentationObject;
140
+ } catch {
141
+ return emptyTokenDocument();
142
+ }
143
+ };
144
+
145
+ const readTokenFile = (relativePath: string): string | null => {
146
+ const absolutePath = path.resolve(tokensExportRoot(), 'tokens', relativePath);
147
+ if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
148
+ return null;
149
+ }
150
+ try {
151
+ return fs.readFileSync(absolutePath, 'utf8');
152
+ } catch {
153
+ return null;
154
+ }
155
+ };
156
+
157
+ /** Read the four core generated formats for a set name, mirroring `fetchTokensString`'s layout. */
158
+ const readCoreTokenArtifacts = (name: string): TokenArtifactResource[] => {
159
+ const candidates: { path: string; format: string; contentType: string }[] = [
160
+ { path: `sass/${name}.scss`, format: 'scss', contentType: 'text/x-scss; charset=utf-8' },
161
+ { path: `types/${name}.scss`, format: 'types', contentType: 'text/x-scss; charset=utf-8' },
162
+ { path: `css/${name}.css`, format: 'css', contentType: 'text/css; charset=utf-8' },
163
+ { path: `sd/tokens/${name}.tokens.json`, format: 'styleDictionary', contentType: 'application/json; charset=utf-8' },
164
+ { path: `sd/tokens/${name}/${name}.tokens.json`, format: 'styleDictionary', contentType: 'application/json; charset=utf-8' },
165
+ ];
166
+ const artifacts: TokenArtifactResource[] = [];
167
+ const seenFormats = new Set<string>();
168
+ for (const candidate of candidates) {
169
+ if (seenFormats.has(candidate.format)) {
170
+ continue;
171
+ }
172
+ const content = readTokenFile(candidate.path);
173
+ if (content != null) {
174
+ artifacts.push({ path: candidate.path, format: candidate.format, content, contentType: candidate.contentType });
175
+ seenFormats.add(candidate.format);
176
+ }
177
+ }
178
+ return artifacts;
179
+ };
180
+
181
+ export const listTokenSets = (): TokenSetListItem[] => deriveTokenSets(readTokensDocument()).map(({ id, kind }) => ({ id, kind }));
182
+
183
+ export const getTokenSetDetail = (id: string): TokenSetDetail | null => {
184
+ const set = deriveTokenSets(readTokensDocument()).find((candidate) => candidate.id === id);
185
+ if (!set) {
186
+ return null;
187
+ }
188
+ return { id: set.id, kind: set.kind, record: set.record, artifacts: readCoreTokenArtifacts(setNameForId(id)) };
189
+ };
@@ -0,0 +1,130 @@
1
+ import { and, eq, inArray } from 'drizzle-orm';
2
+ import matter from 'gray-matter';
3
+ import type { ArtifactBuildStatus } from '@handoff/artifacts/types';
4
+ import type { RegistryDatabase } from '@handoff/registry/db/client';
5
+ import { buildMetadata, docsArtifacts } from '@handoff/registry/db/schema';
6
+ import { createRegistryStore } from '@handoff/store/registry';
7
+ import { contentTypeForArtifactPath } from './artifacts';
8
+ import type { DocsBackend, ResolvedArtifactBody } from './backend';
9
+ import { getRegistryConnection } from '../registry-connection';
10
+ import { getAssetStorageAdapter } from '../asset-storage';
11
+
12
+ /**
13
+ * Registry-mode backing for the docs read API.
14
+ *
15
+ * Everything here resolves from the registry database: normalized records through the DB-backed
16
+ * store, build state from the `build_metadata` table, and content artifacts from
17
+ * the `docs_artifacts` table. No read path materializes source files or runs the build pipeline.
18
+ * The database connection is created once per server process and reused across requests.
19
+ *
20
+ * Server-only: dynamically imported by {@link resolveDocsBackend} only when `runtime.mode` is
21
+ * `registry`, so the Drizzle/Postgres driver code never loads in workspace dev/build.
22
+ */
23
+
24
+ /**
25
+ * Resolve a content artifact from the registry by logical path. Returns `null` (→ `artifact_not_found`)
26
+ * when the artifact is absent, when its content is stored externally (a future object-storage
27
+ * reference with no inline content), or when a **required** structured reference it depends on is
28
+ * not present — an HTML artifact must never be served with a missing required dependency.
29
+ */
30
+ const resolveRegistryArtifact = async (db: RegistryDatabase, segments: string[]): Promise<ResolvedArtifactBody | null> => {
31
+ const artifactPath = segments.join('/');
32
+ const rows = await db.select().from(docsArtifacts).where(eq(docsArtifacts.path, artifactPath)).limit(1);
33
+ const artifact = rows[0];
34
+ if (!artifact || artifact.content == null) {
35
+ return null;
36
+ }
37
+
38
+ const requiredPaths = Array.from(
39
+ new Set((artifact.references ?? []).filter((reference) => reference.required).map((reference) => reference.path))
40
+ );
41
+ if (requiredPaths.length > 0) {
42
+ const rows = await db
43
+ .select({ path: docsArtifacts.path, content: docsArtifacts.content })
44
+ .from(docsArtifacts)
45
+ .where(inArray(docsArtifacts.path, requiredPaths));
46
+ if (rows.filter(({ content }) => content != null).length !== requiredPaths.length) {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ const contentType = artifact.contentType || contentTypeForArtifactPath(artifactPath);
52
+ return { contentType, body: artifact.content };
53
+ };
54
+
55
+ /** Build state for an entity: the stored status, or `missing` for a metadata-only record. */
56
+ const buildStatusFor = async (
57
+ db: RegistryDatabase,
58
+ entityKind: 'component' | 'pattern',
59
+ entityId: string
60
+ ): Promise<ArtifactBuildStatus> => {
61
+ const rows = await db
62
+ .select({ status: buildMetadata.status })
63
+ .from(buildMetadata)
64
+ .where(and(eq(buildMetadata.entityKind, entityKind), eq(buildMetadata.entityId, entityId)))
65
+ .limit(1);
66
+ return rows[0]?.status ?? 'missing';
67
+ };
68
+
69
+ /** Construct the registry-mode docs backend over a live database connection. */
70
+ export const createRegistryDocsBackend = async (): Promise<DocsBackend> => {
71
+ const connection = await getRegistryConnection();
72
+ const { db } = connection;
73
+ // Inject the storage-adapter resolver so object-backed asset content resolves by provider id.
74
+ const store = createRegistryStore({ db, resolveAssetAdapter: getAssetStorageAdapter });
75
+
76
+ return {
77
+ async listComponents() {
78
+ return store.components.list();
79
+ },
80
+ async listPatterns() {
81
+ return store.patterns.list();
82
+ },
83
+ async listPages() {
84
+ return store.pages.list();
85
+ },
86
+ async getComponentDetail(id: string) {
87
+ const record = await store.components.get(id);
88
+ if (!record) {
89
+ return null;
90
+ }
91
+ return { ...record, build: { status: await buildStatusFor(db, 'component', id) } };
92
+ },
93
+ async getPatternDetail(id: string) {
94
+ const record = await store.patterns.get(id);
95
+ if (!record) {
96
+ return null;
97
+ }
98
+ return { ...record, build: { status: await buildStatusFor(db, 'pattern', id) } };
99
+ },
100
+ async getPageDetail(id: string) {
101
+ const record = await store.pages.get(id);
102
+ if (!record) {
103
+ return null;
104
+ }
105
+ // The markdown body travels as the page's single source file; parse it to drop the frontmatter.
106
+ const files = await store.pages.getRelatedSourceFiles(id);
107
+ const { content } = matter(files[0]?.content ?? '');
108
+ return { ...record, content };
109
+ },
110
+ async resolveArtifact(segments: string[]) {
111
+ return resolveRegistryArtifact(db, segments);
112
+ },
113
+ async listTokenSets() {
114
+ return (await store.tokens.listSets()).map(({ id, kind }) => ({ id, kind }));
115
+ },
116
+ async getTokenSetDetail(id: string) {
117
+ const set = await store.tokens.getSet(id);
118
+ if (!set) {
119
+ return null;
120
+ }
121
+ return { id: set.id, kind: set.kind, record: set.record, artifacts: await store.tokens.getArtifacts(id) };
122
+ },
123
+ async listAssets(collection: string) {
124
+ return store.assets.listAssets(collection);
125
+ },
126
+ async getAssetContent(collection: string, assetPath: string) {
127
+ return store.assets.getAssetContent(collection, assetPath);
128
+ },
129
+ };
130
+ };
@@ -0,0 +1,173 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import type { RuntimeMode } from '@handoff/types/config';
4
+ import {
5
+ DEFAULT_DATABASE_URL_ENV,
6
+ DEFAULT_REGISTRY_ADAPTER,
7
+ DEFAULT_REGISTRY_API_TOKEN_ENV,
8
+ type RegistryDatabaseAdapter,
9
+ } from '@handoff/registry/db/adapter';
10
+ import { DEFAULT_ASSET_STORAGE_ADAPTER, type AssetStorageSettings } from '@handoff/registry/asset-storage/resolve';
11
+
12
+ /**
13
+ * Server-side runtime resolution for the docs read API.
14
+ *
15
+ * The docs read API is mode-aware: workspace mode resolves artifacts from generated filesystem
16
+ * files, registry mode resolves them from the database. The browser-facing `client.config.json`
17
+ * carries only the resolved {@link RuntimeMode} (no connection details), so the server needs its
18
+ * own non-secret source for the registry connection *inputs* — the selected adapter and the *name*
19
+ * of the env var holding the database URL. The build persists these to `runtime.server.json`
20
+ * alongside `client.config.json`; this module reads them back, never crossing secrets to the
21
+ * browser (the connection-string value is resolved from the env var at request time, not here).
22
+ *
23
+ * Server-only: imported exclusively by API route handlers and their helpers.
24
+ */
25
+
26
+ /** Non-secret registry connection inputs resolved on the server. */
27
+ export interface ServerRegistryRuntimeConfig {
28
+ /** Selected built-in database adapter. */
29
+ adapter: RegistryDatabaseAdapter;
30
+ /** Name of the env var holding the database connection string (value resolved at request time). */
31
+ databaseUrlEnv: string;
32
+ /**
33
+ * Name of the env var holding the registry management API bearer token. The token *value* is
34
+ * resolved from the environment at request time by the management API guard — never persisted.
35
+ */
36
+ apiTokenEnv: string;
37
+ }
38
+
39
+ /** Resolved server-side runtime configuration for the docs read API. */
40
+ export interface ServerRuntimeConfig {
41
+ mode: RuntimeMode;
42
+ registry: ServerRegistryRuntimeConfig;
43
+ /**
44
+ * Asset storage selection for registry mode (provider + non-secret options + env-var names). Secret
45
+ * values are resolved from `process.env` at request time, never persisted here.
46
+ */
47
+ assetStorage: AssetStorageSettings;
48
+ }
49
+
50
+ let cached: ServerRuntimeConfig | null = null;
51
+
52
+ /** Safe defaults: workspace mode with the default Postgres adapter + `DATABASE_URL` env var. */
53
+ const defaults = (): ServerRuntimeConfig => ({
54
+ mode: 'workspace',
55
+ registry: {
56
+ adapter: DEFAULT_REGISTRY_ADAPTER,
57
+ databaseUrlEnv: DEFAULT_DATABASE_URL_ENV,
58
+ apiTokenEnv: DEFAULT_REGISTRY_API_TOKEN_ENV,
59
+ },
60
+ assetStorage: { adapter: DEFAULT_ASSET_STORAGE_ADAPTER },
61
+ });
62
+
63
+ /** Parse the baked asset-storage selection from env (names/selectors only; JSON options tolerated). */
64
+ const assetStorageFromEnv = (): AssetStorageSettings => {
65
+ const adapter = process.env.HANDOFF_ASSET_STORAGE_ADAPTER?.trim();
66
+ let options: Record<string, unknown> | undefined;
67
+ const rawOptions = process.env.HANDOFF_ASSET_STORAGE_OPTIONS?.trim();
68
+ if (rawOptions) {
69
+ try {
70
+ const parsed = JSON.parse(rawOptions);
71
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) options = parsed as Record<string, unknown>;
72
+ } catch {
73
+ // Ignore malformed baked options rather than crash the server.
74
+ }
75
+ }
76
+ const maxInline = Number(process.env.HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES);
77
+ return {
78
+ adapter: adapter === 'vercel-blob' || adapter === 'custom' ? adapter : DEFAULT_ASSET_STORAGE_ADAPTER,
79
+ module: process.env.HANDOFF_ASSET_STORAGE_MODULE?.trim() || undefined,
80
+ tokenEnv: process.env.HANDOFF_ASSET_STORAGE_TOKEN_ENV?.trim() || undefined,
81
+ maxInlineBytes: Number.isFinite(maxInline) && maxInline > 0 ? maxInline : undefined,
82
+ options,
83
+ };
84
+ };
85
+
86
+ /** Absolute path of the server-only runtime config persisted next to `client.config.json`. */
87
+ const serverRuntimeConfigPath = (): string =>
88
+ path.resolve(
89
+ process.env.HANDOFF_MODULE_PATH ?? '',
90
+ '.handoff',
91
+ process.env.HANDOFF_PROJECT_ID ?? '',
92
+ 'runtime.server.json'
93
+ );
94
+
95
+ /**
96
+ * Resolve the server-side runtime config from the values baked into the bundle at build time
97
+ * (`HANDOFF_RUNTIME_MODE` + registry connection inputs). Returns `null` when no mode was baked, so
98
+ * the file-based path can take over for apps built before env baking existed.
99
+ *
100
+ * This is the resolution path for the packaged registry app: its deploy host has none of
101
+ * the build-machine paths `runtime.server.json` is keyed on, so mode + the DB env-var *name* are
102
+ * carried in the bundle instead. Mode stays config-only — these were derived from `runtime.*` at
103
+ * build, never inferred from the deploy environment.
104
+ */
105
+ const fromEnv = (): ServerRuntimeConfig | null => {
106
+ const mode = process.env.HANDOFF_RUNTIME_MODE?.trim();
107
+ if (!mode) {
108
+ return null;
109
+ }
110
+ const databaseUrlEnv = process.env.HANDOFF_REGISTRY_DATABASE_URL_ENV?.trim() || DEFAULT_DATABASE_URL_ENV;
111
+ const apiTokenEnv = process.env.HANDOFF_REGISTRY_API_TOKEN_ENV?.trim() || DEFAULT_REGISTRY_API_TOKEN_ENV;
112
+ return {
113
+ mode: mode === 'registry' ? 'registry' : 'workspace',
114
+ registry: {
115
+ adapter: process.env.HANDOFF_REGISTRY_ADAPTER?.trim() === 'neon' ? 'neon' : 'pg',
116
+ databaseUrlEnv,
117
+ apiTokenEnv,
118
+ },
119
+ assetStorage: assetStorageFromEnv(),
120
+ };
121
+ };
122
+
123
+ /**
124
+ * Resolve the server-side runtime config, preferring the values baked into the bundle, then reading
125
+ * `runtime.server.json` when present, and falling back to safe workspace defaults otherwise (so an
126
+ * app built before either source existed keeps behaving as a workspace). The result is cached for
127
+ * the lifetime of the server process.
128
+ */
129
+ export const getServerRuntimeConfig = (): ServerRuntimeConfig => {
130
+ if (cached) {
131
+ return cached;
132
+ }
133
+
134
+ const envConfig = fromEnv();
135
+ if (envConfig) {
136
+ cached = envConfig;
137
+ return cached;
138
+ }
139
+
140
+ try {
141
+ const file = serverRuntimeConfigPath();
142
+ if (fs.existsSync(file)) {
143
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8')) as Partial<ServerRuntimeConfig>;
144
+ const databaseUrlEnv =
145
+ typeof parsed?.registry?.databaseUrlEnv === 'string' && parsed.registry.databaseUrlEnv.trim()
146
+ ? parsed.registry.databaseUrlEnv.trim()
147
+ : DEFAULT_DATABASE_URL_ENV;
148
+ const apiTokenEnv =
149
+ typeof parsed?.registry?.apiTokenEnv === 'string' && parsed.registry.apiTokenEnv.trim()
150
+ ? parsed.registry.apiTokenEnv.trim()
151
+ : DEFAULT_REGISTRY_API_TOKEN_ENV;
152
+ const assetStorage: AssetStorageSettings =
153
+ parsed?.assetStorage && typeof parsed.assetStorage === 'object'
154
+ ? parsed.assetStorage
155
+ : { adapter: DEFAULT_ASSET_STORAGE_ADAPTER };
156
+ cached = {
157
+ mode: parsed?.mode === 'registry' ? 'registry' : 'workspace',
158
+ registry: {
159
+ adapter: parsed?.registry?.adapter === 'neon' ? 'neon' : 'pg',
160
+ databaseUrlEnv,
161
+ apiTokenEnv,
162
+ },
163
+ assetStorage,
164
+ };
165
+ return cached;
166
+ }
167
+ } catch {
168
+ // Fall through to defaults on any read/parse failure.
169
+ }
170
+
171
+ cached = defaults();
172
+ return cached;
173
+ };
@@ -0,0 +1,37 @@
1
+ import type { NextApiResponse } from 'next';
2
+ import { validateArtifactSegments } from './artifacts';
3
+ import type { DocsBackend } from './backend';
4
+ import { sendDocsError } from './errors';
5
+
6
+ /**
7
+ * Serve a logical artifact by its path segments, applying the docs read API contract end to end:
8
+ *
9
+ * - traversal/malformed segments → `404 not_found`
10
+ * - an artifact the backend cannot serve → `404 artifact_not_found`
11
+ * - otherwise the raw body with the resolved `Content-Type`
12
+ *
13
+ * Segment validation is mode-independent and stays here; the actual artifact lookup is delegated to
14
+ * the active {@link DocsBackend} (filesystem in workspace mode, database in registry mode), so the
15
+ * canonical artifact route and the convenience preview/inspect/asset routes all produce identical
16
+ * responses regardless of which URL or which mode reached them.
17
+ */
18
+ export const serveArtifactBySegments = async (
19
+ res: NextApiResponse,
20
+ rawSegments: string[] | undefined,
21
+ backend: DocsBackend
22
+ ): Promise<void> => {
23
+ const validation = validateArtifactSegments(rawSegments);
24
+ if (!validation.ok) {
25
+ sendDocsError(res, 'not_found', validation.reason ?? 'Invalid artifact path.');
26
+ return;
27
+ }
28
+
29
+ const resolved = await backend.resolveArtifact(validation.segments);
30
+ if (!resolved) {
31
+ sendDocsError(res, 'artifact_not_found', `Artifact "${validation.segments.join('/')}" was not found.`);
32
+ return;
33
+ }
34
+
35
+ res.setHeader('Content-Type', resolved.contentType);
36
+ res.status(200).send(resolved.body);
37
+ };
@@ -0,0 +1,19 @@
1
+ import type { TokenArtifactResource } from '@handoff/store';
2
+
3
+ /**
4
+ * Pick the four UI-known download strings (css/scss/styleDictionary/types) from a token set's
5
+ * generated artifacts, defaulting to empty strings. The full artifact list (including custom
6
+ * transformer outputs) is served alongside; these named strings back the existing `DownloadTokens`
7
+ * component without changing its shape.
8
+ */
9
+ export const tokenFormatStrings = (
10
+ artifacts: TokenArtifactResource[]
11
+ ): { css: string; scss: string; styleDictionary: string; types: string } => {
12
+ const byFormat = (format: string): string => artifacts.find((artifact) => artifact.format === format)?.content ?? '';
13
+ return {
14
+ css: byFormat('css'),
15
+ scss: byFormat('scss'),
16
+ styleDictionary: byFormat('styleDictionary'),
17
+ types: byFormat('types'),
18
+ };
19
+ };