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
@@ -2,6 +2,9 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import Handoff from '..';
4
4
  import { getClientConfig } from '../config';
5
+ import { resolveAssetStorageFromConfig } from '../registry/asset-storage/resolve';
6
+ import { resolveApiTokenEnv, resolveDatabaseUrlEnv, resolveRegistryAdapter } from '../registry/db/adapter';
7
+ import type { Config, RuntimeMode } from '../types/config';
5
8
  import { getAppPath } from './paths';
6
9
 
7
10
  /**
@@ -42,12 +45,66 @@ export const generateTokensApi = async (handoff: Handoff) => {
42
45
  };
43
46
 
44
47
  /**
45
- * Persists the client config to a JSON file.
48
+ * Build the server-only runtime config consumed by the docs read API.
49
+ *
50
+ * The browser-facing `client.config.json` carries only the resolved mode; the server additionally
51
+ * needs the registry connection *inputs* (selected adapter + the *name* of the database-URL env
52
+ * var) to back the registry-mode docs read API. These are non-secret — the connection-string value
53
+ * itself is never persisted, only resolved from the env var at request time.
46
54
  */
47
- export const persistClientConfig = async (handoff: Handoff) => {
55
+ const buildServerRuntimeConfig = (config: Config, modeOverride?: RuntimeMode) => {
56
+ const assetStorage = resolveAssetStorageFromConfig(config);
57
+ return {
58
+ mode: modeOverride ?? config?.runtime?.mode ?? 'workspace',
59
+ registry: {
60
+ adapter: resolveRegistryAdapter(config),
61
+ databaseUrlEnv: resolveDatabaseUrlEnv(config),
62
+ apiTokenEnv: resolveApiTokenEnv(config),
63
+ },
64
+ assetStorage: {
65
+ adapter: assetStorage.adapterKind,
66
+ module: assetStorage.module,
67
+ tokenEnv: assetStorage.tokenEnv,
68
+ maxInlineBytes: assetStorage.maxInlineBytes,
69
+ options: assetStorage.options,
70
+ },
71
+ };
72
+ };
73
+
74
+ /** Options for {@link persistClientConfig}. */
75
+ export interface PersistClientConfigOptions {
76
+ /**
77
+ * Force the runtime mode persisted to both `client.config.json` and `runtime.server.json`,
78
+ * irrespective of the source project's `runtime.mode`. The registry build sets this to `registry`
79
+ * so the packaged artifact reports registry mode (badge + DB-backed reads) even when built from a
80
+ * workspace-mode project.
81
+ */
82
+ runtimeModeOverride?: RuntimeMode;
83
+ }
84
+
85
+ /**
86
+ * Persists the app's resolved config to disk: the browser-facing `client.config.json` and the
87
+ * server-only `runtime.server.json` the docs read API resolves the active mode + registry
88
+ * connection inputs from. Both are always written together so they never drift.
89
+ */
90
+ export const persistClientConfig = async (handoff: Handoff, options: PersistClientConfigOptions = {}) => {
48
91
  const appPath = getAppPath(handoff);
49
- const destination = path.resolve(appPath, 'client.config.json');
50
92
  // Ensure directory exists
51
93
  await fs.ensureDir(appPath);
52
- await fs.writeJson(destination, { config: getClientConfig(handoff.config) }, { spaces: 2 });
94
+
95
+ const clientConfig = getClientConfig(handoff.config);
96
+ if (options.runtimeModeOverride) {
97
+ // A forced registry mode (the registry build target) is never a connected workspace, so the
98
+ // baked client config must report `connected: false` even when packaged from a connected
99
+ // workspace project.
100
+ const connected = options.runtimeModeOverride === 'registry' ? false : clientConfig.runtime.connected;
101
+ clientConfig.runtime = { ...clientConfig.runtime, mode: options.runtimeModeOverride, connected };
102
+ }
103
+
104
+ await fs.writeJson(path.resolve(appPath, 'client.config.json'), { config: clientConfig }, { spaces: 2 });
105
+ await fs.writeJson(
106
+ path.resolve(appPath, 'runtime.server.json'),
107
+ buildServerRuntimeConfig(handoff.config, options.runtimeModeOverride),
108
+ { spaces: 2 }
109
+ );
53
110
  };
@@ -1,17 +1,7 @@
1
1
  /** outerKey -> Map<innerKey, serializedValue> */
2
2
  export type MapSnapshot = Map<string, Map<string, string>>;
3
3
 
4
- /**
5
- * Deterministic JSON serializer that sorts object keys at every nesting level.
6
- * Prevents false-positive diffs when identical values are constructed with
7
- * different key insertion order across reloads.
8
- */
9
- export const stableStringify = (value: unknown): string =>
10
- JSON.stringify(value, (_, v) =>
11
- v && typeof v === 'object' && !Array.isArray(v)
12
- ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b)))
13
- : v
14
- );
4
+ export { stableStringify } from '../../utils/stable-stringify';
15
5
 
