handoff-app 1.2.2-7 → 2.0.0-1

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 (351) 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/paths.js +25 -1
  14. package/dist/app-builder/static-export.d.ts +17 -0
  15. package/dist/app-builder/static-export.js +206 -0
  16. package/dist/app-builder/vercel-output.d.ts +39 -0
  17. package/dist/app-builder/vercel-output.js +259 -0
  18. package/dist/app-builder/watchers/utils.d.ts +2 -0
  19. package/dist/app-builder/watchers.d.ts +14 -4
  20. package/dist/app-builder/watchers.js +123 -28
  21. package/dist/app-builder/websocket.js +1 -1
  22. package/dist/artifacts/index.d.ts +2 -0
  23. package/dist/artifacts/index.js +16 -0
  24. package/dist/artifacts/types.d.ts +136 -0
  25. package/dist/artifacts/types.js +24 -0
  26. package/dist/artifacts/url.d.ts +67 -0
  27. package/dist/artifacts/url.js +116 -0
  28. package/dist/cache/build-cache.js +48 -4
  29. package/dist/cli.js +0 -0
  30. package/dist/commands/build/app.js +1 -2
  31. package/dist/commands/build/index.d.ts +17 -0
  32. package/dist/commands/build/index.js +66 -0
  33. package/dist/commands/checkout/index.d.ts +24 -0
  34. package/dist/commands/checkout/index.js +71 -0
  35. package/dist/commands/db/migrate.d.ts +11 -0
  36. package/dist/commands/db/migrate.js +33 -0
  37. package/dist/commands/index.js +8 -0
  38. package/dist/commands/publish/index.d.ts +21 -0
  39. package/dist/commands/publish/index.js +68 -0
  40. package/dist/config/defaults.js +15 -1
  41. package/dist/config/loader.js +6 -1
  42. package/dist/config/normalizers/declaration.js +4 -1
  43. package/dist/config/normalizers/page.d.ts +18 -0
  44. package/dist/config/normalizers/page.js +34 -0
  45. package/dist/config/normalizers/pattern.js +7 -1
  46. package/dist/config/runtime.d.ts +4 -0
  47. package/dist/config/runtime.js +53 -5
  48. package/dist/index.d.ts +65 -1
  49. package/dist/index.js +123 -3
  50. package/dist/nav/index.d.ts +83 -0
  51. package/dist/nav/index.js +9 -0
  52. package/dist/nav/registry.d.ts +15 -0
  53. package/dist/nav/registry.js +42 -0
  54. package/dist/nav/resolver.d.ts +8 -0
  55. package/dist/nav/resolver.js +192 -0
  56. package/dist/nav/workspace.d.ts +18 -0
  57. package/dist/nav/workspace.js +103 -0
  58. package/dist/pipeline/styles.js +7 -35
  59. package/dist/pipeline/token-transformers.d.ts +45 -0
  60. package/dist/pipeline/token-transformers.js +71 -0
  61. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
  62. package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
  63. package/dist/registry/asset-storage/define.d.ts +10 -0
  64. package/dist/registry/asset-storage/define.js +13 -0
  65. package/dist/registry/asset-storage/resolve.d.ts +45 -0
  66. package/dist/registry/asset-storage/resolve.js +36 -0
  67. package/dist/registry/asset-storage/types.d.ts +51 -0
  68. package/dist/registry/asset-storage/types.js +9 -0
  69. package/dist/registry/assets/layout.d.ts +22 -0
  70. package/dist/registry/assets/layout.js +30 -0
  71. package/dist/registry/assets/sets.d.ts +18 -0
  72. package/dist/registry/assets/sets.js +44 -0
  73. package/dist/registry/assets/transfer.d.ts +45 -0
  74. package/dist/registry/assets/transfer.js +11 -0
  75. package/dist/registry/checkout/assets.d.ts +16 -0
  76. package/dist/registry/checkout/assets.js +150 -0
  77. package/dist/registry/checkout/index.d.ts +33 -0
  78. package/dist/registry/checkout/index.js +610 -0
  79. package/dist/registry/checkout/tokens.d.ts +15 -0
  80. package/dist/registry/checkout/tokens.js +171 -0
  81. package/dist/registry/client.d.ts +61 -0
  82. package/dist/registry/client.js +211 -0
  83. package/dist/registry/connection.d.ts +30 -0
  84. package/dist/registry/connection.js +36 -0
  85. package/dist/registry/db/adapter.d.ts +43 -0
  86. package/dist/registry/db/adapter.js +59 -0
  87. package/dist/registry/db/client.d.ts +35 -0
  88. package/dist/registry/db/client.js +94 -0
  89. package/dist/registry/db/index.d.ts +5 -0
  90. package/dist/registry/db/index.js +24 -0
  91. package/dist/registry/db/migrate.d.ts +23 -0
  92. package/dist/registry/db/migrate.js +64 -0
  93. package/dist/registry/db/schema.d.ts +4869 -0
  94. package/dist/registry/db/schema.js +347 -0
  95. package/dist/registry/path.d.ts +4 -0
  96. package/dist/registry/path.js +31 -0
  97. package/dist/registry/publish/artifacts.d.ts +16 -0
  98. package/dist/registry/publish/artifacts.js +132 -0
  99. package/dist/registry/publish/assets.d.ts +16 -0
  100. package/dist/registry/publish/assets.js +129 -0
  101. package/dist/registry/publish/errors.d.ts +5 -0
  102. package/dist/registry/publish/errors.js +26 -0
  103. package/dist/registry/publish/index.d.ts +27 -0
  104. package/dist/registry/publish/index.js +126 -0
  105. package/dist/registry/publish/package.d.ts +27 -0
  106. package/dist/registry/publish/package.js +257 -0
  107. package/dist/registry/publish/publish-build.d.ts +12 -0
  108. package/dist/registry/publish/publish-build.js +43 -0
  109. package/dist/registry/publish/tokens.d.ts +18 -0
  110. package/dist/registry/publish/tokens.js +112 -0
  111. package/dist/registry/tokens/sets.d.ts +59 -0
  112. package/dist/registry/tokens/sets.js +109 -0
  113. package/dist/registry/tokens/transfer.d.ts +48 -0
  114. package/dist/registry/tokens/transfer.js +10 -0
  115. package/dist/registry/transfer.d.ts +75 -0
  116. package/dist/registry/transfer.js +11 -0
  117. package/dist/store/filesystem-assets.d.ts +17 -0
  118. package/dist/store/filesystem-assets.js +147 -0
  119. package/dist/store/filesystem.d.ts +76 -0
  120. package/dist/store/filesystem.js +208 -0
  121. package/dist/store/index.d.ts +20 -0
  122. package/dist/store/index.js +38 -0
  123. package/dist/store/registry.d.ts +77 -0
  124. package/dist/store/registry.js +350 -0
  125. package/dist/store/source-files.d.ts +24 -0
  126. package/dist/store/source-files.js +147 -0
  127. package/dist/store/types.d.ts +185 -0
  128. package/dist/store/types.js +12 -0
  129. package/dist/transformers/plugins/csf-render.js +17 -8
  130. package/dist/transformers/plugins/handlebars-previews.js +10 -3
  131. package/dist/transformers/plugins/ssr-render.js +116 -49
  132. package/dist/transformers/preview/component/api.js +3 -1
  133. package/dist/transformers/preview/component/artifacts.js +8 -1
  134. package/dist/transformers/preview/component/builder.js +13 -5
  135. package/dist/transformers/preview/component/css.js +9 -1
  136. package/dist/transformers/preview/component/javascript.d.ts +9 -3
  137. package/dist/transformers/preview/component/javascript.js +45 -17
  138. package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
  139. package/dist/transformers/preview/component/shared-artifacts.js +92 -0
  140. package/dist/transformers/preview/component.js +5 -2
  141. package/dist/transformers/preview/pattern/builder.js +28 -9
  142. package/dist/transformers/preview/pattern/html.d.ts +13 -10
  143. package/dist/transformers/preview/pattern/html.js +62 -27
  144. package/dist/transformers/preview/types.d.ts +38 -0
  145. package/dist/transformers/utils/handlebars.d.ts +3 -0
  146. package/dist/transformers/utils/handlebars.js +19 -5
  147. package/dist/types/config.d.ts +124 -31
  148. package/dist/utils/menu-shell.d.ts +60 -0
  149. package/dist/utils/menu-shell.js +221 -0
  150. package/dist/utils/stable-stringify.d.ts +2 -0
  151. package/dist/utils/stable-stringify.js +8 -0
  152. package/docs/cli.md +4 -2
  153. package/drizzle/0000_init_registry_storage.sql +95 -0
  154. package/drizzle/0001_add_review_metadata.sql +2 -0
  155. package/drizzle/0002_add_pages.sql +28 -0
  156. package/drizzle/0003_add_tokens.sql +29 -0
  157. package/drizzle/0004_add_assets.sql +37 -0
  158. package/drizzle/meta/0000_snapshot.json +642 -0
  159. package/drizzle/meta/0001_snapshot.json +654 -0
  160. package/drizzle/meta/0002_snapshot.json +835 -0
  161. package/drizzle/meta/0003_snapshot.json +1023 -0
  162. package/drizzle/meta/0004_snapshot.json +1252 -0
  163. package/drizzle/meta/_journal.json +41 -0
  164. package/drizzle.config.ts +16 -0
  165. package/package.json +10 -1
  166. package/src/app/components/Component/Preview.tsx +3 -2
  167. package/src/app/components/Component/RegistryActions.tsx +150 -0
  168. package/src/app/components/Foundations/DisplayLogo.tsx +11 -3
  169. package/src/app/components/Layout/Header.tsx +21 -3
  170. package/src/app/components/Layout/Main.tsx +11 -3
  171. package/src/app/components/Navigation/MainNav.tsx +4 -2
  172. package/src/app/components/Navigation/MobileNav.tsx +4 -2
  173. package/src/app/components/Navigation/SideNav.tsx +46 -11
  174. package/src/app/components/NotFound.tsx +24 -0
  175. package/src/app/components/context/NavProvider.tsx +71 -0
  176. package/src/app/components/old/Header.tsx +3 -3
  177. package/src/app/components/util/index.ts +203 -380
  178. package/src/app/components/util/useCollectionAssets.ts +125 -0
  179. package/src/app/components/util/useFoundationTokens.ts +67 -0
  180. package/src/app/generated/default-pages.json +1 -0
  181. package/src/app/generated/nav-shell.json +1 -0
  182. package/src/app/lib/api/query.ts +6 -0
  183. package/src/app/lib/api/redact.ts +46 -0
  184. package/src/app/lib/asset-storage.ts +81 -0
  185. package/src/app/lib/docs-api/artifacts.ts +116 -0
  186. package/src/app/lib/docs-api/backend.ts +151 -0
  187. package/src/app/lib/docs-api/errors.ts +52 -0
  188. package/src/app/lib/docs-api/handler.ts +34 -0
  189. package/src/app/lib/docs-api/index.ts +7 -0
  190. package/src/app/lib/docs-api/records.ts +189 -0
  191. package/src/app/lib/docs-api/registry.ts +130 -0
  192. package/src/app/lib/docs-api/runtime-config.ts +173 -0
  193. package/src/app/lib/docs-api/serve.ts +37 -0
  194. package/src/app/lib/docs-api/token-detail.ts +19 -0
  195. package/src/app/lib/registry-api/allowlist.ts +163 -0
  196. package/src/app/lib/registry-api/asset-transfer.ts +363 -0
  197. package/src/app/lib/registry-api/auth.ts +68 -0
  198. package/src/app/lib/registry-api/entity-routes.ts +197 -0
  199. package/src/app/lib/registry-api/errors.ts +51 -0
  200. package/src/app/lib/registry-api/files.ts +135 -0
  201. package/src/app/lib/registry-api/handler.ts +96 -0
  202. package/src/app/lib/registry-api/index.ts +4 -0
  203. package/src/app/lib/registry-api/meta.ts +87 -0
  204. package/src/app/lib/registry-api/revalidate.ts +36 -0
  205. package/src/app/lib/registry-api/store.ts +348 -0
  206. package/src/app/lib/registry-api/token-transfer.ts +231 -0
  207. package/src/app/lib/registry-api/transfer.ts +513 -0
  208. package/src/app/lib/registry-api/validation.ts +74 -0
  209. package/src/app/lib/registry-connection.ts +58 -0
  210. package/src/app/lib/utils.ts +14 -0
  211. package/src/app/next.config.mjs +72 -13
  212. package/src/app/pages/[...slug]/index.tsx +74 -20
  213. package/src/app/pages/_app.tsx +5 -2
  214. package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
  215. package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
  216. package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
  217. package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
  218. package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
  219. package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
  220. package/src/app/pages/api/docs/components/[id].ts +24 -0
  221. package/src/app/pages/api/docs/components.json.ts +12 -0
  222. package/src/app/pages/api/docs/nav.json.ts +48 -0
  223. package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
  224. package/src/app/pages/api/docs/patterns/[id].ts +24 -0
  225. package/src/app/pages/api/docs/patterns.json.ts +12 -0
  226. package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
  227. package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
  228. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
  229. package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
  230. package/src/app/pages/api/registry/components/[id].ts +20 -0
  231. package/src/app/pages/api/registry/components/index.ts +19 -0
  232. package/src/app/pages/api/registry/health.ts +20 -0
  233. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
  234. package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
  235. package/src/app/pages/api/registry/patterns/[id].ts +20 -0
  236. package/src/app/pages/api/registry/patterns/index.ts +19 -0
  237. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
  238. package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
  239. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
  240. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
  241. package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
  242. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
  243. package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
  244. package/src/app/pages/assets/fonts.tsx +41 -17
  245. package/src/app/pages/assets/index.tsx +2 -2
  246. package/src/app/pages/assets/logos.tsx +6 -3
  247. package/src/app/pages/foundations/colors.tsx +18 -8
  248. package/src/app/pages/foundations/effects.tsx +18 -8
  249. package/src/app/pages/foundations/icons/[name]/index.tsx +28 -16
  250. package/src/app/pages/foundations/icons/index.tsx +18 -12
  251. package/src/app/pages/foundations/index.tsx +1 -1
  252. package/src/app/pages/foundations/logo.tsx +12 -8
  253. package/src/app/pages/foundations/typography.tsx +19 -9
  254. package/src/app/pages/index.tsx +1 -1
  255. package/src/app/pages/system/component/[component]/index.tsx +87 -17
  256. package/src/app/pages/system/index.tsx +13 -4
  257. package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
  258. package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
  259. package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
  260. package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
  261. package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
  262. package/src/app/tsconfig.json +2 -3
  263. package/src/app-builder/build.ts +555 -11
  264. package/src/app-builder/client-config.ts +61 -4
  265. package/src/app-builder/config-diff/snapshot.ts +1 -11
  266. package/src/app-builder/index.ts +1 -1
  267. package/src/app-builder/nav-shell.ts +74 -0
  268. package/src/app-builder/paths.ts +27 -1
  269. package/src/app-builder/static-export.ts +242 -0
  270. package/src/app-builder/vercel-output.ts +297 -0
  271. package/src/app-builder/watchers/utils.ts +2 -0
  272. package/src/app-builder/watchers.ts +149 -25
  273. package/src/app-builder/websocket.ts +2 -2
  274. package/src/artifacts/README.md +14 -0
  275. package/src/artifacts/index.ts +24 -0
  276. package/src/artifacts/types.ts +146 -0
  277. package/src/artifacts/url.ts +122 -0
  278. package/src/cache/build-cache.ts +47 -2
  279. package/src/commands/build/app.ts +1 -2
  280. package/src/commands/build/index.ts +61 -0
  281. package/src/commands/checkout/index.ts +66 -0
  282. package/src/commands/db/migrate.ts +25 -0
  283. package/src/commands/index.ts +8 -0
  284. package/src/commands/publish/index.ts +63 -0
  285. package/src/config/defaults.ts +15 -0
  286. package/src/config/loader.ts +10 -1
  287. package/src/config/normalizers/declaration.ts +3 -0
  288. package/src/config/normalizers/page.ts +45 -0
  289. package/src/config/normalizers/pattern.ts +4 -1
  290. package/src/config/runtime.ts +48 -2
  291. package/src/index.ts +126 -3
  292. package/src/nav/index.ts +94 -0
  293. package/src/nav/registry.ts +54 -0
  294. package/src/nav/resolver.ts +217 -0
  295. package/src/nav/workspace.ts +87 -0
  296. package/src/pipeline/styles.ts +4 -35
  297. package/src/pipeline/token-transformers.ts +97 -0
  298. package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
  299. package/src/registry/asset-storage/define.ts +15 -0
  300. package/src/registry/asset-storage/resolve.ts +72 -0
  301. package/src/registry/asset-storage/types.ts +46 -0
  302. package/src/registry/assets/layout.ts +38 -0
  303. package/src/registry/assets/sets.ts +48 -0
  304. package/src/registry/assets/transfer.ts +50 -0
  305. package/src/registry/checkout/assets.ts +126 -0
  306. package/src/registry/checkout/index.ts +596 -0
  307. package/src/registry/checkout/tokens.ts +152 -0
  308. package/src/registry/client.ts +234 -0
  309. package/src/registry/connection.ts +50 -0
  310. package/src/registry/db/adapter.ts +74 -0
  311. package/src/registry/db/client.ts +88 -0
  312. package/src/registry/db/index.ts +35 -0
  313. package/src/registry/db/migrate.ts +56 -0
  314. package/src/registry/db/schema.ts +444 -0
  315. package/src/registry/path.ts +26 -0
  316. package/src/registry/publish/artifacts.ts +147 -0
  317. package/src/registry/publish/assets.ts +129 -0
  318. package/src/registry/publish/errors.ts +34 -0
  319. package/src/registry/publish/index.ts +128 -0
  320. package/src/registry/publish/package.ts +272 -0
  321. package/src/registry/publish/publish-build.ts +51 -0
  322. package/src/registry/publish/tokens.ts +119 -0
  323. package/src/registry/tokens/sets.ts +132 -0
  324. package/src/registry/tokens/transfer.ts +53 -0
  325. package/src/registry/transfer.ts +82 -0
  326. package/src/store/filesystem-assets.ts +152 -0
  327. package/src/store/filesystem.ts +237 -0
  328. package/src/store/index.ts +62 -0
  329. package/src/store/registry.ts +334 -0
  330. package/src/store/source-files.ts +155 -0
  331. package/src/store/types.ts +210 -0
  332. package/src/transformers/plugins/csf-render.ts +31 -7
  333. package/src/transformers/plugins/handlebars-previews.ts +22 -3
  334. package/src/transformers/plugins/ssr-render.ts +145 -57
  335. package/src/transformers/preview/component/api.ts +2 -1
  336. package/src/transformers/preview/component/artifacts.ts +10 -1
  337. package/src/transformers/preview/component/builder.ts +9 -2
  338. package/src/transformers/preview/component/css.ts +10 -1
  339. package/src/transformers/preview/component/javascript.ts +55 -20
  340. package/src/transformers/preview/component/shared-artifacts.ts +146 -0
  341. package/src/transformers/preview/component.ts +5 -2
  342. package/src/transformers/preview/pattern/builder.ts +33 -7
  343. package/src/transformers/preview/pattern/html.ts +71 -27
  344. package/src/transformers/preview/types.ts +42 -0
  345. package/src/transformers/utils/handlebars.ts +31 -5
  346. package/src/types/config.ts +129 -31
  347. package/src/utils/menu-shell.ts +241 -0
  348. package/src/utils/stable-stringify.ts +7 -0
  349. package/templates/default/package.json.tpl +1 -1
  350. package/handoff.config.js +0 -90
  351. package/src/app/components/ComponentSearch.tsx +0 -93
