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,237 @@
1
+ /**
2
+ * Filesystem-backed implementation of the normalized store abstraction.
3
+ *
4
+ * It is a thin read view over the already-resolved `runtimeConfig.entries`, so the records it
5
+ * returns are the same normalized component/pattern records the build consumes today — discovery
6
+ * and normalization (modern `*.handoff.*` + legacy `{dirname}.*`, react/handlebars/csf/plain-object
7
+ * declarations, CSF story discovery, `entries` resolution) all happen upstream in
8
+ * `initRuntimeConfig`. The store adds the source-location and related-source-files accessors
9
+ * without changing how anything is discovered or built.
10
+ */
11
+
12
+ import fs from 'fs-extra';
13
+ import path from 'path';
14
+ import { resolveTokenTransformers, tokenArtifactPathsForSet } from '../pipeline/token-transformers';
15
+ import { deriveTokenSets, isComponentSet, setNameForId } from '../registry/tokens/sets';
16
+ import type { ComponentListObject, PageListObject, PatternListObject } from '../transformers/preview/types';
17
+ import type { Config, RuntimeConfig } from '../types/config';
18
+ import { getRelatedSourceFilesForRecord, sourceContentTypeForPath } from './source-files';
19
+ import type {
20
+ ComponentStore,
21
+ PageStore,
22
+ PatternStore,
23
+ SourceReference,
24
+ TextFileResource,
25
+ TokenArtifactResource,
26
+ TokenSetRecord,
27
+ TokenStore,
28
+ } from './types';
29
+
30
+ /**
31
+ * Minimal context needed to back the filesystem store. A `Handoff` instance satisfies this (it
32
+ * carries `runtimeConfig`/`config` and the token path accessors), so the store stays a thin read
33
+ * view without importing the `Handoff` class.
34
+ */
35
+ export interface FilesystemStoreContext {
36
+ runtimeConfig?: RuntimeConfig | null;
37
+ config?: Config | null;
38
+ /** Workspace root (`Handoff.workingPath`), holding `public/` and `fonts/`. */
39
+ workingPath?: string;
40
+ /** Absolute path to the generated `tokens.json` (`Handoff.getTokensFilePath()`). */
41
+ getTokensFilePath?(): string;
42
+ /** Absolute path to the generated tokens output dir (`Handoff.getVariablesFilePath()`). */
43
+ getVariablesFilePath?(): string;
44
+ /** Absolute path to the docs API root `public/api` (`Handoff.getAssetsApiPath()`). */
45
+ getAssetsApiPath?(): string;
46
+ /** Absolute path of `icons.zip` (`Handoff.getIconsZipFilePath()`). */
47
+ getIconsZipFilePath?(): string;
48
+ /** Absolute path of `logos.zip` (`Handoff.getLogosZipFilePath()`). */
49
+ getLogosZipFilePath?(): string;
50
+ }
51
+
52
+ const readSourceByReference = (ref: SourceReference): TextFileResource | null => {
53
+ const { absolutePath } = ref;
54
+ try {
55
+ if (!absolutePath || !fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
56
+ return null;
57
+ }
58
+ return {
59
+ path: absolutePath,
60
+ absolutePath,
61
+ kind: 'other',
62
+ content: fs.readFileSync(absolutePath, 'utf8'),
63
+ contentType: sourceContentTypeForPath(absolutePath),
64
+ };
65
+ } catch {
66
+ return null;
67
+ }
68
+ };
69
+
70
+ export class FilesystemComponentStore implements ComponentStore {
71
+ constructor(private readonly context: FilesystemStoreContext) {}
72
+
73
+ private get records(): Record<string, ComponentListObject> {
74
+ return this.context.runtimeConfig?.entries?.components ?? {};
75
+ }
76
+
77
+ list(): ComponentListObject[] {
78
+ return Object.values(this.records);
79
+ }
80
+
81
+ get(id: string): ComponentListObject | null {
82
+ return this.records[id] ?? null;
83
+ }
84
+
85
+ getSource(ref: SourceReference): TextFileResource | null {
86
+ return readSourceByReference(ref);
87
+ }
88
+
89
+ getRelatedSourceFiles(id: string): TextFileResource[] {
90
+ const record = this.records[id];
91
+ if (!record) return [];
92
+ return getRelatedSourceFilesForRecord(record);
93
+ }
94
+ }
95
+
96
+ export class FilesystemPatternStore implements PatternStore {
97
+ constructor(private readonly context: FilesystemStoreContext) {}
98
+
99
+ private get records(): Record<string, PatternListObject> {
100
+ return this.context.runtimeConfig?.entries?.patterns ?? {};
101
+ }
102
+
103
+ list(): PatternListObject[] {
104
+ return Object.values(this.records);
105
+ }
106
+
107
+ get(id: string): PatternListObject | null {
108
+ return this.records[id] ?? null;
109
+ }
110
+
111
+ getSource(ref: SourceReference): TextFileResource | null {
112
+ return readSourceByReference(ref);
113
+ }
114
+
115
+ getRelatedSourceFiles(id: string): TextFileResource[] {
116
+ const record = this.records[id];
117
+ if (!record) return [];
118
+ return getRelatedSourceFilesForRecord(record);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Read a page's verbatim source `.md` as a registry-safe `markdown` resource. The registry-safe path
124
+ * is `${id}.md` (id may contain slashes for nested pages), which is also where checkout writes it.
125
+ */
126
+ const readPageSource = (record: PageListObject): TextFileResource | null => {
127
+ const absolutePath = record.sourcePath;
128
+ try {
129
+ if (!absolutePath || !fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
130
+ return null;
131
+ }
132
+ return {
133
+ path: `${record.id}.md`,
134
+ absolutePath,
135
+ kind: 'markdown',
136
+ content: fs.readFileSync(absolutePath, 'utf8'),
137
+ contentType: 'text/markdown; charset=utf-8',
138
+ };
139
+ } catch {
140
+ return null;
141
+ }
142
+ };
143
+
144
+ export class FilesystemPageStore implements PageStore {
145
+ constructor(private readonly context: FilesystemStoreContext) {}
146
+
147
+ private get records(): Record<string, PageListObject> {
148
+ return this.context.runtimeConfig?.entries?.pages ?? {};
149
+ }
150
+
151
+ list(): PageListObject[] {
152
+ return Object.values(this.records);
153
+ }
154
+
155
+ get(id: string): PageListObject | null {
156
+ return this.records[id] ?? null;
157
+ }
158
+
159
+ getSource(ref: SourceReference): TextFileResource | null {
160
+ return readSourceByReference(ref);
161
+ }
162
+
163
+ getRelatedSourceFiles(id: string): TextFileResource[] {
164
+ const record = this.records[id];
165
+ if (!record) return [];
166
+ const resource = readPageSource(record);
167
+ return resource ? [resource] : [];
168
+ }
169
+ }
170
+
171
+ /** Content type for a generated token artifact, keyed by extension. */
172
+ const CONTENT_TYPE_BY_EXT: Record<string, string> = {
173
+ '.css': 'text/css; charset=utf-8',
174
+ '.scss': 'text/x-scss; charset=utf-8',
175
+ '.sass': 'text/x-sass; charset=utf-8',
176
+ '.json': 'application/json; charset=utf-8',
177
+ };
178
+
179
+ export const tokenArtifactContentType = (artifactPath: string): string =>
180
+ CONTENT_TYPE_BY_EXT[path.extname(artifactPath).toLowerCase()] ?? 'text/plain; charset=utf-8';
181
+
182
+ /**
183
+ * Filesystem-backed token store. Derives logical sets from the generated `tokens.json` and reads
184
+ * their generated artifacts from the tokens output dir using the shared transformer layout, so the
185
+ * paths it reads are exactly the ones `buildStyles` writes.
186
+ */
187
+ export class FilesystemTokenStore implements TokenStore {
188
+ constructor(private readonly context: FilesystemStoreContext) {}
189
+
190
+ private readDocument(): ReturnType<typeof deriveTokenSets> {
191
+ const tokensFilePath = this.context.getTokensFilePath?.();
192
+ if (!tokensFilePath || !fs.existsSync(tokensFilePath)) {
193
+ return [];
194
+ }
195
+ try {
196
+ const doc = JSON.parse(fs.readFileSync(tokensFilePath, 'utf8'));
197
+ return deriveTokenSets(doc);
198
+ } catch {
199
+ return [];
200
+ }
201
+ }
202
+
203
+ listSets(): TokenSetRecord[] {
204
+ return this.readDocument().map(({ id, kind, record }) => ({ id, kind, record }));
205
+ }
206
+
207
+ getSet(id: string): TokenSetRecord | null {
208
+ return this.listSets().find((set) => set.id === id) ?? null;
209
+ }
210
+
211
+ getArtifacts(id: string): TokenArtifactResource[] {
212
+ const baseDir = this.context.getVariablesFilePath?.();
213
+ if (!baseDir || !this.getSet(id)) {
214
+ return [];
215
+ }
216
+ const transformers = resolveTokenTransformers(this.context);
217
+ const layouts = tokenArtifactPathsForSet(transformers, setNameForId(id), isComponentSet(id));
218
+ const resources: TokenArtifactResource[] = [];
219
+ for (const layout of layouts) {
220
+ const absolutePath = path.resolve(baseDir, ...layout.path.split('/'));
221
+ if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
222
+ continue;
223
+ }
224
+ resources.push({
225
+ path: layout.path,
226
+ format: layout.format,
227
+ content: fs.readFileSync(absolutePath, 'utf8'),
228
+ contentType: tokenArtifactContentType(layout.path),
229
+ });
230
+ }
231
+ return resources;
232
+ }
233
+
234
+ getArtifact(id: string, format: string): TokenArtifactResource | null {
235
+ return this.getArtifacts(id).find((artifact) => artifact.format === format) ?? null;
236
+ }
237
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Normalized store abstraction.
3
+ *
4
+ * The workspace (filesystem) store is exported here. The registry (database) store implements the
5
+ * same interface in `./registry`, but is intentionally **not** re-exported from this barrel so the
6
+ * Drizzle/Postgres dependencies it pulls in stay out of the workspace/static path — registry
7
+ * consumers import `@handoff/store/registry` directly (and lazily). Consumers depend only on the
8
+ * interfaces exported here so they remain storage-agnostic.
9
+ */
10
+
11
+ export type {
12
+ AssetContentResource,
13
+ AssetMetadata,
14
+ AssetStore,
15
+ Awaitable,
16
+ ComponentStore,
17
+ HandoffStore,
18
+ PageStore,
19
+ PatternStore,
20
+ RegistryTextFileKind,
21
+ RegistryTextFileResource,
22
+ SourceReference,
23
+ TextFileKind,
24
+ TextFileResource,
25
+ TokenArtifactResource,
26
+ TokenSetRecord,
27
+ TokenStore,
28
+ } from './types';
29
+
30
+ export {
31
+ FilesystemComponentStore,
32
+ FilesystemPageStore,
33
+ FilesystemPatternStore,
34
+ FilesystemTokenStore,
35
+ type FilesystemStoreContext,
36
+ } from './filesystem';
37
+
38
+ export { FilesystemAssetStore } from './filesystem-assets';
39
+
40
+ export { getRelatedSourceFilesForRecord, isRegistrySourceFile, isWorkspaceOnlyFile, sourceContentTypeForPath } from './source-files';
41
+
42
+ import {
43
+ FilesystemComponentStore,
44
+ FilesystemPageStore,
45
+ FilesystemPatternStore,
46
+ FilesystemTokenStore,
47
+ type FilesystemStoreContext,
48
+ } from './filesystem';
49
+ import { FilesystemAssetStore } from './filesystem-assets';
50
+ import type { HandoffStore } from './types';
51
+
52
+ /**
53
+ * Build the filesystem-backed store set for a runtime context (a `Handoff` instance satisfies
54
+ * {@link FilesystemStoreContext}).
55
+ */
56
+ export const createFilesystemStore = (context: FilesystemStoreContext): HandoffStore => ({
57
+ components: new FilesystemComponentStore(context),
58
+ patterns: new FilesystemPatternStore(context),
59
+ pages: new FilesystemPageStore(context),
60
+ tokens: new FilesystemTokenStore(context),
61
+ assets: new FilesystemAssetStore(context),
62
+ });
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Database-backed implementation of the normalized store abstraction.
3
+ *
4
+ * Registry mode serves only what was published to it: this store reads normalized component and
5
+ * pattern records — and their checkout/inspection source files — straight from the registry
6
+ * database. It is the registry counterpart to {@link FilesystemComponentStore},
7
+ * so the docs read API and other consumers stay storage-agnostic.
8
+ *
9
+ * Two invariants hold here that the filesystem store does not enforce:
10
+ * - Filesystem workspace declarations are never loaded as registry content; declarations are
11
+ * workspace-only (synthesized on checkout) and the registry text-file tables reject the
12
+ * `declaration` kind at the schema level, so they can never surface through this store.
13
+ * - Reads never materialize source files or run the build pipeline — every record and file is
14
+ * returned exactly as it was published.
15
+ */
16
+
17
+ import { and, eq } from 'drizzle-orm';
18
+ import type { AssetStorage, AssetStorageReadResult } from '../registry/asset-storage/types';
19
+ import type { RegistryDatabase } from '../registry/db/client';
20
+ import {
21
+ assetBlobs,
22
+ assetCollections,
23
+ assets,
24
+ componentFiles,
25
+ components,
26
+ pageFiles,
27
+ pages,
28
+ patternFiles,
29
+ patterns,
30
+ tokenArtifacts,
31
+ tokenSets,
32
+ type AssetStorageProvider,
33
+ } from '../registry/db/schema';
34
+ import type { TokenSetKind } from '../registry/tokens/sets';
35
+ import type { ComponentListObject, PageListObject, PatternListObject } from '../transformers/preview/types';
36
+ import type {
37
+ AssetContentResource,
38
+ AssetMetadata,
39
+ AssetStore,
40
+ ComponentStore,
41
+ HandoffStore,
42
+ PageStore,
43
+ PatternStore,
44
+ SourceReference,
45
+ TextFileResource,
46
+ TokenArtifactResource,
47
+ TokenSetRecord,
48
+ TokenStore,
49
+ } from './types';
50
+
51
+ /** Minimal context needed to back a registry store: a live, typed Drizzle database. */
52
+ export interface RegistryStoreContext {
53
+ db: RegistryDatabase;
54
+ /**
55
+ * Resolve the {@link AssetStorage} adapter for a blob's recorded provider (or `null` for the inline
56
+ * `database` provider). Injected by the app runtime so `src/store` stays free of app-side imports;
57
+ * when omitted, only inline DB-backed asset content is resolvable.
58
+ */
59
+ resolveAssetAdapter?: (provider: AssetStorageProvider) => Promise<AssetStorage | null>;
60
+ }
61
+
62
+ /** A persisted text-file row, as selected from `component_files`/`pattern_files`. */
63
+ type RegistryFileRow = {
64
+ path: string;
65
+ kind: TextFileResource['kind'];
66
+ content: string | null;
67
+ contentType: string;
68
+ };
69
+
70
+ /**
71
+ * Map a persisted file row to a {@link TextFileResource}. Rows whose content is stored externally
72
+ * (a future object-storage `storageRef` with no inline `content`) are skipped — there is nothing to
73
+ * return inline for them yet. Registry records have no filesystem location, so `absolutePath` is
74
+ * empty (it is documented as workspace-store-only on the type).
75
+ */
76
+ const toTextFileResource = (row: RegistryFileRow): TextFileResource | null => {
77
+ if (row.content == null) {
78
+ return null;
79
+ }
80
+ return {
81
+ path: row.path,
82
+ absolutePath: '',
83
+ kind: row.kind,
84
+ content: row.content,
85
+ contentType: row.contentType,
86
+ };
87
+ };
88
+
89
+ export class RegistryComponentStore implements ComponentStore {
90
+ constructor(private readonly context: RegistryStoreContext) {}
91
+
92
+ async list(): Promise<ComponentListObject[]> {
93
+ const rows = await this.context.db.select({ record: components.record }).from(components);
94
+ return rows.map((row) => row.record);
95
+ }
96
+
97
+ async get(id: string): Promise<ComponentListObject | null> {
98
+ const rows = await this.context.db
99
+ .select({ record: components.record })
100
+ .from(components)
101
+ .where(eq(components.id, id))
102
+ .limit(1);
103
+ return rows[0]?.record ?? null;
104
+ }
105
+
106
+ /**
107
+ * Registry source files are keyed by `(entity, path)`, not by an absolute filesystem path, so the
108
+ * filesystem-shaped {@link SourceReference} has no resolvable target here. Related files are read
109
+ * through {@link getRelatedSourceFiles}.
110
+ */
111
+ async getSource(_ref: SourceReference): Promise<TextFileResource | null> {
112
+ return null;
113
+ }
114
+
115
+ async getRelatedSourceFiles(id: string): Promise<TextFileResource[]> {
116
+ const rows = await this.context.db
117
+ .select({
118
+ path: componentFiles.path,
119
+ kind: componentFiles.kind,
120
+ content: componentFiles.content,
121
+ contentType: componentFiles.contentType,
122
+ })
123
+ .from(componentFiles)
124
+ .where(eq(componentFiles.componentId, id));
125
+ return rows.map(toTextFileResource).filter((file): file is TextFileResource => file !== null);
126
+ }
127
+ }
128
+
129
+ export class RegistryPatternStore implements PatternStore {
130
+ constructor(private readonly context: RegistryStoreContext) {}
131
+
132
+ async list(): Promise<PatternListObject[]> {
133
+ const rows = await this.context.db.select({ record: patterns.record }).from(patterns);
134
+ return rows.map((row) => row.record);
135
+ }
136
+
137
+ async get(id: string): Promise<PatternListObject | null> {
138
+ const rows = await this.context.db
139
+ .select({ record: patterns.record })
140
+ .from(patterns)
141
+ .where(eq(patterns.id, id))
142
+ .limit(1);
143
+ return rows[0]?.record ?? null;
144
+ }
145
+
146
+ async getSource(_ref: SourceReference): Promise<TextFileResource | null> {
147
+ return null;
148
+ }
149
+
150
+ async getRelatedSourceFiles(id: string): Promise<TextFileResource[]> {
151
+ const rows = await this.context.db
152
+ .select({
153
+ path: patternFiles.path,
154
+ kind: patternFiles.kind,
155
+ content: patternFiles.content,
156
+ contentType: patternFiles.contentType,
157
+ })
158
+ .from(patternFiles)
159
+ .where(eq(patternFiles.patternId, id));
160
+ return rows.map(toTextFileResource).filter((file): file is TextFileResource => file !== null);
161
+ }
162
+ }
163
+
164
+ export class RegistryPageStore implements PageStore {
165
+ constructor(private readonly context: RegistryStoreContext) {}
166
+
167
+ async list(): Promise<PageListObject[]> {
168
+ const rows = await this.context.db.select({ record: pages.record }).from(pages);
169
+ return rows.map((row) => row.record);
170
+ }
171
+
172
+ async get(id: string): Promise<PageListObject | null> {
173
+ const rows = await this.context.db.select({ record: pages.record }).from(pages).where(eq(pages.id, id)).limit(1);
174
+ return rows[0]?.record ?? null;
175
+ }
176
+
177
+ async getSource(_ref: SourceReference): Promise<TextFileResource | null> {
178
+ return null;
179
+ }
180
+
181
+ async getRelatedSourceFiles(id: string): Promise<TextFileResource[]> {
182
+ const rows = await this.context.db
183
+ .select({
184
+ path: pageFiles.path,
185
+ kind: pageFiles.kind,
186
+ content: pageFiles.content,
187
+ contentType: pageFiles.contentType,
188
+ })
189
+ .from(pageFiles)
190
+ .where(eq(pageFiles.pageId, id));
191
+ return rows.map(toTextFileResource).filter((file): file is TextFileResource => file !== null);
192
+ }
193
+ }
194
+
195
+ export class RegistryTokenStore implements TokenStore {
196
+ constructor(private readonly context: RegistryStoreContext) {}
197
+
198
+ async listSets(): Promise<TokenSetRecord[]> {
199
+ const rows = await this.context.db.select({ id: tokenSets.id, kind: tokenSets.kind, record: tokenSets.record }).from(tokenSets);
200
+ return rows.map((row) => ({ id: row.id, kind: row.kind as TokenSetKind, record: row.record }));
201
+ }
202
+
203
+ async getSet(id: string): Promise<TokenSetRecord | null> {
204
+ const rows = await this.context.db
205
+ .select({ id: tokenSets.id, kind: tokenSets.kind, record: tokenSets.record })
206
+ .from(tokenSets)
207
+ .where(eq(tokenSets.id, id))
208
+ .limit(1);
209
+ const row = rows[0];
210
+ return row ? { id: row.id, kind: row.kind as TokenSetKind, record: row.record } : null;
211
+ }
212
+
213
+ async getArtifacts(id: string): Promise<TokenArtifactResource[]> {
214
+ const rows = await this.context.db
215
+ .select({ path: tokenArtifacts.path, format: tokenArtifacts.format, content: tokenArtifacts.content, contentType: tokenArtifacts.contentType })
216
+ .from(tokenArtifacts)
217
+ .where(eq(tokenArtifacts.tokenSetId, id));
218
+ return rows
219
+ .filter((row) => row.content != null)
220
+ .map((row) => ({ path: row.path, format: row.format, content: row.content as string, contentType: row.contentType }));
221
+ }
222
+
223
+ async getArtifact(id: string, format: string): Promise<TokenArtifactResource | null> {
224
+ return (await this.getArtifacts(id)).find((artifact) => artifact.format === format) ?? null;
225
+ }
226
+ }
227
+
228
+ const readableToBuffer = async (stream: NodeJS.ReadableStream): Promise<Buffer> => {
229
+ const chunks: Buffer[] = [];
230
+ for await (const chunk of stream) {
231
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : typeof chunk === 'string' ? Buffer.from(chunk) : Buffer.from(chunk as unknown as Uint8Array));
232
+ }
233
+ return Buffer.concat(chunks);
234
+ };
235
+
236
+ export class RegistryAssetStore implements AssetStore {
237
+ constructor(private readonly context: RegistryStoreContext) {}
238
+
239
+ async listCollections(): Promise<string[]> {
240
+ const rows = await this.context.db.select({ collection: assetCollections.collection }).from(assetCollections);
241
+ return rows.map((row) => row.collection);
242
+ }
243
+
244
+ async listAssets(collection: string): Promise<AssetMetadata[]> {
245
+ const rows = await this.context.db
246
+ .select({
247
+ collection: assets.collection,
248
+ path: assets.path,
249
+ name: assets.name,
250
+ contentType: assets.contentType,
251
+ size: assets.size,
252
+ contentHash: assets.contentHash,
253
+ metadata: assets.metadata,
254
+ })
255
+ .from(assets)
256
+ .where(eq(assets.collection, collection));
257
+ return rows.map((row) => ({
258
+ collection: row.collection,
259
+ path: row.path,
260
+ name: row.name,
261
+ contentType: row.contentType,
262
+ size: row.size ?? 0,
263
+ contentHash: row.contentHash,
264
+ metadata: (row.metadata as Record<string, unknown> | null) ?? null,
265
+ }));
266
+ }
267
+
268
+ async getAsset(collection: string, assetPath: string): Promise<AssetMetadata | null> {
269
+ return (await this.listAssets(collection)).find((asset) => asset.path === assetPath) ?? null;
270
+ }
271
+
272
+ async getAssetContent(collection: string, assetPath: string): Promise<AssetContentResource | null> {
273
+ const rows = await this.context.db
274
+ .select({
275
+ name: assets.name,
276
+ contentType: assets.contentType,
277
+ size: assets.size,
278
+ contentHash: assets.contentHash,
279
+ metadata: assets.metadata,
280
+ blobContent: assetBlobs.content,
281
+ storageRef: assetBlobs.storageRef,
282
+ storageProvider: assetBlobs.storageProvider,
283
+ })
284
+ .from(assets)
285
+ .innerJoin(assetBlobs, eq(assets.blobHash, assetBlobs.hash))
286
+ .where(and(eq(assets.collection, collection), eq(assets.path, assetPath)))
287
+ .limit(1);
288
+ const row = rows[0];
289
+ if (!row) {
290
+ return null;
291
+ }
292
+
293
+ const base: AssetMetadata = {
294
+ collection,
295
+ path: assetPath,
296
+ name: row.name,
297
+ contentType: row.contentType,
298
+ size: row.size ?? 0,
299
+ contentHash: row.contentHash,
300
+ metadata: (row.metadata as Record<string, unknown> | null) ?? null,
301
+ };
302
+
303
+ // Inline (bytea, `database` provider) - return bytes directly.
304
+ if (row.blobContent != null) {
305
+ return { ...base, body: Buffer.from(row.blobContent as unknown as Uint8Array) };
306
+ }
307
+
308
+ // Object-backed - resolve through the provider that stored it.
309
+ if (!row.storageRef || !this.context.resolveAssetAdapter) {
310
+ return null;
311
+ }
312
+ const adapter = await this.context.resolveAssetAdapter(row.storageProvider);
313
+ if (!adapter) {
314
+ return null;
315
+ }
316
+ const result: AssetStorageReadResult = await adapter.get(row.storageRef);
317
+ if (result.kind === 'redirect') {
318
+ return { ...base, redirectUrl: result.url };
319
+ }
320
+ if (result.kind === 'bytes') {
321
+ return { ...base, body: result.bytes };
322
+ }
323
+ return { ...base, body: await readableToBuffer(result.stream) };
324
+ }
325
+ }
326
+
327
+ /** Build the database-backed store set for a registry database connection. */
328
+ export const createRegistryStore = (context: RegistryStoreContext): HandoffStore => ({
329
+ components: new RegistryComponentStore(context),
330
+ patterns: new RegistryPatternStore(context),
331
+ pages: new RegistryPageStore(context),
332
+ tokens: new RegistryTokenStore(context),
333
+ assets: new RegistryAssetStore(context),
334
+ });