16
6
  /**
17
7
  * Compares two MapSnapshots and returns the outer keys where the inner maps
@@ -1,3 +1,3 @@
1
1
  export { buildComponents } from '../pipeline/components';
2
- export { default, devApp, watchApp } from './build';
2
+ export { default, devApp, watchApp, HandoffBuildError, DEFAULT_BUILD_TARGET, type BuildPackage, type BuildTarget } from './build';
3
3
 
@@ -0,0 +1,74 @@
1
+ import fs from 'fs-extra';
2
+ import matter from 'gray-matter';
3
+ import path from 'path';
4
+ import Handoff from '..';
5
+ import { buildMenuShell } from '../utils/menu-shell';
6
+ import { Logger } from '../utils/logger';
7
+
8
+ /**
9
+ * Relative path (within the staged Next app) of the baked navigation shell. The docs read API
10
+ * (`pages/api/docs/nav.json.ts`) imports this statically so it is bundled into the route chunk and
11
+ * available at runtime without any filesystem read — see `src/utils/menu-shell.ts` for why the
12
+ * markdown-driven shell must be frozen at build time for the Vercel registry function.
13
+ */
14
+ export const NAV_SHELL_RELATIVE_PATH = path.join('generated', 'nav-shell.json');
15
+
16
+ /** Baked package-default markdown used as the registry catch-all's runtime fallback. */
17
+ export const DEFAULT_PAGES_RELATIVE_PATH = path.join('generated', 'default-pages.json');
18
+
19
+ interface BakedDefaultPage {
20
+ metadata: Record<string, unknown>;
21
+ content: string;
22
+ }
23
+
24
+ /** Recursively collect package docs (except dedicated index routes) by their catch-all id. */
25
+ const collectDefaultPages = (root: string, relativeParts: string[] = []): Record<string, BakedDefaultPage> => {
26
+ if (!fs.existsSync(root)) return {};
27
+ const pages: Record<string, BakedDefaultPage> = {};
28
+ for (const entry of fs.readdirSync(root)) {
29
+ const absolutePath = path.join(root, entry);
30
+ if (fs.lstatSync(absolutePath).isDirectory()) {
31
+ Object.assign(pages, collectDefaultPages(absolutePath, [...relativeParts, entry]));
32
+ } else if (entry.endsWith('.md') && entry !== 'index.md') {
33
+ const id = [...relativeParts, entry.replace(/\.md$/, '')].join('/');
34
+ const { data, content } = matter(fs.readFileSync(absolutePath, 'utf8'));
35
+ pages[id] = { metadata: data, content };
36
+ }
37
+ }
38
+ return pages;
39
+ };
40
+
41
+ /**
42
+ * Bake package-default docs into the staged app so registry fallback renders do not depend on the
43
+ * build machine's absolute module path. The static JSON import is traced into every deployment.
44
+ */
45
+ export const generateDefaultPages = async (handoff: Handoff, appPath: string): Promise<void> => {
46
+ const pages = collectDefaultPages(path.join(handoff.modulePath, 'config', 'docs'));
47
+ const target = path.resolve(appPath, DEFAULT_PAGES_RELATIVE_PATH);
48
+ await fs.ensureDir(path.dirname(target));
49
+ await fs.writeJson(target, pages);
50
+ };
51
+
52
+ /**
53
+ * Bake the markdown-driven navigation shell into the staged app as `generated/nav-shell.json`.
54
+ *
55
+ * Must run BEFORE `next build` so Next inlines the JSON import into the API route chunk. Generated
56
+ * for every build target (workspace dev/start, static export, registry) so the static import never
57
+ * fails to resolve; only registry-mode clients actually fetch and use it.
58
+ */
59
+ export const generateNavShell = async (handoff: Handoff, appPath: string, includeWorkspacePages = true): Promise<void> => {
60
+ const docRoot = path.join(handoff.modulePath, 'config', 'docs');
61
+ // A registry serves only what is in its database, so workspace `pages/` present at build time must
62
+ // NOT be frozen into the shell (same rule as components/patterns). Published pages are merged into
63
+ // the shell at request time by `/api/docs/nav.json`; the package's own `config/docs` stays baked.
64
+ const workingPagesDir = includeWorkspacePages ? path.resolve(handoff.workingPath, 'pages') : undefined;
65
+ const basePath = handoff.config?.app?.base_path ?? '';
66
+
67
+ const shell = buildMenuShell({ docRoot, workingPagesDir, basePath });
68
+
69
+ const target = path.resolve(appPath, NAV_SHELL_RELATIVE_PATH);
70
+ await fs.ensureDir(path.dirname(target));
71
+ await fs.writeJson(target, shell, { spaces: 2 });
72
+
73
+ Logger.info(`Baked navigation shell (${shell.length} sections) to ${NAV_SHELL_RELATIVE_PATH}.`);
74
+ };
@@ -0,0 +1,215 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import Handoff from '..';
4
+ import { ARTIFACTS_ROUTE_SEGMENT } from '../artifacts/url';
5
+ import type { ArtifactBuildStatus } from '../artifacts/types';
6
+ import type { ComponentListObject, PatternListObject } from '../transformers/preview/types';
7
+ import { Logger } from '../utils/logger';
8
+ import { getAppPath } from './paths';
9
+
10
+ /**
11
+ * Static build target materialization.
12
+ *
13
+ * Next's `output: 'export'` disables the workspace docs read API (`/api/docs/*`) — the live route
14
+ * handlers cannot serve in an exported site. This module reproduces that read model as route-shaped
15
+ * static files so the exported site consumes the *same* canonical URLs it uses under `next dev`:
16
+ *
17
+ * - `{basePath}/api/docs/artifacts/{path}` — every generated artifact, served by extension.
18
+ * - `{basePath}/api/docs/components.json` / `patterns.json` — the list metadata reads.
19
+ * - `{basePath}/api/docs/components/{id}.json` / `patterns/{id}.json` — the detail metadata reads.
20
+ *
21
+ * Materialized files carry natural extensions so any plain static host serves them with correct
22
+ * content types and no host-specific rewrites. Preview/inspect/asset reads are pure aliases of
23
+ * artifacts the UI loads exclusively through the canonical artifact route, so they are served by the
24
+ * materialized artifact tree rather than duplicated as extensionless files.
25
+ */
26
+
27
+ /** Detail metadata shape returned by the docs read API (mirrors `lib/docs-api/records`). */
28
+ type ComponentDetail = ComponentListObject & { build: { status: ArtifactBuildStatus } };
29
+ type PatternDetail = PatternListObject & { build: { status: ArtifactBuildStatus } };
30
+
31
+ /**
32
+ * Absolute path of the generated artifact root for the running app — the same
33
+ * `.handoff/{projectId}/public/api` directory the docs read API resolves artifacts from.
34
+ */
35
+ const getStaticArtifactRoot = (handoff: Handoff): string => path.resolve(getAppPath(handoff), 'public', 'api');
36
+
37
+ const readJsonArray = <T>(absolutePath: string): T[] => {
38
+ try {
39
+ if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
40
+ return [];
41
+ }
42
+ const parsed = JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
43
+ return Array.isArray(parsed) ? (parsed as T[]) : [];
44
+ } catch {
45
+ return [];
46
+ }
47
+ };
48
+
49
+ /** Whether the entity's primary detail artifact was generated (drives `current` vs `missing`). */
50
+ const detailArtifactExists = (artifactRoot: string, relativePath: string): boolean => {
51
+ const absolutePath = path.resolve(artifactRoot, relativePath);
52
+ return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile();
53
+ };
54
+
55
+ const deriveBuildStatus = (artifactRoot: string, detailRelativePath: string): ArtifactBuildStatus =>
56
+ detailArtifactExists(artifactRoot, detailRelativePath) ? 'current' : 'missing';
57
+
58
+ /**
59
+ * Suffix of a component-owned React client/hydration bundle (`<id>.client.js`). Interactive React
60
+ * previews treat this artifact as a *required* reference: the
61
+ * preview cannot hydrate without it, so a missing one is a genuine build failure.
62
+ */
63
+ const CLIENT_ARTIFACT_SUFFIX = '.client.js';
64
+
65
+ /**
66
+ * Whether a referenced artifact is *required* — i.e. the referencing HTML cannot render correctly
67
+ * without it. Only the React client bundle qualifies. Component-owned styles/scripts
68
+ * (`<id>.css`/`<id>.js`) and shared/global artifacts (`main.css`/`main.js`/`shared.css`) are
69
+ * optional: the renderers emit `<id>.css`/`<id>.js` optimistically and a component that produces no
70
+ * stylesheet/script simply has none, so an absent optional artifact must not fail the build.
71
+ */
72
+ const isRequiredReference = (artifactPath: string): boolean => artifactPath.endsWith(CLIENT_ARTIFACT_SUFFIX);
73
+
74
+ /**
75
+ * Extract the logical artifact paths a generated HTML document references through the canonical
76
+ * artifact route. References are read from the emitted `href`/`src` URLs (the build's structured
77
+ * references are realized as these tags); this is a build-time integrity check, not the runtime
78
+ * dependency-resolution path.
79
+ */
80
+ const extractReferencedArtifactPaths = (html: string): string[] => {
81
+ // Only `href`/`src` attribute values are treated as references, so artifact-shaped strings that
82
+ // happen to appear in embedded preview content are not mistaken for real dependencies.
83
+ const pattern = new RegExp(`(?:href|src)=["'][^"']*?${ARTIFACTS_ROUTE_SEGMENT}/([^"']+)["']`, 'g');
84
+ const paths = new Set<string>();
85
+ let match: RegExpExecArray | null;
86
+ while ((match = pattern.exec(html)) !== null) {
87
+ const encoded = match[1].split(/[?#]/)[0];
88
+ const segments = encoded.split('/').filter(Boolean);
89
+ try {
90
+ const decoded = segments.map((segment) => decodeURIComponent(segment));
91
+ if (decoded.length > 0) {
92
+ paths.add(decoded.join('/'));
93
+ }
94
+ } catch {
95
+ // A reference that cannot be decoded is treated as missing below by failing to resolve.
96
+ paths.add(encoded);
97
+ }
98
+ }
99
+ return Array.from(paths);
100
+ };
101
+
102
+ /** Recursively collect every file under a directory as paths relative to it. */
103
+ const collectFilesRelative = (root: string): string[] => {
104
+ const results: string[] = [];
105
+ const walk = (current: string): void => {
106
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
107
+ const absolute = path.join(current, entry.name);
108
+ if (entry.isDirectory()) {
109
+ walk(absolute);
110
+ } else if (entry.isFile()) {
111
+ results.push(path.relative(root, absolute));
112
+ }
113
+ }
114
+ };
115
+ if (fs.existsSync(root)) {
116
+ walk(root);
117
+ }
118
+ return results;
119
+ };
120
+
121
+ /**
122
+ * Validate that every artifact referenced by generated HTML can be materialized — i.e. the
123
+ * referenced file exists under the artifact root. Required references (e.g. a React preview's
124
+ * `component/<id>.client.js`) are always emitted, so a missing one fails the static build *before*
125
+ * export rather than producing a site with broken previews.
126
+ *
127
+ * @throws When any generated HTML references an artifact that is not present on disk.
128
+ */
129
+ export const validateReferencedArtifacts = (handoff: Handoff): void => {
130
+ const artifactRoot = getStaticArtifactRoot(handoff);
131
+ if (!fs.existsSync(artifactRoot)) {
132
+ return;
133
+ }
134
+
135
+ const htmlFiles = collectFilesRelative(artifactRoot).filter((relative) => relative.toLowerCase().endsWith('.html'));
136
+ const missing = new Map<string, Set<string>>();
137
+
138
+ for (const htmlRelative of htmlFiles) {
139
+ const html = fs.readFileSync(path.resolve(artifactRoot, htmlRelative), 'utf8');
140
+ for (const referenced of extractReferencedArtifactPaths(html)) {
141
+ // Only required references gate the build; absent optional artifacts (component-owned
142
+ // styles/scripts, shared/global artifacts) are tolerated.
143
+ if (!isRequiredReference(referenced)) {
144
+ continue;
145
+ }
146
+ const target = path.resolve(artifactRoot, ...referenced.split('/'));
147
+ const contained = !path.relative(artifactRoot, target).startsWith('..');
148
+ if (!contained || !fs.existsSync(target) || !fs.statSync(target).isFile()) {
149
+ if (!missing.has(referenced)) {
150
+ missing.set(referenced, new Set());
151
+ }
152
+ missing.get(referenced)!.add(htmlRelative);
153
+ }
154
+ }
155
+ }
156
+
157
+ if (missing.size > 0) {
158
+ const details = Array.from(missing.entries())
159
+ .map(([artifactPath, referrers]) => ` - ${artifactPath} (referenced by ${Array.from(referrers).join(', ')})`)
160
+ .join('\n');
161
+ throw new Error(
162
+ `Static build aborted: ${missing.size} required artifact(s) referenced by generated HTML could not be materialized:\n${details}`
163
+ );
164
+ }
165
+ };
166
+
167
+ /**
168
+ * Materialize the docs read model into route-shaped static files under `outDir` (the Next export
169
+ * output). Produces the canonical artifact tree plus the list/detail metadata reads, matching the
170
+ * shapes the live docs read API returns so the exported site is byte-for-byte interchangeable with
171
+ * the dev server's responses.
172
+ */
173
+ export const materializeDocsReadModel = async (handoff: Handoff, outDir: string): Promise<void> => {
174
+ const artifactRoot = getStaticArtifactRoot(handoff);
175
+ if (!fs.existsSync(artifactRoot)) {
176
+ Logger.warn('No generated artifacts found to materialize for the static build.');
177
+ return;
178
+ }
179
+
180
+ const docsRoot = path.join(outDir, 'api', 'docs');
181
+ const artifactsOut = path.join(docsRoot, 'artifacts');
182
+
183
+ // Canonical artifact route: every generated artifact, addressable at `/api/docs/artifacts/{path}`.
184
+ await fs.ensureDir(artifactsOut);
185
+ await fs.copy(artifactRoot, artifactsOut, { overwrite: true });
186
+
187
+ const components = readJsonArray<ComponentListObject>(path.resolve(artifactRoot, 'components.json'));
188
+ const patterns = readJsonArray<PatternListObject>(path.resolve(artifactRoot, 'patterns.json'));
189
+ await fs.ensureDir(docsRoot);
190
+ await fs.writeJson(path.join(docsRoot, 'components.json'), components);
191
+ await fs.writeJson(path.join(docsRoot, 'patterns.json'), patterns);
192
+
193
+ // Detail metadata reads, each carrying the artifact-derived build state.
194
+ await fs.ensureDir(path.join(docsRoot, 'components'));
195
+ for (const component of components) {
196
+ const detail: ComponentDetail = {
197
+ ...component,
198
+ build: { status: deriveBuildStatus(artifactRoot, path.join('component', `${component.id}.json`)) },
199
+ };
200
+ await fs.writeJson(path.join(docsRoot, 'components', `${component.id}.json`), detail);
201
+ }
202
+
203
+ await fs.ensureDir(path.join(docsRoot, 'patterns'));
204
+ for (const pattern of patterns) {
205
+ const detail: PatternDetail = {
206
+ ...pattern,
207
+ build: { status: deriveBuildStatus(artifactRoot, path.join('pattern', `${pattern.id}.json`)) },
208
+ };
209
+ await fs.writeJson(path.join(docsRoot, 'patterns', `${pattern.id}.json`), detail);
210
+ }
211
+
212
+ Logger.success(
213
+ `Materialized docs read model (${components.length} component(s), ${patterns.length} pattern(s)) for static export.`
214
+ );
215
+ };