@@ -10,9 +10,60 @@ const resolveBasePath = (rawBasePath) => {
10
10
  return trimmed ? `/${trimmed}` : '';
11
11
  };
12
12
 
13
+ // Resolved build target drives Next's output mode. The target — not NODE_ENV
14
+ // — decides static export vs. dynamic packaging, so workspace dev, the static snapshot, and the
15
+ // registry app never get conflated:
16
+ // - `static` → `output: 'export'` (a self-contained static snapshot).
17
+ // - `registry` → `output: 'standalone'` (a deployable dynamic Next.js app + traced node_modules).
18
+ // - otherwise → a normal server (workspace `next dev`/`start`).
19
+ const handoffBuildTarget = process.env.HANDOFF_BUILD_TARGET;
20
+ const handoffWorkingPath = path.resolve('%HANDOFF_WORKING_PATH%');
21
+
22
+ // A configured custom asset-storage adapter module (relative to the consumer project). It is loaded
23
+ // by a variable dynamic import at runtime, which nft cannot statically trace, so force it (and its
24
+ // resolvable deps) into the registry bundle. Empty/unset for the built-in database/Vercel adapters.
25
+ const handoffAssetStorageModule = '%HANDOFF_ASSET_STORAGE_MODULE%';
26
+ const resolveAssetStorageInclude = () => {
27
+ if (handoffBuildTarget !== 'registry' || !handoffAssetStorageModule || handoffAssetStorageModule.startsWith('%HANDOFF_')) {
28
+ return undefined;
29
+ }
30
+ const abs = path.isAbsolute(handoffAssetStorageModule) ? handoffAssetStorageModule : path.resolve(handoffWorkingPath, handoffAssetStorageModule);
31
+ return { '/api/**': [abs] };
32
+ };
33
+
34
+ const resolveOutputMode = (target) => {
35
+ if (target === 'static') {
36
+ // Static export disables Next API routes, which the workspace docs read API (`/api/docs/*`)
37
+ // depends on, so the static build materializes that read model into route-shaped files instead.
38
+ return 'export';
39
+ }
40
+ if (target === 'registry') {
41
+ // The registry app is a deployable dynamic server (DB-backed docs/registry APIs). Standalone
42
+ // traces the runtime + selected DB driver into a self-contained bundle for Vercel/Node/containers
43
+ // and never runs a static export.
44
+ return 'standalone';
45
+ }
46
+ return undefined;
47
+ };
48
+
13
49
  /** @type {import('next').NextConfig} */
