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,297 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import Handoff from '..';
4
+ import { Logger } from '../utils/logger';
5
+
6
+ /**
7
+ * Vercel Build Output API layout.
8
+ *
9
+ * `--package vercel` emits the Vercel Build Output API directory (`.vercel/output`) instead of the
10
+ * sites-directory deliverable. This module is the single seam that produces that layout; it is
11
+ * invoked as the final-assembly branch from the build functions and never re-stages or re-runs Next.
12
+ *
13
+ * Hard constraint: `.vercel/output` must live at the **repo root** (`handoff.workingPath`) with that
14
+ * exact name. Vercel ignores any `sitesOutputDirectory`-style override for this packaging.
15
+ */
16
+
17
+ /** Build Output API config version emitted in `.vercel/output/config.json`. */
18
+ const BUILD_OUTPUT_API_VERSION = 3;
19
+
20
+ /** Absolute path of the `.vercel/output` directory at the repo root. */
21
+ export const getVercelOutputPath = (handoff: Handoff): string => path.resolve(handoff.workingPath, '.vercel', 'output');
22
+
23
+ /**
24
+ * Resolve the app base path exactly as `next.config.mjs`'s `resolveBasePath` does, so the emitted
25
+ * `config.json` reflects the same `basePath` the static export was built with. Empty string means no
26
+ * base path (the Build Output API omits the field rather than carrying an empty value).
27
+ */
28
+ const resolveBasePath = (rawBasePath: string | undefined): string => {
29
+ if (!rawBasePath) {
30
+ return '';
31
+ }
32
+ const trimmed = rawBasePath.replace(/^\/+|\/+$/g, '');
33
+ return trimmed ? `/${trimmed}` : '';
34
+ };
35
+
36
+ /**
37
+ * Lay a materialized static export under `.vercel/output/static/` and write a minimal Build Output
38
+ * API v3 `config.json` carrying the app's `trailingSlash` and resolved `basePath`. Pure CDN files —
39
+ * no functions, no database.
40
+ *
41
+ * @param exportDir Absolute path of the materialized static export (the same content the static
42
+ * target writes to `out/<projectId>`).
43
+ */
44
+ export const writeStaticVercelOutput = async (handoff: Handoff, exportDir: string): Promise<void> => {
45
+ const outputPath = getVercelOutputPath(handoff);
46
+ await fs.remove(outputPath);
47
+ await fs.ensureDir(outputPath);
48
+
49
+ // Lay the export under static/ — pure CDN files, served directly by Vercel.
50
+ const staticDir = path.join(outputPath, 'static');
51
+ await fs.copy(exportDir, staticDir, { overwrite: true });
52
+
53
+ // The staged Next app sets `trailingSlash: true` (src/app/next.config.mjs); the registry/static
54
+ // exports inherit it. Carry it and the resolved base path so the static deploy serves correct URLs.
55
+ const basePath = resolveBasePath(handoff.config?.app?.base_path);
56
+ const config: { version: number; trailingSlash: boolean; basePath?: string } = {
57
+ version: BUILD_OUTPUT_API_VERSION,
58
+ trailingSlash: true,
59
+ };
60
+ if (basePath) {
61
+ config.basePath = basePath;
62
+ }
63
+ await fs.writeFile(path.join(outputPath, 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
64
+
65
+ Logger.success(`Packaged static Vercel Build Output API artifact at ${outputPath} (static/ + config.json).`);
66
+ };
67
+
68
+ /**
69
+ * Node.js runtime identifier baked into the registry function's `.vc-config.json`. The `pg` adapter
70
+ * (and the Neon serverless driver in its Node fallback) need the Node.js runtime, not the edge
71
+ * runtime — this is the required baseline. Vercel resolves the patch
72
+ * version within this major; pinning the major keeps the artifact stable.
73
+ */
74
+ const VERCEL_NODE_RUNTIME = 'nodejs20.x';
75
+
76
+ /** Name (sans `.func`) of the single registry function. Served at `/` and the catch-all route. */
77
+ const REGISTRY_FUNCTION_NAME = 'index';
78
+
79
+ /** Handler file the Node launcher executes; wraps the Next standalone server as a request listener. */
80
+ const REGISTRY_FUNCTION_HANDLER = 'index.js';
81
+
82
+ /** Catch-all docs pages may be overridden by registry records and must always reach the function. */
83
+ const MUTABLE_REGISTRY_PAGE_SRC_ROUTE = '/[...slug]';
84
+
85
+ /**
86
+ * Node launcher emitted into the registry function directory. The Next `standalone` `server.js`
87
+ * binds a port via `startServer`, which is not the Vercel Node function contract (a module exporting
88
+ * a `(req, res)` request listener). This launcher reuses the exact same request pipeline that
89
+ * `startServer` boots — the router-server handler from `next/dist/server/lib/start-server` — but
90
+ * without the port bind, so it serves requests identically to `node server.js`.
91
+ *
92
+ * Why not the public `next().getRequestHandler()`: that handler resolves page/document and `/api`
93
+ * routes, but does NOT resolve on-demand `/_next/data/<buildId>/…json` requests for
94
+ * `fallback:'blocking'` pages — it 404s them. That broke client-side `<Link>` navigation to the
95
+ * component/pattern detail pages (their data fetch went to the function and came back 404, which the
96
+ * Next router then turns into a hard navigation). `getRequestHandlers` returns the full router-server
97
+ * handler (data-route resolution, dynamic param extraction, `trailingSlash` redirects), so on-demand
98
+ * data routes resolve the same way they do under the standalone server. `minimalMode:false` keeps
99
+ * the function doing full in-process routing (the catch-all route forwards everything here).
100
+ *
101
+ * Runtime mode, adapter, and the DB env-var *name* are already baked into the resolved config the
102
+ * standalone build wrote to `.next/required-server-files.json`; the connection-string value is still
103
+ * read from the named env var at request time, never baked.
104
+ */
105
+ const REGISTRY_FUNCTION_LAUNCHER = `// Generated by handoff-app (\`build --target registry --package vercel\`). Do not edit.
106
+ // Wraps the Next.js standalone server as a Vercel Build Output API Node function.
107
+ process.env.NODE_ENV = 'production';
108
+ process.chdir(__dirname);
109
+
110
+ const requiredServerFiles = require('./.next/required-server-files.json');
111
+ const nextConfig = requiredServerFiles.config;
112
+
113
+ // The standalone runtime reads its resolved config from this env var (mirrors standalone server.js).
114
+ process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig);
115
+
116
+ // require('next') first so its module hooks are installed before pulling the internal server lib,
117
+ // matching the require order of the generated standalone server.js.
118
+ require('next');
119
+ const { getRequestHandlers } = require('next/dist/server/lib/start-server');
120
+
121
+ // Initialize the router-server handler once on cold start and reuse it across warm invocations.
122
+ // This is the same handler the standalone server.js uses; it does NOT bind a port (the port bind
123
+ // lives in startServer, which we never call), so it fits the (req, res) Node function contract.
124
+ let handlersPromise;
125
+ const ready = () =>
126
+ (handlersPromise =
127
+ handlersPromise ||
128
+ getRequestHandlers({
129
+ dir: __dirname,
130
+ port: parseInt(process.env.PORT, 10) || 3000,
131
+ hostname: process.env.HOSTNAME || 'localhost',
132
+ isDev: false,
133
+ minimalMode: false,
134
+ }));
135
+
136
+ module.exports = async (req, res) => {
137
+ const { requestHandler } = await ready();
138
+ return requestHandler(req, res);
139
+ };
140
+ `;
141
+
142
+ /** Build Output API Node serverless-function config (`.vc-config.json`). */
143
+ interface VercelNodeFunctionConfig {
144
+ runtime: string;
145
+ handler: string;
146
+ launcherType: 'Nodejs';
147
+ }
148
+
149
+ /**
150
+ * Options for {@link writeRegistryVercelOutput}. The build module owns the standalone assembly (entry
151
+ * flattening + static/public/config layout) and passes it in, keeping this seam free of any
152
+ * standalone-layout knowledge and avoiding a circular import.
153
+ */
154
+ export interface RegistryVercelOutputOptions {
155
+ /** Absolute path of the staged Next app (source of `.next/static` + `public`). */
156
+ appPath: string;
157
+ /** Absolute path of the Next `standalone` build root (`<appPath>/.next/standalone`). */
158
+ standaloneRoot: string;
159
+ /**
160
+ * Lay the standalone bundle out under the given entry dir (flatten the server entry, copy static
161
+ * assets, public tree, and runtime config beside it) and return the absolute server-entry path.
162
+ * Shared with the standalone deliverable so both layouts stay identical.
163
+ */
164
+ assembleStandalone: (handoff: Handoff, appPath: string, standaloneRoot: string, entryDir: string) => Promise<string>;
165
+ }
166
+
167
+ /**
168
+ * Copy the genuinely-static prerendered pages into the Edge CDN layer (`static/`) so their HTML and
169
+ * `_next/data` JSON are served directly by the CDN instead of invoking the single registry function.
170
+ * This is a load/latency optimization, not a correctness requirement: the function (see the launcher)
171
+ * now resolves on-demand data routes itself, so anything not copied here still resolves via the
172
+ * catch-all.
173
+ *
174
+ * Routes materialized from the mutable docs catch-all also appear in `routes` when package defaults
175
+ * are prerendered. They are identified by `srcRoute` and deliberately left behind the function so a
176
+ * published DB record can override the fallback. Other `fallback:'blocking'` dynamic routes are
177
+ * absent and naturally reach the function. Immutable pages — e.g. the header's `<Link href="/">`
178
+ * prefetch of `/_next/data/<buildId>/index.json` — are served straight from the CDN.
179
+ *
180
+ * No-op when nothing is prerendered (empty `routes`). Paths are emitted base-path-less, matching the
181
+ * existing `_next/static` copy and the registry route table.
182
+ */
183
+ const copyPrerenderedStaticPages = async (appPath: string, staticOutDir: string): Promise<void> => {
184
+ const nextDir = path.resolve(appPath, '.next');
185
+ const manifestPath = path.join(nextDir, 'prerender-manifest.json');
186
+ const buildIdPath = path.join(nextDir, 'BUILD_ID');
187
+ if (!fs.existsSync(manifestPath) || !fs.existsSync(buildIdPath)) {
188
+ return;
189
+ }
190
+
191
+ const manifest = (await fs.readJson(manifestPath)) as {
192
+ routes?: Record<string, { dataRoute?: string | null; srcRoute?: string | null }>;
193
+ };
194
+ const routes = manifest.routes ?? {};
195
+ const serverPages = path.join(nextDir, 'server', 'pages');
196
+
197
+ let copied = 0;
198
+ for (const [route, routeConfig] of Object.entries(routes)) {
199
+ // A static copy would win at the Build Output API filesystem route and permanently hide a
200
+ // published override (and its on-demand revalidation) from the registry function.
201
+ if (routeConfig.srcRoute === MUTABLE_REGISTRY_PAGE_SRC_ROUTE) {
202
+ continue;
203
+ }
204
+
205
+ // `.next/server/pages/<rel>.{html,json}` — `/` maps to `index`, nested routes keep their path.
206
+ const rel = route === '/' ? 'index' : route.replace(/^\/+/, '');
207
+ const htmlSrc = path.join(serverPages, `${rel}.html`);
208
+ const jsonSrc = path.join(serverPages, `${rel}.json`);
209
+
210
+ if (fs.existsSync(htmlSrc)) {
211
+ // trailingSlash:true → serve as `<route>/index.html`; `/` → `static/index.html`.
212
+ const htmlDestDir = route === '/' ? staticOutDir : path.join(staticOutDir, rel);
213
+ await fs.ensureDir(htmlDestDir);
214
+ await fs.copy(htmlSrc, path.join(htmlDestDir, 'index.html'), { overwrite: true });
215
+ }
216
+
217
+ // The data URL (with the build's BUILD_ID baked in) comes from the manifest's `dataRoute`.
218
+ const dataRoute = routeConfig.dataRoute;
219
+ if (dataRoute && fs.existsSync(jsonSrc)) {
220
+ const jsonDest = path.join(staticOutDir, dataRoute.replace(/^\/+/, ''));
221
+ await fs.ensureDir(path.dirname(jsonDest));
222
+ await fs.copy(jsonSrc, jsonDest, { overwrite: true });
223
+ copied += 1;
224
+ }
225
+ }
226
+
227
+ Logger.info(`Copied ${copied} prerendered static page data file(s) into the Edge CDN layer.`);
228
+ };
229
+
230
+ /**
231
+ * Package the dynamic registry app as the Vercel Build Output API directory. Lays the traced
232
+ * standalone bundle into a single Node function (`functions/index.func/`)
233
+ * wrapping the Next standalone server, copies the immutable static assets + public tree under
234
+ * `static/` for the Edge CDN, and writes a route table that serves static files first and routes
235
+ * everything else (SSR pages, the docs read API, the registry API) to the function.
236
+ *
237
+ * Hard constraint: `.vercel/output` lives at the repo root with that exact name (see module header).
238
+ */
239
+ export const writeRegistryVercelOutput = async (handoff: Handoff, options: RegistryVercelOutputOptions): Promise<void> => {
240
+ const { appPath, standaloneRoot, assembleStandalone } = options;
241
+
242
+ const outputPath = getVercelOutputPath(handoff);
243
+ await fs.remove(outputPath);
244
+ await fs.ensureDir(outputPath);
245
+
246
+ // The function directory *is* the standalone bundle root: the traced server + node_modules, the
247
+ // server entry flattened to its root, and `.next/static` + public + runtime config beside it. The
248
+ // launcher constructs a NextServer from this directory, so it needs the full standalone layout.
249
+ const functionDir = path.join(outputPath, 'functions', `${REGISTRY_FUNCTION_NAME}.func`);
250
+ await fs.ensureDir(functionDir);
251
+ await assembleStandalone(handoff, appPath, standaloneRoot, functionDir);
252
+
253
+ // The standalone `server.js` binds a port; replace the entry with the Build Output API Node
254
+ // launcher (a `(req, res)` request listener) and declare the Node runtime so the pg/Neon drivers
255
+ // work. `server.js` is left in place (harmless, unused) to keep the bundle a valid standalone too.
256
+ await fs.writeFile(path.join(functionDir, REGISTRY_FUNCTION_HANDLER), REGISTRY_FUNCTION_LAUNCHER);
257
+ const functionConfig: VercelNodeFunctionConfig = {
258
+ runtime: VERCEL_NODE_RUNTIME,
259
+ handler: REGISTRY_FUNCTION_HANDLER,
260
+ launcherType: 'Nodejs',
261
+ };
262
+ await fs.writeFile(path.join(functionDir, '.vc-config.json'), `${JSON.stringify(functionConfig, null, 2)}\n`);
263
+
264
+ // Serve the app's immutable assets straight from the Edge CDN. `_next/static` is content-hashed and
265
+ // public files sit at the URL root; copying them under `static/` lets the route table satisfy them
266
+ // via `handle: filesystem` before the catch-all reaches the function.
267
+ const staticOutDir = path.join(outputPath, 'static');
268
+ await fs.ensureDir(staticOutDir);
269
+ const nextStaticSrc = path.resolve(appPath, '.next', 'static');
270
+ if (fs.existsSync(nextStaticSrc)) {
271
+ await fs.copy(nextStaticSrc, path.join(staticOutDir, '_next', 'static'), { overwrite: true });
272
+ }
273
+ const publicSrc = path.resolve(appPath, 'public');
274
+ if (fs.existsSync(publicSrc)) {
275
+ await fs.copy(publicSrc, staticOutDir, { overwrite: true });
276
+ }
277
+
278
+ // Serve genuinely-static prerendered pages (HTML + `_next/data` JSON) from the CDN as an
279
+ // optimization so they skip the function. Dynamic `fallback:'blocking'` pages are not copied (their
280
+ // data is live), and the function now resolves their on-demand `_next/data` routes itself.
281
+ await copyPrerenderedStaticPages(appPath, staticOutDir);
282
+
283
+ // Route table: try static files first (`handle: filesystem` serves anything copied into static/),
284
+ // then route every remaining request — SSR/on-demand pages, their `/_next/data/*` routes,
285
+ // /api/docs/*, /api/registry/* — to the single function. The `.func` suffix is dropped from the
286
+ // URL, so `index.func` is reached via `/index`.
287
+ const config = {
288
+ version: BUILD_OUTPUT_API_VERSION,
289
+ routes: [{ handle: 'filesystem' }, { src: '/(.*)', dest: `/${REGISTRY_FUNCTION_NAME}` }],
290
+ };
291
+ await fs.writeFile(path.join(outputPath, 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
292
+
293
+ Logger.success(
294
+ `Packaged registry Vercel Build Output API artifact at ${outputPath} ` +
295
+ `(functions/${REGISTRY_FUNCTION_NAME}.func + static/ + config.json).`
296
+ );
297
+ };
@@ -7,6 +7,8 @@ export interface WatcherState {
7
7
  runtimeComponentsWatcher: chokidar.FSWatcher | null;
8
8
  runtimeConfigurationWatcher: chokidar.FSWatcher | null;
9
9
  componentDirectoriesWatcher: chokidar.FSWatcher | null;
10
+ /** Watches entries.patterns parent directories for newly created pattern subdirectories. */
11
+ patternDirectoriesWatcher: chokidar.FSWatcher | null;
10
12
  }
11
13
 
12
14
  /**
@@ -2,6 +2,7 @@ import chokidar from 'chokidar';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import Handoff from '..';
5
+ import { buildPatterns } from '../pipeline/patterns';
5
6
  import processComponents, { ComponentSegment } from '../transformers/preview/component/builder';
6
7
  import { buildMainCss } from '../transformers/preview/component/css';
7
8
  import { buildMainJS } from '../transformers/preview/component/javascript';
@@ -209,6 +210,15 @@ export const watchRuntimeComponents = (
209
210
  }
210
211
  };
211
212
 
213
+ const rebuildPatternComponentPreviews = async (handoff: Handoff, patternId: string) => {
214
+ const pattern = handoff.runtimeConfig?.entries?.patterns?.[patternId];
215
+ if (!pattern?.components?.length) return;
216
+
217
+ for (const ref of pattern.components) {
218
+ await processComponents(handoff, ref.id, ComponentSegment.Previews);
219
+ }
220
+ };
221
+
212
222
  /**
213
223
  * Watches the runtime configuration for changes.
214
224
  *
@@ -256,6 +266,8 @@ export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState)
256
266
  await processComponents(handoff);
257
267
  } else if (handle) {
258
268
  await handle.apply(handoff, entryAfter?.entityId);
269
+ } else if (entryAfter?.kind === 'pattern') {
270
+ await rebuildPatternComponentPreviews(handoff, entryAfter.entityId);
259
271
  } else {
260
272
  const effectiveId = entryAfter?.entityId ?? path.basename(path.dirname(changedFile));
261
273
  await processComponents(handoff, effectiveId);
@@ -273,22 +285,50 @@ export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState)
273
285
  };
274
286
 
275
287
  /**
276
- * Watches the parent component directories from config.entries.components
277
- * for new components being added. When a new config file (e.g. button.json)
278
- * appears in a new subdirectory, reloads the runtime config and restarts
279
- * the component/configuration watchers so the new component is picked up.
288
+ * Shared factory for watching a parent directory for newly created entity
289
+ * subdirectories (components or patterns). Handles the common scaffolding so
290
+ * that callers only describe what differs for their entity kind:
291
+ *
292
+ * - which config-entry paths to watch
293
+ * - how to read the current known ids after a reload
294
+ * - which WatcherState slot to use
295
+ * - a log label for the entity kind
296
+ * - the rebuild work to run once the runtime config is fresh
280
297
  */
281
- export const watchComponentDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => {
282
- if (state.componentDirectoriesWatcher) {
283
- state.componentDirectoriesWatcher.close();
298
+ const watchEntityDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions, options: {
299
+ /** Config paths to watch, e.g. handoff.config.entries?.components */
300
+ getConfigPaths: (handoff: Handoff) => string[];
301
+ /** Current known entity ids from runtime config, called again after reload to refresh the set */
302
+ getKnownIds: (handoff: Handoff) => string[];
303
+ /** Read the active FSWatcher from state */
304
+ getWatcher: (state: WatcherState) => chokidar.FSWatcher | null;
305
+ /** Write the active FSWatcher back into state */
306
+ setWatcher: (state: WatcherState, watcher: chokidar.FSWatcher) => void;
307
+ /** Prefix for the scheduleHandler key, e.g. 'newComponent' or 'newPattern' */
308
+ scheduleKeyPrefix: string;
309
+ /** Human-readable label used in log messages, e.g. 'component' or 'pattern' */
310
+ entityLabel: string;
311
+ /**
312
+ * Entity-specific rebuild work, called after reload + persistClientConfig +
313
+ * watcher re-registration. Receives the handoff instance (with fresh runtime
314
+ * config), the triggering directory name, and actual entity ids discovered
315
+ * after reload.
316
+ */
317
+ onDetected: (handoff: Handoff, context: { dirName: string; addedIds: string[] }) => Promise<void>;
318
+ }) => {
319
+ const { getConfigPaths, getKnownIds, getWatcher, setWatcher, scheduleKeyPrefix, entityLabel, onDetected } = options;
320
+
321
+ const existingWatcher = getWatcher(state);
322
+ if (existingWatcher) {
323
+ existingWatcher.close();
284
324
  }
285
325
 
286
- const componentPaths = handoff.config.entries?.components ?? [];
287
- if (componentPaths.length === 0) return;
326
+ const configPaths = getConfigPaths(handoff);
327
+ if (configPaths.length === 0) return;
288
328
 
289
329
  const dirsToWatch: string[] = [];
290
- for (const componentPath of componentPaths) {
291
- const resolved = path.resolve(handoff.workingPath, componentPath);
330
+ for (const configPath of configPaths) {
331
+ const resolved = path.resolve(handoff.workingPath, configPath);
292
332
  if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
293
333
  dirsToWatch.push(resolved);
294
334
  }
@@ -296,40 +336,124 @@ export const watchComponentDirectories = (handoff: Handoff, state: WatcherState,
296
336
 
297
337
  if (dirsToWatch.length === 0) return;
298
338
 
299
- const knownComponents = new Set(Object.keys(handoff.runtimeConfig?.entries?.components ?? {}));
339
+ // Snapshot known ids at watch startup so we only react to genuinely new
340
+ // entities, not to file changes inside already-known directories.
341
+ const knownIds = new Set(getKnownIds(handoff));
300
342
 
301
- state.componentDirectoriesWatcher = chokidar.watch(dirsToWatch, {
343
+ const watcher = chokidar.watch(dirsToWatch, {
302
344
  ...chokidarConfig,
303
345
  depth: 1,
304
346
  });
347
+ setWatcher(state, watcher);
305
348
 
306
- state.componentDirectoriesWatcher.on('add', async (file) => {
349
+ watcher.on('add', async (file) => {
307
350
  const basename = path.basename(file);
308
351
  const dirName = path.basename(path.dirname(file));
309
352
 
310
- const isConfigFile = basename.endsWith('.json') || basename.endsWith('.js') || basename.endsWith('.cjs');
311
- const isNewComponent = isConfigFile && basename.startsWith(dirName) && !knownComponents.has(dirName);
353
+ // Only react to the primary declaration file for the directory (e.g.
354
+ // button.json, button.js, or button.handoff.ts inside a button/ subdir).
355
+ // The basename.startsWith(dirName) guard below ensures .ts here only
356
+ // ever matches files named after their parent directory, not arbitrary
357
+ // TypeScript source files.
358
+ const isConfigFile = basename.endsWith('.json') || basename.endsWith('.js') || basename.endsWith('.cjs') || basename.endsWith('.ts');
359
+ const isNewEntity = isConfigFile && basename.startsWith(dirName) && !knownIds.has(dirName);
312
360
 
313
- if (!isNewComponent) return;
361
+ if (!isNewEntity) return;
314
362
 
315
- await scheduleHandler(state, `newComponent:${dirName}`, async () => {
363
+ await scheduleHandler(state, `${scheduleKeyPrefix}:${dirName}`, async () => {
316
364
  try {
317
- Logger.warn(`New component detected: ${dirName}. Reloading configuration...`);
365
+ Logger.warn(`New ${entityLabel} detected: ${dirName}. Reloading configuration...`);
366
+
367
+ // Snapshot ids before reload so we can tell whether the new entity
368
+ // was successfully registered afterwards.
369
+ const idsBefore = new Set(getKnownIds(handoff));
370
+
318
371
  handoff.reload();
319
- knownComponents.add(dirName);
320
372
 
321
- for (const id of Object.keys(handoff.runtimeConfig?.entries?.components ?? {})) {
322
- knownComponents.add(id);
373
+ // Refresh from the post-reload runtime config so any ids that were
374
+ // discovered alongside the new entity are also marked as known.
375
+ const idsAfter = getKnownIds(handoff);
376
+ const addedIds = idsAfter.filter((id) => !idsBefore.has(id));
377
+ for (const id of idsAfter) {
378
+ knownIds.add(id);
323
379
  }
324
380
 
325
381
  await persistClientConfig(handoff);
382
+
383
+ // Re-register file watchers so the new config file and any runtime
384
+ // files it introduces are covered going forward. This must happen even
385
+ // when the file failed to parse so that watchRuntimeConfiguration can
386
+ // trigger a retry the moment the file is saved with valid content.
326
387
  watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
327
388
  watchRuntimeConfiguration(handoff, state);
328
- await processComponents(handoff, dirName);
329
- await runAllFinalizers(handoff, { patternRebuildComponentIds: [dirName] });
389
+
390
+ // If no new ids appeared after reload the file was empty or invalid.
391
+ // Skip the build — watchRuntimeConfiguration will fire when it is saved.
392
+ if (addedIds.length === 0) {
393
+ Logger.warn(`${entityLabel} "${dirName}" config is empty or incomplete — build will run automatically once the file is saved.`);
394
+ return;
395
+ }
396
+
397
+ await onDetected(handoff, { dirName, addedIds });
330
398
  } catch (e) {
331
- Logger.error('Error processing new component:', e);
399
+ Logger.error(`Error processing new ${entityLabel}:`, e);
332
400
  }
333
401
  });
334
402
  });
335
403
  };
404
+
405
+ /**
406
+ * Watches the parent component directories from config.entries.components for
407
+ * new components being added. When a new config file appears in a new
408
+ * subdirectory, reloads the runtime config, restarts the component and
409
+ * configuration watchers, then builds the new component and any patterns that
410
+ * reference it.
411
+ */
412
+ export const watchComponentDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => {
413
+ watchEntityDirectories(handoff, state, chokidarConfig, {
414
+ getConfigPaths: (h) => h.config.entries?.components ?? [],
415
+ getKnownIds: (h) => Object.keys(h.runtimeConfig?.entries?.components ?? {}),
416
+ getWatcher: (s) => s.componentDirectoriesWatcher,
417
+ setWatcher: (s, w) => { s.componentDirectoriesWatcher = w; },
418
+ scheduleKeyPrefix: 'newComponent',
419
+ entityLabel: 'component',
420
+ onDetected: async (handoff, { addedIds }) => {
421
+ for (const componentId of addedIds) {
422
+ await processComponents(handoff, componentId);
423
+ await runAllFinalizers(handoff, { patternRebuildComponentIds: [componentId] });
424
+ }
425
+ },
426
+ });
427
+ };
428
+
429
+ /**
430
+ * Watches the parent pattern directories from config.entries.patterns for new
431
+ * patterns being added. The rebuild sequence intentionally differs from
432
+ * watchComponentDirectories because the dependency arrow is reversed: a new
433
+ * pattern references existing components, so we rebuild only the preview
434
+ * segment of those components (to produce the __pattern_* HTML files that
435
+ * injectPatternPreviews registered) before composing the pattern itself.
436
+ */
437
+ export const watchPatternDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => {
438
+ watchEntityDirectories(handoff, state, chokidarConfig, {
439
+ getConfigPaths: (h) => h.config.entries?.patterns ?? [],
440
+ getKnownIds: (h) => Object.keys(h.runtimeConfig?.entries?.patterns ?? {}),
441
+ getWatcher: (s) => s.patternDirectoriesWatcher,
442
+ setWatcher: (s, w) => { s.patternDirectoriesWatcher = w; },
443
+ scheduleKeyPrefix: 'newPattern',
444
+ entityLabel: 'pattern',
445
+ onDetected: async (handoff, { addedIds }) => {
446
+ // Rebuild only the previews segment for each component the new pattern
447
+ // references. Their JS/CSS/structure are unchanged — only the new
448
+ // synthetic preview HTML files need to be generated so that buildPatterns
449
+ // can assemble the pattern from them.
450
+ for (const patternId of addedIds) {
451
+ await rebuildPatternComponentPreviews(handoff, patternId);
452
+ }
453
+
454
+ // Build only the new pattern directly rather than going through
455
+ // runAllFinalizers, which would rebuild all patterns unnecessarily.
456
+ await buildPatterns(handoff, { onlyPatternIds: new Set(addedIds) });
457
+ },
458
+ });
459
+ };
@@ -1,4 +1,4 @@
1
- import { WebSocket } from 'ws';
1
+ import { WebSocket, WebSocketServer } from 'ws';
2
2
  import { Logger } from '../utils/logger';
3
3
 
4
4
  export interface HandoffWebSocket extends WebSocket {
@@ -15,7 +15,7 @@ export interface HandoffWebSocket extends WebSocket {
15
15
  export const createWebSocketServer = async (port: number = 3001) => {
16
16
  Logger.info(`Starting WebSocket server for live reload on port ${port}…`);
17
17
 
18
- const wss = new WebSocket.Server({ port });
18
+ const wss = new WebSocketServer({ port });
19
19
 
20
20
  wss.on('listening', () => {
21
21
  Logger.info(`WebSocket server listening on ws://localhost:${port} (live reload)`);
@@ -0,0 +1,14 @@
1
+ # Artifacts Module
2
+
3
+ Shared v2 artifact contract: the structured artifact/build-metadata model the workspace and
4
+ registry tracks both produce and consume, plus the one canonical artifact URL builder used by
5
+ all generated HTML and docs read data. Pure types and utilities — no artifact is read or written
6
+ here; consumers (store, docs read API, build, publish/ingest) come in later issues.
7
+
8
+ ## Files
9
+
10
+ | File | Purpose |
11
+ |------|---------|
12
+ | `types.ts` | `ArtifactDescriptor`, `ArtifactReference`, `ArtifactBuildMetadata`, `ArtifactSizeDiagnostics`, artifact kind/owner/status unions, and shared-artifact/manifest path + format-version constants |
13
+ | `url.ts` | `buildArtifactUrl()`, `normalizeBasePath()`, `ARTIFACTS_ROUTE_SEGMENT` — centralized `{basePath}/api/docs/artifacts/{path}` URL builder (segment-encoded, basePath-preserved, never query strings) |
14
+ | `index.ts` | Barrel re-exports |
@@ -0,0 +1,17 @@
1
+ export {
2
+ ARTIFACT_METADATA_PATH,
3
+ REACT_PREVIEW_FORMAT_VERSION,
4
+ SHARED_MAIN_CSS_ARTIFACT_PATH,
5
+ SHARED_MAIN_JS_ARTIFACT_PATH,
6
+ SHARED_STYLES_CSS_ARTIFACT_PATH,
7
+ type ArtifactBuildMetadata,
8
+ type ArtifactBuildStatus,
9
+ type ArtifactDescriptor,
10
+ type ArtifactKind,
11
+ type ArtifactOwnerKind,
12
+ type ArtifactReference,
13
+ type ArtifactReferenceKind,
14
+ type ArtifactSizeDiagnostics,
15
+ } from './types';
16
+
17
+ export { ARTIFACTS_ROUTE_SEGMENT, buildArtifactUrl, normalizeBasePath } from './url';