14
50
  const nextConfig = {
15
- output: 'export',
51
+ output: resolveOutputMode(handoffBuildTarget),
52
+ // The tracer only copies files inside its root, so root it at the consumer project — the common
53
+ // ancestor of the staged app (`node_modules/handoff-app/.handoff/<projectId>`) and the runtime deps
54
+ // hoisted to the top-level `node_modules`. Rooting at the package dir would trace an empty
55
+ // `node_modules` and ship a non-bootable bundle. Registry-only; workspace dev/static is unchanged.
56
+ outputFileTracingRoot: handoffBuildTarget === 'registry' ? handoffWorkingPath : undefined,
57
+ // nft speculatively traces the export-only paths the app references (`export-detail.json` and the
58
+ // materializer's `.next/export/**` targets) as runtime deps. They never exist in a `standalone`
59
+ // build, and `copyTracedFiles` copies traced files with no existence guard, so leaving them in
60
+ // fails the build with `ENOENT`. Exclude them (`**` matches the server + every per-page trace).
61
+ // Registry-only; static export, which legitimately produces these files, is untouched.
62
+ outputFileTracingExcludes:
63
+ handoffBuildTarget === 'registry' ? { '**': ['**/export-detail.json', '**/.next/export/**'] } : undefined,
64
+ // Force a configured custom asset-storage module into the registry trace (dynamic import is opaque
65
+ // to nft). Its SDK deps are additionally asserted via `getRequiredRegistryRuntimeModules`.
66
+ outputFileTracingIncludes: resolveAssetStorageInclude(),
16
67
  reactStrictMode: true,
17
68
  pageExtensions: ['js', 'jsx', 'ts', 'tsx'],
18
69
  trailingSlash: true,
@@ -26,25 +77,42 @@ const nextConfig = {
26
77
  typescript: {
27
78
  tsconfigPath: 'tsconfig.json',
28
79
  },
29
- //distDir: 'out',
30
80
  basePath: resolveBasePath('%HANDOFF_APP_BASE_PATH%'),
31
81
  env: {
32
82
  HANDOFF_PROJECT_ID: '%HANDOFF_PROJECT_ID%',
33
83
  HANDOFF_APP_BASE_PATH: '%HANDOFF_APP_BASE_PATH%',
84
+ // Resolved build target, baked so the client can tell a self-contained static snapshot apart
85
+ // from a live workspace/registry app (e.g. the header hides the runtime-mode badge in the
86
+ // static export). Read straight from the build env — empty for workspace dev/start.
87
+ HANDOFF_BUILD_TARGET: handoffBuildTarget ?? '',
34
88
  HANDOFF_WORKING_PATH: '%HANDOFF_WORKING_PATH%',
35
89
  HANDOFF_MODULE_PATH: '%HANDOFF_MODULE_PATH%',
36
90
  HANDOFF_EXPORT_PATH: '%HANDOFF_EXPORT_PATH%',
37
91
  HANDOFF_WEBSOCKET_PORT: '%HANDOFF_WEBSOCKET_PORT%',
92
+ // Resolved runtime mode + registry connection *inputs* (names only, never secrets), baked at
93
+ // build time so the deployed registry app resolves its mode and DB env-var name without any
94
+ // build-machine filesystem (the absolute paths above do not exist on the deploy host). Mode stays
95
+ // config-only — these are derived from `runtime.*`, not inferred from env at runtime. The DB
96
+ // connection string itself is read from the named env var at request time (deployment-supplied).
97
+ HANDOFF_RUNTIME_MODE: '%HANDOFF_RUNTIME_MODE%',
98
+ HANDOFF_REGISTRY_ADAPTER: '%HANDOFF_REGISTRY_ADAPTER%',
99
+ HANDOFF_REGISTRY_DATABASE_URL_ENV: '%HANDOFF_REGISTRY_DATABASE_URL_ENV%',
100
+ HANDOFF_REGISTRY_API_TOKEN_ENV: '%HANDOFF_REGISTRY_API_TOKEN_ENV%',
101
+ // Asset storage selection (provider + non-secret options + env-var names). Secret values (Blob
102
+ // token, custom credentials) are read from their named env var at request time, never baked.
103
+ HANDOFF_ASSET_STORAGE_ADAPTER: '%HANDOFF_ASSET_STORAGE_ADAPTER%',
104
+ HANDOFF_ASSET_STORAGE_MODULE: '%HANDOFF_ASSET_STORAGE_MODULE%',
105
+ HANDOFF_ASSET_STORAGE_TOKEN_ENV: '%HANDOFF_ASSET_STORAGE_TOKEN_ENV%',
106
+ HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES: '%HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES%',
107
+ HANDOFF_ASSET_STORAGE_OPTIONS: '%HANDOFF_ASSET_STORAGE_OPTIONS%',
38
108
  },
39
109
  images: {
40
110
  unoptimized: true,
41
111
  },
42
112
  sassOptions: {
43
113
  additionalData: (content, _) => {
44
- // Local state
45
114
  let foundTheme = false;
46
115
 
47
- // Local environment
48
116
  const env = {
49
117
  HANDOFF_PROJECT_ID: '%HANDOFF_PROJECT_ID%',
50
118
  HANDOFF_APP_BASE_PATH: '%HANDOFF_APP_BASE_PATH%',
@@ -54,22 +122,16 @@ const nextConfig = {
54
122
  HANDOFF_WEBSOCKET_PORT: '%HANDOFF_WEBSOCKET_PORT%',
55
123
  };
56
124
 
57
- // Check if client configuration exists
58
125
  const clientConfigPath = path.resolve(env.HANDOFF_WORKING_PATH, 'handoff.config.json');
59
126
  if (fs.existsSync(clientConfigPath)) {
60
- // Load client configuration
61
127
  const clientConfigRaw = fs.readFileSync(clientConfigPath);
62
128
  const clientConfig = JSON.parse(clientConfigRaw);
63
- // Check if client configuration is a valid object
64
129
  if (typeof clientConfig === 'object' && !Array.isArray(clientConfig) && clientConfig !== null) {
65
- // Check if the client configuration specifies a theme
66
- // If the theme is specified, check if the theme exists in the 'themes' folder
67
130
  if (
68
131
  clientConfig.hasOwnProperty('app') &&
69
132
  clientConfig['app'].hasOwnProperty('theme') &&
70
133
  fs.existsSync(path.resolve(env.HANDOFF_WORKING_PATH, 'theme', `${clientConfig['app']['theme']}.scss`))
71
134
  ) {
72
- // Use custom theme
73
135
  foundTheme = true;
74
136
  content = content + `\n@import '${path.resolve(env.HANDOFF_WORKING_PATH, 'theme', clientConfig['app']['theme'])}';`;
75
137
  console.log(
@@ -84,9 +146,7 @@ const nextConfig = {
84
146
  }
85
147
 
86
148
  if (!foundTheme) {
87
- // Check if there is a custom version of the default theme
88
149
  if (fs.existsSync(path.resolve(env.HANDOFF_WORKING_PATH, 'theme', `default.scss`))) {
89
- // Use custom theme
90
150
  content = content + `\n@import 'theme/default';`;
91
151
  console.log(
92
152
  `- ${chalk.cyan('info')} Using default app theme override (path: ${path.resolve(
@@ -96,7 +156,6 @@ const nextConfig = {
96
156
  )})`
97
157
  );
98
158
  } else {
99
- // Use default theme
100
159
  content = content + `\n@import 'themes/default';`;
101
160
  console.log(`- ${chalk.cyan('info')} Using default app theme`);
102
161
  }
@@ -1,6 +1,5 @@
1
1
  import { GetStaticProps } from 'next';
2
2
  import Head from 'next/head';
3
- import Link from 'next/link';
4
3
  import { useRef } from 'react';
5
4
  import ReactMarkdown from 'react-markdown';
6
5
  import rehypeRaw from 'rehype-raw';
@@ -8,32 +7,101 @@ import remarkGfm from 'remark-gfm';
8
7
  import Layout from '../../components/Layout/Main';
9
8
  import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
10
9
  import { PageTOC } from '../../components/Navigation/AnchorNav';
10
+ import NotFound from '../../components/NotFound';
11
11
  import HeadersType from '../../components/Typography/Headers';
12
+ import defaultPages from '../../generated/default-pages.json';
12
13
  import {
13
14
  buildCatchAllStaticPaths,
14
15
  DocumentationProps,
15
16
  fetchDocPageMarkdown,
16
17
  getClientRuntimeConfig,
18
+ getNavProps,
19
+ isRegistryRuntime,
17
20
  } from '../../components/util';
21
+ import { resolveDocsBackend } from '../../lib/docs-api/backend';
22
+
23
+ /**
24
+ * Registry deployments can run across independent serverless instances whose local ISR caches do
25
+ * not share on-demand invalidations. Keep mutable page entries short-lived so every instance
26
+ * converges on the published DB record even when it did not handle the publish request.
27
+ */
28
+ const REGISTRY_PAGE_REVALIDATE_SECONDS = 1;
29
+
30
+ type BakedDefaultPage = { metadata: Record<string, unknown>; content: string };
31
+
32
+ /** Normalize published records and baked frontmatter into the page component's metadata contract. */
33
+ const documentationMetadata = (source: Record<string, unknown>) => {
34
+ const value = (key: string): string => (typeof source[key] === 'string' ? (source[key] as string) : '');
35
+ const title = value('title');
36
+ const description = value('description');
37
+ return {
38
+ title,
39
+ description,
40
+ metaTitle: value('metaTitle') || title,
41
+ metaDescription: value('metaDescription') || description,
42
+ };
43
+ };
18
44
 
19
45
  export async function getStaticPaths() {
46
+ // Registry resolves every catch-all page on demand so a cold serverless instance queries the DB
47
+ // before it can cache a package default. Defaults are baked into the server bundle as JSON.
48
+ // Workspace/static stays fully prerendered from markdown (workspace pages included).
49
+ if (isRegistryRuntime()) {
50
+ return { paths: [], fallback: 'blocking' as const };
51
+ }
20
52
  return {
21
53
  paths: buildCatchAllStaticPaths(),
22
54
  fallback: false,
23
55
  };
24
56
  }
25
57
 
26
- export const getStaticProps: GetStaticProps = (context) => {
58
+ export const getStaticProps: GetStaticProps = async (context) => {
27
59
  const { slug } = context.params as { slug: string[] };
60
+ const config = getClientRuntimeConfig();
28
61
  const dirParts = slug.slice(0, -1);
29
62
  const file = slug[slug.length - 1];
30
63
  const docPath = dirParts.length > 0 ? `docs/${dirParts.join('/')}/` : 'docs/';
31
64
  const sectionId = `/${slug[0]}`;
32
65
 
66
+ // Registry mode resolves published content first, then the package-default fallback baked during
67
+ // the build. Workspace markdown is never read and the deployed server needs no build-machine path.
68
+ if (isRegistryRuntime()) {
69
+ const id = slug.join('/');
70
+ const detail = await (await resolveDocsBackend()).getPageDetail(id);
71
+ if (detail) {
72
+ const navProps = await getNavProps(sectionId);
73
+ return {
74
+ props: {
75
+ metadata: documentationMetadata(detail as unknown as Record<string, unknown>),
76
+ content: detail.content,
77
+ ...navProps,
78
+ config,
79
+ },
80
+ revalidate: REGISTRY_PAGE_REVALIDATE_SECONDS,
81
+ };
82
+ }
83
+
84
+ const defaultPage = (defaultPages as Record<string, BakedDefaultPage>)[id];
85
+ if (defaultPage) {
86
+ const navProps = await getNavProps(sectionId);
87
+ return {
88
+ props: {
89
+ metadata: documentationMetadata(defaultPage.metadata),
90
+ content: defaultPage.content,
91
+ ...navProps,
92
+ config,
93
+ },
94
+ revalidate: REGISTRY_PAGE_REVALIDATE_SECONDS,
95
+ };
96
+ }
97
+
98
+ return { notFound: true, revalidate: REGISTRY_PAGE_REVALIDATE_SECONDS };
99
+ }
100
+
33
101
  return {
34
102
  props: {
35
- ...fetchDocPageMarkdown(docPath, file, sectionId).props,
36
- config: getClientRuntimeConfig(),
103
+ ...(await fetchDocPageMarkdown(docPath, file, sectionId)).props,
104
+ config,
37
105
  },
38
106
  };
39
107
  };
@@ -41,28 +109,14 @@ export const getStaticProps: GetStaticProps = (context) => {
41
109
  export default function DocCatchAllPage({ content, menu, metadata, current, config }: DocumentationProps) {
42
110
  const bodyRef = useRef<HTMLDivElement>(null);
43
111
 
44
- if (!content) {
112
+ if (!content && !metadata?.title) {
45
113
  return (
46
114
  <div className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-gray-900">
47
115
  <Head>
48
116
  <title>404 - Page Not Found</title>
49
117
  <meta name="description" content="Page Not Found" />
50
118
  </Head>
51
- <div className="flex flex-col items-center">
52
- <div className="mb-2 text-7xl font-bold text-gray-800 dark:text-white">404</div>
53
- <h1 className="mb-4 text-2xl font-semibold text-gray-700 dark:text-gray-300">Oops! Page not found.</h1>
54
- <p className="mb-6 max-w-md text-center text-gray-500 dark:text-gray-400">
55
- Sorry, the page you are looking for does not exist or has been moved.
56
- <br />
57
- Please check the URL or return to the homepage.
58
- </p>
59
- <Link
60
- href="/"
61
- className="rounded-md bg-blue-600 px-6 py-2 font-medium text-white shadow-md transition-colors duration-200 hover:bg-blue-700"
62
- >
63
- Go Home
64
- </Link>
65
- </div>
119
+ <NotFound />
66
120
  </div>
67
121
  );
68
122
  }
@@ -1,14 +1,17 @@
1
1
  import type { AppProps } from 'next/app';
2
2
 
3
+ import { NavProvider } from '../components/context/NavProvider';
3
4
  import '../css/index.css';
4
5
  import '../css/theme.css';
5
6
 
6
7
  function MyApp({ Component, pageProps }: AppProps) {
7
8
  return (
8
- <>
9
+ // NavProvider lives above the page tree so the registry nav (shell + entities) is fetched once
10
+ // and cached across soft navigations, resetting only on hard refresh.
11
+ <NavProvider initialNav={pageProps.navData} currentSectionId={pageProps.currentSectionId}>
9
12
  {/* @ts-ignore */}
10
13
  <Component {...pageProps} />
11
- </>
14
+ </NavProvider>
12
15
  );
13
16
  }
14
17
 
@@ -0,0 +1,16 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { handleDocsRoute, serveArtifactBySegments } from '@/lib/docs-api';
3
+
4
+ /**
5
+ * Canonical artifact route — `GET {basePath}/api/docs/artifacts/{artifactPath}`.
6
+ * Serves CSS/JS/HTML/JSON/text artifacts by logical path with traversal rejection and correct
7
+ * content types. Every artifact reference in generated HTML and docs read data resolves here, from
8
+ * the filesystem in workspace mode and from the database in registry mode.
9
+ */
10
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
11
+ return handleDocsRoute(req, res, async (backend) => {
12
+ const { artifactPath } = req.query;
13
+ const segments = Array.isArray(artifactPath) ? artifactPath : artifactPath ? [artifactPath] : undefined;
14
+ await serveArtifactBySegments(res, segments, backend);
15
+ });
16
+ }
@@ -0,0 +1,50 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { singleQueryValue } from '@/lib/api/query';
3
+ import { handleDocsRoute, sendDocsError } from '@/lib/docs-api';
4
+
5
+ /**
6
+ * `GET /api/docs/assets/{collection}/{...path}`: one asset's content, by its logical path
7
+ * (`assets/icons/add.svg`, `icons-sprite.svg`, `icons.zip`, `fonts/Inter.zip`, …). Serves the bytes
8
+ * with the correct `Content-Type`/`Content-Length`, an `ETag` of the content hash, and revalidation
9
+ * caching (stable logical URLs re-validate when their mapped hash changes); object-backed content is
10
+ * redirected to a provider URL. Mode-independent.
11
+ */
12
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
13
+ return handleDocsRoute(req, res, async (backend) => {
14
+ const collection = singleQueryValue(req.query.collection);
15
+ const segments = req.query.path;
16
+ const logicalPath = Array.isArray(segments) ? segments.join('/') : segments;
17
+ if (!collection || !logicalPath) {
18
+ sendDocsError(res, 'not_found', 'Missing asset collection or path.');
19
+ return;
20
+ }
21
+
22
+ const content = await backend.getAssetContent(collection, logicalPath);
23
+ if (!content) {
24
+ sendDocsError(res, 'artifact_not_found', `Asset "${logicalPath}" was not found in collection "${collection}".`);
25
+ return;
26
+ }
27
+
28
+ // Object-backed content: redirect to the provider URL (keeps large payloads out of the function).
29
+ if (content.redirectUrl) {
30
+ res.redirect(302, content.redirectUrl);
31
+ return;
32
+ }
33
+ if (!content.body) {
34
+ sendDocsError(res, 'artifact_not_found', `Asset "${logicalPath}" has no resolvable content.`);
35
+ return;
36
+ }
37
+
38
+ const etag = `"${content.contentHash}"`;
39
+ // Logical URLs revalidate against the content-hash ETag so a republish surfaces new bytes.
40
+ res.setHeader('ETag', etag);
41
+ res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
42
+ if (req.headers['if-none-match'] === etag) {
43
+ res.status(304).end();
44
+ return;
45
+ }
46
+ res.setHeader('Content-Type', content.contentType);
47
+ res.setHeader('Content-Length', String(content.body.length));
48
+ res.status(200).send(content.body);
49
+ });
50
+ }
@@ -0,0 +1,22 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { singleQueryValue } from '@/lib/api/query';
3
+ import { handleDocsRoute, sendDocsData, sendDocsError } from '@/lib/docs-api';
4
+
5
+ /**
6
+ * `GET /api/docs/assets/{collection}`: lightweight metadata for every asset in a collection. Never
7
+ * carries a binary/SVG body: bodies are fetched one at a time via the `[...path]` content route, and
8
+ * the icon grid uses the published sprite. Mode-independent; `Cache-Control: no-store` so a publish
9
+ * surfaces without a rebuild/redeploy.
10
+ */
11
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
12
+ return handleDocsRoute(req, res, async (backend) => {
13
+ const collection = singleQueryValue(req.query.collection);
14
+ if (!collection) {
15
+ sendDocsError(res, 'not_found', 'Missing asset collection.');
16
+ return;
17
+ }
18
+ const assets = await backend.listAssets(collection);
19
+ res.setHeader('Cache-Control', 'no-store, max-age=0');
20
+ sendDocsData(res, 200, { collection, assets });
21
+ });
22
+ }
@@ -0,0 +1,33 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { singleQueryValue } from '@/lib/api/query';
3
+ import { handleDocsRoute, sendDocsError, serveArtifactBySegments } from '@/lib/docs-api';
4
+
5
+ /**
6
+ * `GET /api/docs/components/{id}/assets/{asset}` — serves a component asset artifact:
7
+ * `style`→`component/{id}.css`, `script`→`component/{id}.js`,
8
+ * `client`→`component/{id}.client.js`. Unknown asset kinds are `404 not_found`; a known asset with
9
+ * no generated/published artifact is `404 artifact_not_found`. In registry mode the artifact
10
+ * resolves from the database.
11
+ */
12
+ const ASSET_FILE_SUFFIX: Record<string, string> = {
13
+ style: 'css',
14
+ script: 'js',
15
+ client: 'client.js',
16
+ };
17
+
18
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
19
+ return handleDocsRoute(req, res, async (backend) => {
20
+ const id = singleQueryValue(req.query.id);
21
+ const asset = singleQueryValue(req.query.asset);
22
+ if (!id || !asset) {
23
+ sendDocsError(res, 'not_found', 'Missing component id or asset kind.');
24
+ return;
25
+ }
26
+ const suffix = ASSET_FILE_SUFFIX[asset];
27
+ if (!suffix) {
28
+ sendDocsError(res, 'not_found', `Unknown component asset "${asset}".`);
29
+ return;
30
+ }
31
+ await serveArtifactBySegments(res, ['component', `${id}.${suffix}`], backend);
32
+ });
33
+ }
@@ -0,0 +1,20 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { singleQueryValue } from '@/lib/api/query';
3
+ import { handleDocsRoute, sendDocsError, serveArtifactBySegments } from '@/lib/docs-api';
4
+
5
+ /**
6
+ * `GET /api/docs/components/{id}/inspect/{previewId}` — serves the inspect HTML artifact
7
+ * `component/{id}-{previewId}-inspect.html`. A metadata-only record yields
8
+ * `404 artifact_not_found`. In registry mode the artifact resolves from the database.
9
+ */
10
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
11
+ return handleDocsRoute(req, res, async (backend) => {
12
+ const id = singleQueryValue(req.query.id);
13
+ const previewId = singleQueryValue(req.query.previewId);
14
+ if (!id || !previewId) {
15
+ sendDocsError(res, 'not_found', 'Missing component id or preview id.');
16
+ return;
17
+ }
18
+ await serveArtifactBySegments(res, ['component', `${id}-${previewId}-inspect.html`], backend);
19
+ });
20
+ }
@@ -0,0 +1,21 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { singleQueryValue } from '@/lib/api/query';
3
+ import { handleDocsRoute, sendDocsError, serveArtifactBySegments } from '@/lib/docs-api';
4
+
5
+ /**
6
+ * `GET /api/docs/components/{id}/previews/{previewId}` — serves the preview HTML artifact
7
+ * `component/{id}-{previewId}.html`. A metadata-only record (no generated
8
+ * preview) yields `404 artifact_not_found`. In registry mode the artifact and its required
9
+ * references resolve from the database.
10
+ */
11
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
12
+ return handleDocsRoute(req, res, async (backend) => {
13
+ const id = singleQueryValue(req.query.id);
14
+ const previewId = singleQueryValue(req.query.previewId);
15
+ if (!id || !previewId) {
16
+ sendDocsError(res, 'not_found', 'Missing component id or preview id.');
17
+ return;
18
+ }
19
+ await serveArtifactBySegments(res, ['component', `${id}-${previewId}.html`], backend);
20
+ });
21
+ }
@@ -0,0 +1,24 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { handleDocsRoute, idFromJsonParam, sendDocsData, sendDocsError } from '@/lib/docs-api';
3
+
4
+ /**
5
+ * `GET /api/docs/components/{id}.json` — a single `ComponentListObject` including build state;
6
+ * `404 not_found` when the component does not exist. The dynamic param
7
+ * carries the explicit `.json` extension, which is stripped to recover the id. The shape is
8
+ * identical regardless of whether the filesystem or the database backs it.
9
+ */
10
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
11
+ return handleDocsRoute(req, res, async (backend) => {
12
+ const id = idFromJsonParam(req.query.id);
13
+ if (!id) {
14
+ sendDocsError(res, 'not_found', 'Missing component id.');
15
+ return;
16
+ }
17
+ const detail = await backend.getComponentDetail(id);
18
+ if (!detail) {
19
+ sendDocsError(res, 'not_found', `Component "${id}" was not found.`);
20
+ return;
21
+ }
22
+ sendDocsData(res, 200, detail);
23
+ });
24
+ }
@@ -0,0 +1,12 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { handleDocsRoute, sendDocsData } from '@/lib/docs-api';
3
+
4
+ /**
5
+ * `GET /api/docs/components.json` — the component list (`ComponentListObject[]`), each record
6
+ * carrying its `path`. Identical shape in workspace and registry mode.
7
+ */
8
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
9
+ return handleDocsRoute(req, res, async (backend) => {
10
+ sendDocsData(res, 200, await backend.listComponents());
11
+ });
12
+ }
@@ -0,0 +1,48 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { getRegistryNavData, type NavTokenSet, type SectionLink } from '@handoff/nav';
3
+ import { setNameForId } from '@handoff/registry/tokens/sets';
4
+ import { ensureGet, resolveDocsBackend, sendDocsData, sendDocsError } from '@/lib/docs-api';
5
+ // Kept static so the baked structure is traced into every registry serverless bundle.
6
+ import navShell from '@/generated/nav-shell.json';
7
+
8
+ /** Thin registry refresh adapter. The adapter/resolver owns all navigation structure and filling. */
9
+ export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
10
+ if (!ensureGet(req, res)) return;
11
+
12
+ try {
13
+ const nav = await getRegistryNavData({
14
+ shell: navShell as unknown as SectionLink[],
15
+ load: 'refresh',
16
+ basePath: process.env.HANDOFF_APP_BASE_PATH,
17
+ fetchRecords: async () => {
18
+ const backend = await resolveDocsBackend();
19
+ const safe = async <T>(read: () => Promise<T[]>): Promise<T[]> => {
20
+ try {
21
+ return (await read()) ?? [];
22
+ } catch {
23
+ return [];
24
+ }
25
+ };
26
+ const [components, patterns, pages, tokenSets] = await Promise.all([
27
+ safe(() => backend.listComponents()),
28
+ safe(() => backend.listPatterns()),
29
+ safe(() => backend.listPages()),
30
+ safe(() => backend.listTokenSets()),
31
+ ]);
32
+ return {
33
+ components,
34
+ patterns,
35
+ pages,
36
+ tokenSets: tokenSets
37
+ .filter((set) => set.kind === 'component')
38
+ .map((set) => ({ id: setNameForId(set.id), kind: 'component' })) as NavTokenSet[],
39
+ };
40
+ },
41
+ });
42
+ res.setHeader('Cache-Control', 'no-store, max-age=0');
43
+ sendDocsData(res, 200, nav);
44
+ } catch (error) {
45
+ console.error('Docs navigation response failed.', error);
46
+ sendDocsError(res, 'unexpected_error', 'Unexpected docs read API error.');
47
+ }
48
+ }
@@ -0,0 +1,19 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { singleQueryValue } from '@/lib/api/query';
3
+ import { handleDocsRoute, sendDocsError, serveArtifactBySegments } from '@/lib/docs-api';
4
+
5
+ /**
6
+ * `GET /api/docs/patterns/{id}/preview` — serves the pattern preview HTML artifact
7
+ * `pattern/{id}.html`. A metadata-only pattern yields `404 artifact_not_found`.
8
+ * In registry mode the artifact resolves from the database.
9
+ */
10
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
11
+ return handleDocsRoute(req, res, async (backend) => {
12
+ const id = singleQueryValue(req.query.id);
13
+ if (!id) {
14
+ sendDocsError(res, 'not_found', 'Missing pattern id.');
15
+ return;
16
+ }
17
+ await serveArtifactBySegments(res, ['pattern', `${id}.html`], backend);
18
+ });
19
+ }
@@ -0,0 +1,24 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { handleDocsRoute, idFromJsonParam, sendDocsData, sendDocsError } from '@/lib/docs-api';
3
+
4
+ /**
5
+ * `GET /api/docs/patterns/{id}.json` — a single `PatternListObject` including build state;
6
+ * `404 not_found` when the pattern does not exist. The dynamic param carries
7
+ * the explicit `.json` extension, which is stripped to recover the id. The shape is identical
8
+ * regardless of whether the filesystem or the database backs it.
9
+ */
10
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
11
+ return handleDocsRoute(req, res, async (backend) => {
12
+ const id = idFromJsonParam(req.query.id);
13
+ if (!id) {
14
+ sendDocsError(res, 'not_found', 'Missing pattern id.');
15
+ return;
16
+ }
17
+ const detail = await backend.getPatternDetail(id);
18
+ if (!detail) {
19
+ sendDocsError(res, 'not_found', `Pattern "${id}" was not found.`);
20
+ return;
21
+ }
22
+ sendDocsData(res, 200, detail);
23
+ });
24
+ }
@@ -0,0 +1,12 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { handleDocsRoute, sendDocsData } from '@/lib/docs-api';
3
+
4
+ /**
5
+ * `GET /api/docs/patterns.json` — the pattern list (`PatternListObject[]`).
6
+ * Identical shape in workspace and registry mode.
7
+ */
8
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
9
+ return handleDocsRoute(req, res, async (backend) => {
10
+ sendDocsData(res, 200, await backend.listPatterns());
11
+ });
12
+ }
@@ -0,0 +1,30 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next';
2
+ import { handleDocsRoute, idFromJsonParam, sendDocsData, sendDocsError } from '@/lib/docs-api';
3
+ import { tokenFormatStrings } from '@/lib/docs-api/token-detail';
4
+
5
+ /**
6
+ * `GET /api/docs/tokens/components/{id}.json` — the `component/{id}` token set: the component token
7
+ * object (`component`) plus the generated download strings (css/scss/styleDictionary/types) and the
8
+ * full artifact list. Mode-independent shape. `Cache-Control: no-store` so a publish surfaces without
9
+ * a rebuild/redeploy.
10
+ */
11
+ export default function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
12
+ return handleDocsRoute(req, res, async (backend) => {
13
+ const id = idFromJsonParam(req.query.id);
14
+ if (!id) {
15
+ sendDocsError(res, 'not_found', 'Missing component token id.');
16
+ return;
17
+ }
18
+ const detail = await backend.getTokenSetDetail(`component/${id}`);
19
+ if (!detail) {
20
+ sendDocsError(res, 'not_found', `Component token set "${id}" was not found.`);
21
+ return;
22
+ }
23
+ res.setHeader('Cache-Control', 'no-store, max-age=0');
24
+ sendDocsData(res, 200, {
25
+ component: detail.record,
26
+ ...tokenFormatStrings(detail.artifacts),
27
+ artifacts: detail.artifacts,
28
+ });
29
+ });
30
+ }