handoff-app 2.0.0-8 → 2.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 (322) hide show
  1. package/{Changelog.md → CHANGELOG.md} +85 -1
  2. package/{License.md → LICENSE.md} +1 -1
  3. package/README.md +816 -393
  4. package/RECIPES.md +975 -0
  5. package/UPGRADE.md +57 -9
  6. package/components.json +20 -20
  7. package/config/docs/foundations/colors/guidelines.md +0 -1
  8. package/config/docs/foundations/colors.md +0 -1
  9. package/config/docs/foundations/effects/guidelines.md +0 -1
  10. package/config/docs/foundations/effects/resources.md +0 -1
  11. package/config/docs/foundations/effects.md +0 -1
  12. package/config/docs/foundations/fonts.md +0 -1
  13. package/config/docs/foundations/grid.md +0 -1
  14. package/config/docs/foundations/icon.md +0 -1
  15. package/config/docs/foundations/icons/guidelines.md +0 -1
  16. package/config/docs/foundations/icons.md +0 -1
  17. package/config/docs/foundations/images/guidelines.md +0 -1
  18. package/config/docs/foundations/images/resources.md +0 -1
  19. package/config/docs/foundations/images.md +0 -1
  20. package/config/docs/foundations/logo/resources.md +0 -1
  21. package/config/docs/foundations/logo.md +0 -1
  22. package/config/docs/foundations/typography/guidelines.md +0 -1
  23. package/config/docs/foundations/typography/resources.md +0 -1
  24. package/config/docs/foundations/typography.md +0 -1
  25. package/config/docs/foundations.md +0 -1
  26. package/config/docs/guidelines.md +0 -1
  27. package/config/docs/index.md +0 -1
  28. package/config/docs/system/tokens/foundations/colors.md +0 -4
  29. package/config/docs/system/tokens/foundations/effects.md +0 -4
  30. package/config/docs/system/tokens/foundations/typography.md +0 -2
  31. package/config/docs/system.md +0 -2
  32. package/config/templates/page.md +2 -1
  33. package/config/templates/page.mdx +2 -1
  34. package/dist/ai/connections.d.ts +66 -0
  35. package/dist/ai/connections.js +149 -0
  36. package/dist/ai/define.d.ts +10 -0
  37. package/dist/ai/define.js +13 -0
  38. package/dist/ai/types.d.ts +26 -0
  39. package/dist/ai/types.js +10 -0
  40. package/dist/app-builder/build.d.ts +14 -7
  41. package/dist/app-builder/build.js +195 -66
  42. package/dist/app-builder/client-config.js +3 -2
  43. package/dist/app-builder/paths.d.ts +0 -12
  44. package/dist/app-builder/paths.js +3 -20
  45. package/dist/app-builder/static-export.js +1 -1
  46. package/dist/app-builder/vercel-output.d.ts +1 -1
  47. package/dist/app-builder/vercel-output.js +5 -4
  48. package/dist/cache/build-cache.d.ts +27 -4
  49. package/dist/cache/build-cache.js +114 -47
  50. package/dist/cache/build-inputs.d.ts +13 -0
  51. package/dist/cache/build-inputs.js +19 -0
  52. package/dist/cache/index.d.ts +2 -1
  53. package/dist/cache/index.js +5 -1
  54. package/dist/catalog/normalize.js +3 -0
  55. package/dist/catalog/previews.js +5 -4
  56. package/dist/catalog/types.d.ts +8 -0
  57. package/dist/cli/create.js +1 -2
  58. package/dist/commands/build/index.d.ts +1 -1
  59. package/dist/commands/build/index.js +1 -1
  60. package/dist/commands/utils.d.ts +3 -3
  61. package/dist/config/defaults.js +7 -4
  62. package/dist/config/from-env.d.ts +4 -0
  63. package/dist/config/from-env.js +38 -1
  64. package/dist/config/helpers.js +2 -2
  65. package/dist/config/index.d.ts +0 -1
  66. package/dist/config/index.js +1 -4
  67. package/dist/config/loader.js +28 -2
  68. package/dist/config/resolve-env.d.ts +2 -1
  69. package/dist/config/resolve-env.js +28 -12
  70. package/dist/config/runtime.js +9 -11
  71. package/dist/handlebars.d.ts +10 -2
  72. package/dist/index.d.ts +12 -7
  73. package/dist/index.js +18 -22
  74. package/dist/nav/index.d.ts +2 -2
  75. package/dist/nav/registry.d.ts +3 -3
  76. package/dist/nav/registry.js +1 -1
  77. package/dist/nav/workspace.d.ts +3 -3
  78. package/dist/nav/workspace.js +1 -1
  79. package/dist/pipeline/figma.d.ts +13 -2
  80. package/dist/pipeline/figma.js +86 -46
  81. package/dist/pipeline/styles.js +1 -1
  82. package/dist/pipeline/validation.d.ts +1 -1
  83. package/dist/pipeline/validation.js +3 -3
  84. package/dist/providers.d.ts +10 -0
  85. package/dist/providers.js +13 -0
  86. package/dist/registry/asset-storage/define.d.ts +5 -5
  87. package/dist/registry/asset-storage/define.js +4 -4
  88. package/dist/registry/asset-storage/resolve.d.ts +9 -19
  89. package/dist/registry/asset-storage/resolve.js +33 -21
  90. package/dist/registry/asset-storage/types.d.ts +37 -17
  91. package/dist/registry/asset-storage/types.js +5 -5
  92. package/dist/registry/auth/ai-keys.d.ts +32 -0
  93. package/dist/registry/auth/ai-keys.js +69 -0
  94. package/dist/registry/auth/crypto.d.ts +17 -0
  95. package/dist/registry/auth/crypto.js +61 -1
  96. package/dist/registry/auth/index.d.ts +2 -1
  97. package/dist/registry/auth/index.js +11 -2
  98. package/dist/registry/checkout/index.d.ts +2 -1
  99. package/dist/registry/checkout/index.js +57 -127
  100. package/dist/registry/client.js +53 -17
  101. package/dist/registry/db/driver.js +3 -3
  102. package/dist/registry/db/schema.d.ts +261 -3
  103. package/dist/registry/db/schema.js +29 -4
  104. package/dist/registry/email/define.d.ts +10 -0
  105. package/dist/registry/email/define.js +13 -0
  106. package/dist/registry/email/providers/smtp.d.ts +10 -0
  107. package/dist/registry/email/providers/smtp.js +73 -0
  108. package/dist/registry/email/resolve.d.ts +28 -0
  109. package/dist/registry/email/resolve.js +74 -0
  110. package/dist/registry/email/types.d.ts +24 -0
  111. package/dist/registry/email/types.js +6 -0
  112. package/dist/registry/path.d.ts +2 -0
  113. package/dist/registry/path.js +11 -1
  114. package/dist/registry/publish/package.js +8 -0
  115. package/dist/registry/transfer.d.ts +4 -0
  116. package/dist/store/registry.d.ts +3 -3
  117. package/dist/store/registry.js +4 -4
  118. package/dist/transformers/plugins/ssr-render.js +40 -15
  119. package/dist/transformers/preview/component/builder.d.ts +1 -9
  120. package/dist/transformers/preview/component/builder.js +17 -17
  121. package/dist/transformers/preview/component/css.d.ts +3 -1
  122. package/dist/transformers/preview/component/css.js +92 -51
  123. package/dist/transformers/preview/component/html.d.ts +3 -1
  124. package/dist/transformers/preview/component/html.js +6 -2
  125. package/dist/transformers/preview/component/javascript.d.ts +2 -1
  126. package/dist/transformers/preview/component/javascript.js +10 -4
  127. package/dist/transformers/preview/pattern/html.js +3 -1
  128. package/dist/transformers/preview/types.d.ts +13 -0
  129. package/dist/transformers/utils/build.d.ts +24 -0
  130. package/dist/transformers/utils/build.js +100 -1
  131. package/dist/transformers/utils/module.js +2 -1
  132. package/dist/types/config.d.ts +135 -18
  133. package/dist/utils/path.d.ts +1 -5
  134. package/dist/utils/path.js +6 -23
  135. package/docs/api.md +6 -4
  136. package/docs/cli.md +275 -54
  137. package/drizzle/0007_add_ai_keys.sql +11 -0
  138. package/drizzle/0008_add_source_dir.sql +2 -0
  139. package/drizzle/meta/0007_snapshot.json +2013 -0
  140. package/drizzle/meta/0008_snapshot.json +2025 -0
  141. package/drizzle/meta/_journal.json +14 -0
  142. package/package.json +19 -8
  143. package/src/ai/README.md +42 -0
  144. package/src/ai/connections.ts +189 -0
  145. package/src/ai/define.ts +15 -0
  146. package/src/ai/types.ts +30 -0
  147. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  148. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  149. package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
  150. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  151. package/src/app/components/AiAssistant/dockState.ts +86 -0
  152. package/src/app/components/AiAssistant/toolLabel.ts +24 -0
  153. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  154. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  155. package/src/app/components/AiAssistant/usePageContext.ts +63 -0
  156. package/src/app/components/Auth/AccountLayout.tsx +58 -19
  157. package/src/app/components/Auth/AuthShell.tsx +27 -33
  158. package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
  159. package/src/app/components/Auth/installStatus.ts +29 -0
  160. package/src/app/components/Component/PageSliceResolver.tsx +1 -0
  161. package/src/app/components/Component/Preview.tsx +383 -189
  162. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
  163. package/src/app/components/Foundations/ColorGrid.tsx +13 -14
  164. package/src/app/components/Layout/Header.tsx +21 -15
  165. package/src/app/components/Layout/Main.tsx +2 -2
  166. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  167. package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
  168. package/src/app/components/Navigation/MobileNav.tsx +2 -2
  169. package/src/app/components/Navigation/SideNav.tsx +1 -15
  170. package/src/app/components/context/NavProvider.tsx +7 -3
  171. package/src/app/components/ui/attachment.tsx +50 -0
  172. package/src/app/components/ui/bubble.tsx +57 -0
  173. package/src/app/components/ui/button.tsx +3 -1
  174. package/src/app/components/ui/confirm-dialog.tsx +87 -0
  175. package/src/app/components/ui/dialog.tsx +7 -2
  176. package/src/app/components/ui/marker.tsx +56 -0
  177. package/src/app/components/ui/sidebar.tsx +3 -1
  178. package/src/app/components/util/index.ts +3 -12
  179. package/src/app/css/index.css +64 -0
  180. package/src/app/lib/ai/auth.ts +33 -0
  181. package/src/app/lib/ai/context.ts +99 -0
  182. package/src/app/lib/ai/model.ts +79 -0
  183. package/src/app/lib/ai/prompt.ts +36 -0
  184. package/src/app/lib/ai/resolve.ts +97 -0
  185. package/src/app/lib/ai/tools.ts +120 -0
  186. package/src/app/lib/api/methods.ts +13 -0
  187. package/src/app/lib/asset-storage.ts +35 -36
  188. package/src/app/lib/auth/api.ts +4 -12
  189. package/src/app/lib/auth/email.ts +96 -23
  190. package/src/app/lib/docs-api/page-rendering.ts +1 -1
  191. package/src/app/lib/docs-api/registry.ts +2 -3
  192. package/src/app/lib/docs-api/runtime-config.ts +47 -21
  193. package/src/app/lib/mcp/components.ts +523 -0
  194. package/src/app/lib/mcp/page-url.ts +24 -0
  195. package/src/app/lib/mcp/pages.ts +84 -0
  196. package/src/app/lib/mcp/result.ts +33 -0
  197. package/src/app/lib/mcp/server.ts +39 -318
  198. package/src/app/lib/mcp/tokens.ts +334 -0
  199. package/src/app/lib/mcp/tool.ts +19 -0
  200. package/src/app/lib/registry-api/asset-transfer.ts +119 -11
  201. package/src/app/lib/registry-api/errors.ts +2 -0
  202. package/src/app/lib/registry-api/handler.ts +7 -1
  203. package/src/app/lib/registry-api/transfer.ts +28 -6
  204. package/src/app/lib/server-module.ts +34 -0
  205. package/src/app/lib/utils.ts +20 -0
  206. package/src/app/next.config.mjs +26 -24
  207. package/src/app/pages/[...slug]/index.tsx +2 -1
  208. package/src/app/pages/_app.tsx +6 -2
  209. package/src/app/pages/_document.tsx +6 -0
  210. package/src/app/pages/account/ai.tsx +183 -0
  211. package/src/app/pages/account/index.tsx +68 -68
  212. package/src/app/pages/account/tokens.tsx +37 -7
  213. package/src/app/pages/account/users.tsx +75 -15
  214. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  215. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  216. package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
  217. package/src/app/pages/api/admin/users/index.ts +2 -2
  218. package/src/app/pages/api/ai/chat.ts +115 -0
  219. package/src/app/pages/api/ai/connections.ts +23 -0
  220. package/src/app/pages/api/auth/request-reset.ts +14 -16
  221. package/src/app/pages/api/docs/nav.json.ts +1 -1
  222. package/src/app/pages/api/install.ts +3 -3
  223. package/src/app/pages/api/mcp.ts +6 -0
  224. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
  225. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
  226. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  227. package/src/app/pages/cli/device.tsx +10 -5
  228. package/src/app/pages/foundations/colors.tsx +9 -8
  229. package/src/app/pages/foundations/index.tsx +3 -3
  230. package/src/app/pages/index.tsx +2 -2
  231. package/src/app/pages/install.tsx +7 -2
  232. package/src/app/pages/login.tsx +32 -27
  233. package/src/app/pages/reset-password.tsx +21 -20
  234. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  235. package/src/app/pages/system/index.tsx +11 -11
  236. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  237. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  238. package/src/app-builder/README.md +2 -1
  239. package/src/app-builder/build.ts +218 -73
  240. package/src/app-builder/client-config.ts +3 -2
  241. package/src/app-builder/paths.ts +3 -22
  242. package/src/app-builder/static-export.ts +1 -1
  243. package/src/app-builder/vercel-output.ts +5 -4
  244. package/src/cache/README.md +4 -1
  245. package/src/cache/build-cache.ts +128 -43
  246. package/src/cache/build-inputs.ts +28 -0
  247. package/src/cache/index.ts +4 -0
  248. package/src/catalog/README.md +3 -0
  249. package/src/catalog/normalize.ts +2 -0
  250. package/src/catalog/previews.ts +6 -3
  251. package/src/catalog/types.ts +8 -0
  252. package/src/cli/create.ts +1 -2
  253. package/src/commands/build/index.ts +1 -1
  254. package/src/config/README.md +10 -9
  255. package/src/config/defaults.ts +7 -4
  256. package/src/config/from-env.ts +33 -0
  257. package/src/config/helpers.ts +1 -3
  258. package/src/config/index.ts +0 -3
  259. package/src/config/loader.ts +27 -3
  260. package/src/config/resolve-env.ts +28 -15
  261. package/src/config/runtime.ts +10 -5
  262. package/src/handlebars.ts +8 -2
  263. package/src/index.ts +33 -29
  264. package/src/nav/index.ts +2 -2
  265. package/src/nav/registry.ts +3 -3
  266. package/src/nav/workspace.ts +3 -3
  267. package/src/pipeline/README.md +1 -1
  268. package/src/pipeline/figma.ts +98 -49
  269. package/src/pipeline/styles.ts +1 -1
  270. package/src/pipeline/validation.ts +3 -3
  271. package/src/providers.ts +18 -0
  272. package/src/registry/asset-storage/define.ts +6 -6
  273. package/src/registry/asset-storage/resolve.ts +38 -37
  274. package/src/registry/asset-storage/types.ts +39 -17
  275. package/src/registry/auth/ai-keys.ts +62 -0
  276. package/src/registry/auth/crypto.ts +63 -1
  277. package/src/registry/auth/index.ts +11 -1
  278. package/src/registry/checkout/index.ts +60 -138
  279. package/src/registry/client.ts +60 -20
  280. package/src/registry/db/driver.ts +2 -2
  281. package/src/registry/db/schema.ts +49 -22
  282. package/src/registry/email/define.ts +15 -0
  283. package/src/registry/email/providers/smtp.ts +39 -0
  284. package/src/registry/email/resolve.ts +94 -0
  285. package/src/registry/email/types.ts +28 -0
  286. package/src/registry/path.ts +11 -0
  287. package/src/registry/publish/package.ts +9 -0
  288. package/src/registry/transfer.ts +4 -0
  289. package/src/store/registry.ts +7 -7
  290. package/src/transformers/plugins/ssr-render.ts +53 -16
  291. package/src/transformers/preview/component/builder.ts +19 -25
  292. package/src/transformers/preview/component/css.ts +101 -53
  293. package/src/transformers/preview/component/html.ts +6 -2
  294. package/src/transformers/preview/component/javascript.ts +14 -4
  295. package/src/transformers/preview/pattern/html.ts +3 -1
  296. package/src/transformers/preview/types.ts +14 -0
  297. package/src/transformers/utils/build.ts +93 -0
  298. package/src/transformers/utils/module.ts +4 -2
  299. package/src/types/config.ts +148 -20
  300. package/src/utils/README.md +1 -1
  301. package/src/utils/path.ts +6 -29
  302. package/templates/default/handoff.config.js.tpl +10 -2
  303. package/templates/default/handoff.config.ts.tpl +10 -2
  304. package/templates/default/handoff.config.with-examples.js.tpl +10 -2
  305. package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
  306. package/templates/default/package.json.tpl +2 -2
  307. package/.editorconfig +0 -19
  308. package/.eslintrc.json +0 -4
  309. package/.prettierignore +0 -0
  310. package/.prettierrc.json +0 -8
  311. package/.vscode/settings.json +0 -17
  312. package/dist/config/validator.d.ts +0 -9
  313. package/dist/config/validator.js +0 -24
  314. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
  315. package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
  316. package/src/app/components/old/Header.tsx +0 -89
  317. package/src/app/lib/mcp/shape.ts +0 -596
  318. package/src/app/pages/assets/fonts.tsx +0 -129
  319. package/src/app/pages/assets/index.tsx +0 -91
  320. package/src/app/pages/assets/logos.tsx +0 -75
  321. package/src/config/validator.ts +0 -22
  322. package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Registry email settings resolution.
3
+ *
4
+ * `runtime.registry.email.provider` selects the built-in `smtp` provider or a `custom` module. Without
5
+ * an email block, email delivery is off. The build bakes the provider, the module location, and the
6
+ * options. An option literal is baked as it is. An environment reference stays `{ $env }` and is
7
+ * resolved at request time.
8
+ */
9
+
10
+ import { HandoffConfigError } from '../../config/errors';
11
+ import type { ResolvedConfig } from '../../types/config';
12
+
13
+ export type EmailProviderKind = 'smtp' | 'custom';
14
+
15
+ const PROVIDERS: readonly EmailProviderKind[] = ['smtp', 'custom'];
16
+
17
+ /** The options each built-in provider accepts. A custom provider accepts any. */
18
+ const BUILT_IN_OPTIONS: Record<Exclude<EmailProviderKind, 'custom'>, readonly string[]> = {
19
+ smtp: ['host', 'port', 'secure', 'user', 'password'],
20
+ };
21
+
22
+ /** Email settings as they are baked: literals and `{ $env }` references, never secret values. */
23
+ export interface EmailSettings {
24
+ from?: string;
25
+ /** Unset when the config has no email block. */
26
+ provider?: EmailProviderKind;
27
+ /** For `custom`: the server-only provider module path. */
28
+ module?: string;
29
+ options: Record<string, unknown>;
30
+ }
31
+
32
+ const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
33
+ value !== null && typeof value === 'object' && !Array.isArray(value);
34
+
35
+ /** Resolve and validate the email block from a loaded config (CLI/build side). An unusable block fails the build. */
36
+ export const resolveEmailFromConfig = (config: ResolvedConfig | null | undefined): EmailSettings => {
37
+ const email = config?.runtime?.registry?.email as unknown as Record<string, unknown> | undefined;
38
+ const fail = (problem: string): never => {
39
+ throw new HandoffConfigError(`Config "runtime.registry.email": ${problem}`);
40
+ };
41
+
42
+ if (email === undefined) return { options: {} };
43
+ const provider = email.provider as EmailProviderKind;
44
+ if (!PROVIDERS.includes(provider)) {
45
+ fail(
46
+ email.provider === undefined
47
+ ? '"provider" is required. Use "smtp" or "custom".'
48
+ : `unknown provider ${JSON.stringify(provider)}. Use "smtp" or "custom".`
49
+ );
50
+ }
51
+ if (email.options !== undefined && !isPlainRecord(email.options)) {
52
+ fail('"options" must be an object.');
53
+ }
54
+ const options = { ...((email.options as Record<string, unknown> | undefined) ?? {}) };
55
+ const module = typeof email.module === 'string' ? email.module.trim() : undefined;
56
+
57
+ if (provider === 'custom') {
58
+ if (!module) fail('provider "custom" needs a "module" that default-exports a defineEmailProvider() result.');
59
+ } else {
60
+ if (email.module !== undefined) fail(`"module" is used only by provider "custom", not "${provider}".`);
61
+ const allowed = BUILT_IN_OPTIONS[provider];
62
+ const unknown = Object.keys(options).filter((key) => !allowed.includes(key));
63
+ if (unknown.length > 0) {
64
+ fail(
65
+ `provider "${provider}" does not support ${unknown.map((key) => `"options.${key}"`).join(', ')}. ` +
66
+ `Use ${allowed.map((key) => `"${key}"`).join(', ')}.`
67
+ );
68
+ }
69
+ }
70
+ if (provider === 'smtp' && options.host === undefined) fail('provider "smtp" needs "options.host".');
71
+
72
+ const from = typeof email.from === 'string' ? email.from.trim() || undefined : undefined;
73
+ return { from, provider, ...(module ? { module } : {}), options };
74
+ };
75
+
76
+ /** Read baked settings back leniently (server side): a malformed value must not take the server down. */
77
+ export const parseEmailSettings = (raw: { from?: unknown; provider?: unknown; module?: unknown; options?: unknown }): EmailSettings => {
78
+ let options: unknown = raw.options;
79
+ if (typeof options === 'string') {
80
+ try {
81
+ options = options.trim() ? JSON.parse(options) : {};
82
+ } catch {
83
+ options = {};
84
+ }
85
+ }
86
+ const provider = PROVIDERS.find((kind) => kind === raw.provider);
87
+ const module = typeof raw.module === 'string' ? raw.module.trim() : '';
88
+ return {
89
+ from: typeof raw.from === 'string' ? raw.from.trim() || undefined : undefined,
90
+ ...(provider ? { provider } : {}),
91
+ ...(module ? { module } : {}),
92
+ options: isPlainRecord(options) ? options : {},
93
+ };
94
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Server-only email provider contract. Handoff renders every message, and a provider only delivers it.
3
+ * SMTP is built in. Anything else is a {@link defineEmailProvider} module.
4
+ */
5
+
6
+ /** One rendered message. */
7
+ export interface EmailMessage {
8
+ from: string;
9
+ to: string;
10
+ subject: string;
11
+ html: string;
12
+ text: string;
13
+ }
14
+
15
+ /** The pluggable provider contract. `send` throws when the message is not accepted for delivery. */
16
+ export interface EmailProvider {
17
+ send(message: EmailMessage): Promise<void>;
18
+ }
19
+
20
+ /** Context passed to a provider factory. */
21
+ export interface EmailProviderContext {
22
+ /** `runtime.registry.email.options`, with environment references resolved to their values. */
23
+ options: Record<string, unknown>;
24
+ env: NodeJS.ProcessEnv;
25
+ }
26
+
27
+ /** A custom provider may export the provider directly or a factory that builds it from its context. */
28
+ export type EmailProviderFactory = (context: EmailProviderContext) => EmailProvider | Promise<EmailProvider>;
@@ -13,6 +13,17 @@ export const isSafeRelativePath = (value: string): boolean => {
13
13
 
14
14
  export const normalizeRelativePath = (value: string): string => value.replace(/\\/g, '/');
15
15
 
16
+ /** Unlike {@link isSafeRelativePath}, it can start with `../`. */
17
+ export const isWorkspaceRelativePath = (value: unknown): value is string =>
18
+ typeof value === 'string' &&
19
+ value !== '' &&
20
+ value !== '.' &&
21
+ !value.includes('\0') &&
22
+ !value.includes('\\') &&
23
+ !value.startsWith('/') &&
24
+ !/^[a-zA-Z]:/.test(value) &&
25
+ path.posix.normalize(value) === value;
26
+
16
27
  export const isSafePathSegment = (value: string): boolean =>
17
28
  typeof value === 'string' && value === value.trim() && isSafeRelativePath(value) && !normalizeRelativePath(value).includes('/');
18
29
 
@@ -88,6 +88,13 @@ const enrichItem = async (
88
88
  return item;
89
89
  };
90
90
 
91
+ const findSourceDir = async (handoff: Handoff, kind: RenderedEntityKind, id: string): Promise<string | undefined> => {
92
+ const runtime = kind === 'component' ? await handoff.store.components.get(id) : await handoff.store.patterns.get(id);
93
+ const sourceDir = (runtime as { path?: string } | null)?.path;
94
+ const relativeDir = sourceDir ? path.relative(handoff.workingPath, sourceDir) : '';
95
+ return relativeDir && !path.isAbsolute(relativeDir) ? relativeDir.split(path.sep).join('/') : undefined;
96
+ };
97
+
91
98
  /** Map an entity's related source files to transfer files, dropping workspace-only declarations. */
92
99
  const collectSourceFiles = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<TransferFile[]> => {
93
100
  const store = kind === 'component' ? handoff.store.components : kind === 'pattern' ? handoff.store.patterns : handoff.store.pages;
@@ -164,6 +171,7 @@ const buildComponentPackage = async (handoff: Handoff, id: string): Promise<Tran
164
171
  const artifacts = Array.from(byPath.values());
165
172
  return {
166
173
  item: await enrichItem(handoff, 'component', id, summary as unknown as Record<string, unknown>),
174
+ sourceDir: await findSourceDir(handoff, 'component', id),
167
175
  files: await collectSourceFiles(handoff, 'component', id),
168
176
  artifacts,
169
177
  build: createArtifactBuild(artifacts),
@@ -214,6 +222,7 @@ const buildPatternPackage = async (handoff: Handoff, id: string): Promise<Transf
214
222
  const artifacts = Array.from(byPath.values());
215
223
  return {
216
224
  item: await enrichItem(handoff, 'pattern', id, summary as unknown as Record<string, unknown>),
225
+ sourceDir: await findSourceDir(handoff, 'pattern', id),
217
226
  files: await collectSourceFiles(handoff, 'pattern', id),
218
227
  artifacts,
219
228
  build: createArtifactBuild(artifacts),
@@ -65,6 +65,8 @@ export interface TransferBuild {
65
65
  export interface TransferPackage {
66
66
  /** Normalized component, pattern, or page record stored as the served record. */
67
67
  item: Record<string, unknown>;
68
+ /** Workspace-relative entity directory, for components and patterns. It can start with `../`. */
69
+ sourceDir?: string;
68
70
  files: TransferFile[];
69
71
  artifacts: TransferArtifact[];
70
72
  build: TransferBuild;
@@ -78,6 +80,8 @@ export interface CheckoutPayload {
78
80
  kind: TransferEntityKind;
79
81
  /** Normalized record used to reconstruct the entity. */
80
82
  item: Record<string, unknown>;
83
+ /** The published {@link TransferPackage.sourceDir}. */
84
+ sourceDir?: string;
81
85
  files: TransferFile[];
82
86
  }
83
87
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { orderPreviews } from '../catalog/previews';
18
18
  import { and, eq, inArray, or, sql, type SQLWrapper } from 'drizzle-orm';
19
- import type { AssetStorage, AssetStorageReadResult } from '../registry/asset-storage/types';
19
+ import type { StorageProvider, StorageReadResult } from '../registry/asset-storage/types';
20
20
  import type { RegistryDatabase } from '../registry/db/client';
21
21
  import {
22
22
  assetBlobs,
@@ -53,11 +53,11 @@ import type {
53
53
  export interface RegistryStoreContext {
54
54
  db: RegistryDatabase;
55
55
  /**
56
- * Resolve the {@link AssetStorage} adapter for a blob's recorded provider (or `null` for the inline
56
+ * Resolve the {@link StorageProvider} for a blob's recorded provider (or `null` for the inline
57
57
  * `database` provider). Injected by the app runtime so `src/store` stays free of app-side imports;
58
58
  * when omitted, only inline DB-backed asset content is resolvable.
59
59
  */
60
- resolveAssetAdapter?: (provider: AssetStorageProvider) => Promise<AssetStorage | null>;
60
+ resolveStorageProvider?: (provider: AssetStorageProvider) => Promise<StorageProvider | null>;
61
61
  }
62
62
 
63
63
  /** A persisted text-file row, as selected from `component_files`/`pattern_files`. */
@@ -422,14 +422,14 @@ export class RegistryAssetStore implements AssetStore {
422
422
  }
423
423
 
424
424
  // Object-backed - resolve through the provider that stored it.
425
- if (!row.storageRef || !this.context.resolveAssetAdapter) {
425
+ if (!row.storageRef || !this.context.resolveStorageProvider) {
426
426
  return null;
427
427
  }
428
- const adapter = await this.context.resolveAssetAdapter(row.storageProvider);
429
- if (!adapter) {
428
+ const storage = await this.context.resolveStorageProvider(row.storageProvider);
429
+ if (!storage) {
430
430
  return null;
431
431
  }
432
- const result: AssetStorageReadResult = await adapter.get(row.storageRef);
432
+ const result: StorageReadResult = await storage.get(row.storageRef);
433
433
  if (result.kind === 'redirect') {
434
434
  return { ...base, redirectUrl: result.url };
435
435
  }
@@ -25,7 +25,7 @@ import {
25
25
  type SharedArtifactPresence,
26
26
  } from '../preview/component/shared-artifacts';
27
27
  import { TransformComponentTokensResult } from '../preview/types';
28
- import { DEFAULT_CLIENT_BUILD_CONFIG, createReactResolvePlugin } from '../utils/build';
28
+ import { DEFAULT_CLIENT_BUILD_CONFIG, createReactResolvePlugin, withStyleLoaders } from '../utils/build';
29
29
  import { formatHtml, trimPreview } from '../utils/html';
30
30
  import { buildAndEvaluateModule } from '../utils/module';
31
31
  import { loadSchemaFromComponent, loadSchemaFromFile } from '../utils/schema-loader';
@@ -138,13 +138,31 @@ async function loadComponentSchemaAndModule(
138
138
  * @param componentExport - Export holding the implementation; the default export when absent
139
139
  * @returns Client-side hydration source code
140
140
  */
141
- function generateClientHydrationSource(componentId: string, componentPath: string, componentExport?: string): string {
141
+ function generateClientHydrationSource(
142
+ componentId: string,
143
+ componentPath: string,
144
+ componentExport?: string,
145
+ declarationPath?: string
146
+ ): string {
142
147
  const importClause = componentExport && componentExport !== 'default' ? `{ ${componentExport} as Component }` : 'Component';
148
+ // A function cannot travel in the JSON props, so the client re-reads `render` from the module that
149
+ // defines it. Conditional, so an unaffected component does not ship its declaration's imports.
150
+ const declarationImport = declarationPath ? `import * as previewDeclaration from '${normalizePath(declarationPath)}';` : '';
151
+ const resolveComponent = declarationPath
152
+ ? `(mount) => {
153
+ const previewKey = mount.getAttribute('data-handoff-preview');
154
+ const render = previewKey ? previewDeclaration[previewKey]?.render : undefined;
155
+ return typeof render === 'function' ? render : Component;
156
+ }`
157
+ : '() => Component';
143
158
 
144
159
  return `
145
160
  import React from 'react';
146
161
  import { hydrateRoot } from 'react-dom/client';
147
162
  import ${importClause} from '${normalizePath(componentPath)}';
163
+ ${declarationImport}
164
+
165
+ const resolveComponent = ${resolveComponent};
148
166
 
149
167
  const parseProps = (propsId) => {
150
168
  const raw = propsId ? document.getElementById(propsId)?.textContent : '{}';
@@ -157,12 +175,14 @@ function generateClientHydrationSource(componentId: string, componentPath: strin
157
175
 
158
176
  const standaloneRoot = document.getElementById('${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}');
159
177
  if (standaloneRoot && !standaloneRoot.hasAttribute('data-handoff-component')) {
160
- hydrateRoot(standaloneRoot, <Component {...parseProps('${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}')} />);
178
+ const StandaloneComponent = resolveComponent(standaloneRoot);
179
+ hydrateRoot(standaloneRoot, <StandaloneComponent {...parseProps('${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}')} />);
161
180
  }
162
181
 
163
182
  const patternMounts = document.querySelectorAll('[data-handoff-component=' + JSON.stringify('${componentId}') + ']');
164
183
  patternMounts.forEach((mount) => {
165
- hydrateRoot(mount, <Component {...parseProps(mount.getAttribute('data-handoff-props'))} />);
184
+ const MountComponent = resolveComponent(mount);
185
+ hydrateRoot(mount, <MountComponent {...parseProps(mount.getAttribute('data-handoff-props'))} />);
166
186
  });
167
187
  `;
168
188
  }
@@ -175,12 +195,13 @@ function generateClientHydrationSource(componentId: string, componentPath: strin
175
195
  * artifact cannot be loaded.
176
196
  * @param componentId - Component identifier
177
197
  * @param previewTitle - Title for the preview
178
- * @param renderedHtml - Server-rendered HTML content
198
+ * @param renderedHtml - Server-rendered HTML, embedded verbatim so the client can hydrate it
179
199
  * @param props - Component props as JSON
180
200
  * @returns Complete HTML document
181
201
  */
182
202
  function generateHtmlDocument(
183
203
  componentId: string,
204
+ previewKey: string,
184
205
  previewTitle: string,
185
206
  renderedHtml: string,
186
207
  props: any,
@@ -220,7 +241,7 @@ function generateHtmlDocument(
220
241
  <title>${previewTitle}</title>
221
242
  </head>
222
243
  <body>
223
- <div id="${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}">${renderedHtml}</div>
244
+ <div id="${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}" data-handoff-preview="${previewKey}">${renderedHtml}</div>
224
245
  </body>
225
246
  </html>`;
226
247
  }
@@ -314,7 +335,15 @@ export function ssrRenderPlugin(
314
335
  // `component/<id>.client.js` artifact. The hydration source only imports the component and
315
336
  // reads props from the in-document `__APP_PROPS__` element, so it is identical across every
316
337
  // preview of this component.
317
- const clientHydrationSource = generateClientHydrationSource(componentId, componentPath, componentData.componentExport);
338
+ const declarationPath = Object.values(componentData.previews ?? {}).some((preview) => typeof preview.render === 'function')
339
+ ? componentData.entries?.declaration
340
+ : undefined;
341
+ const clientHydrationSource = generateClientHydrationSource(
342
+ componentId,
343
+ componentPath,
344
+ componentData.componentExport,
345
+ declarationPath
346
+ );
318
347
  const clientBuildConfig = {
319
348
  ...DEFAULT_CLIENT_BUILD_CONFIG,
320
349
  logLevel: 'silent' as const,
@@ -326,9 +355,10 @@ export function ssrRenderPlugin(
326
355
  plugins: [createReactResolvePlugin(handoff.workingPath, handoff.modulePath)],
327
356
  };
328
357
 
329
- const finalClientBuildConfig = handoff.config?.hooks?.clientBuildConfig
358
+ const hookedClientBuildConfig = handoff.config?.hooks?.clientBuildConfig
330
359
  ? handoff.config.hooks.clientBuildConfig(clientBuildConfig)
331
360
  : clientBuildConfig;
361
+ const finalClientBuildConfig = withStyleLoaders(hookedClientBuildConfig);
332
362
 
333
363
  let clientBundleJs: string;
334
364
  try {
@@ -364,16 +394,23 @@ export function ssrRenderPlugin(
364
394
 
365
395
  // Generate previews for each variation
366
396
  for (const previewKey in componentData.previews) {
367
- const previewProps = componentData.previews[previewKey].values;
368
-
369
- // Server-side render the component
370
- const serverRenderedHtml = ReactDOMServer.renderToString(React.createElement(ReactComponent, previewProps));
371
- const formattedHtml = await formatHtml(serverRenderedHtml);
397
+ const preview = componentData.previews[previewKey];
398
+ const previewProps = preview.values;
399
+ // A synthetic pattern preview is not a named export, so the client looks up its
400
+ // `sourcePreview` instead. A preview without one has no `render` either.
401
+ const hydrationPreviewKey = preview.sourcePreview ?? previewKey;
402
+ // Rendered as a component rather than called, so a preview can use hooks and own state.
403
+ const PreviewComponent = (typeof preview.render === 'function' ? preview.render : ReactComponent) as ReactComponent;
404
+
405
+ // The markup must reach the document exactly as rendered. Formatting it adds text nodes React
406
+ // does not expect, so it discards the server markup and re-renders the preview on the client.
407
+ const serverRenderedHtml = ReactDOMServer.renderToString(React.createElement(PreviewComponent, previewProps));
372
408
 
373
409
  finalHtml = generateHtmlDocument(
374
410
  componentId,
375
- componentData.previews[previewKey].title,
376
- formattedHtml,
411
+ hydrationPreviewKey,
412
+ preview.title,
413
+ serverRenderedHtml,
377
414
  previewProps,
378
415
  sharedArtifacts,
379
416
  componentArtifacts
@@ -402,7 +439,7 @@ export function ssrRenderPlugin(
402
439
  });
403
440
  }
404
441
 
405
- // Format final HTML and update component data
442
+ // The previews are already emitted, so this formats only the docs code view.
406
443
  finalHtml = await formatHtml(finalHtml);
407
444
  componentData.format = 'react';
408
445
  componentData.preview = '';
@@ -5,8 +5,10 @@ import type { SourceFormat } from '../../../catalog/renderers';
5
5
  import {
6
6
  BuildCache,
7
7
  checkOutputExists,
8
+ computeBuildInputStates,
8
9
  computeComponentFileStates,
9
10
  computeGlobalDepsState,
11
+ createBuildInputs,
10
12
  createEmptyCache,
11
13
  hasComponentChanged,
12
14
  haveGlobalDepsChanged,
@@ -162,27 +164,17 @@ const createComponentBuildPlan = (
162
164
  };
163
165
  };
164
166
 
165
- /**
166
- * Options for processing components
167
- */
168
- export interface ProcessComponentsOptions {
169
- /** Enable caching to skip unchanged components */
170
- useCache?: boolean;
171
- }
172
-
173
167
  /**
174
168
  * Process components and generate their code, styles, and previews
175
169
  * @param handoff - The Handoff instance containing configuration and state
176
170
  * @param id - Optional component ID to process a specific component
177
171
  * @param segmentToProcess - Optional segment to update
178
- * @param options - Optional processing options including cache settings
179
172
  * @returns Promise resolving to an array of processed components
180
173
  */
181
174
  export async function processComponents(
182
175
  handoff: Handoff,
183
176
  id?: string,
184
- segmentToProcess?: ComponentSegment,
185
- options?: ProcessComponentsOptions
177
+ segmentToProcess?: ComponentSegment
186
178
  ): Promise<ComponentListObject[]> {
187
179
  const result: ComponentListObject[] = [];
188
180
 
@@ -204,18 +196,17 @@ export async function processComponents(
204
196
  return [];
205
197
  }
206
198
 
207
- // Determine which components need building based on cache (when enabled)
199
+ // Determine which components need building based on cache
208
200
  let componentsToBuild: Set<string>;
209
201
  let cache: BuildCache | null = null;
210
202
  let currentGlobalDeps = {};
211
203
  const componentFileStatesMap: Map<string, Awaited<ReturnType<typeof computeComponentFileStates>>> = new Map();
212
204
 
213
205
  // Only use caching when:
214
- // - useCache option is enabled
215
206
  // - No specific component ID is requested (full build scenario)
216
207
  // - No specific segment is requested (full build scenario)
217
208
  // - Force flag is not set
218
- const shouldUseCache = options?.useCache && !id && !segmentToProcess && !handoff.force;
209
+ const shouldUseCache = !id && !segmentToProcess && !handoff.force;
219
210
 
220
211
  if (shouldUseCache) {
221
212
  Logger.debug('Loading build cache...');
@@ -240,7 +231,7 @@ export async function processComponents(
240
231
  if (!cachedEntry) {
241
232
  Logger.info(`Component '${componentId}': new component, will build`);
242
233
  componentsToBuild.add(componentId);
243
- } else if (hasComponentChanged(cachedEntry, currentFileStates)) {
234
+ } else if (await hasComponentChanged(cachedEntry, currentFileStates)) {
244
235
  Logger.info(`Component '${componentId}': source files changed, will rebuild`);
245
236
  componentsToBuild.add(componentId);
246
237
  } else if (!(await checkOutputExists(handoff, componentId))) {
@@ -372,6 +363,7 @@ export async function processComponents(
372
363
  // Components should always have at least one preview variation.
373
364
  ensureDefaultPreview(data);
374
365
 
366
+ const inputs = shouldUseCache ? createBuildInputs() : undefined;
375
367
  const runsViteBuildSteps = buildPlan.js || buildPlan.css || buildPlan.previews;
376
368
  let viteBuildStartedAtMs = 0;
377
369
  if (runsViteBuildSteps) {
@@ -381,11 +373,11 @@ export async function processComponents(
381
373
 
382
374
  // Build JS if needed (new build, validation missing, or explicit segment request).
383
375
  if (buildPlan.js) {
384
- data = await buildComponentJs(data, handoff);
376
+ data = await buildComponentJs(data, handoff, inputs);
385
377
  }
386
378
  // Build CSS if needed.
387
379
  if (buildPlan.css) {
388
- data = await buildComponentCss(data, handoff);
380
+ data = await buildComponentCss(data, handoff, inputs);
389
381
  }
390
382
  // Build previews (HTML, snapshots, etc) if needed.
391
383
  if (buildPlan.previews) {
@@ -395,7 +387,8 @@ export async function processComponents(
395
387
  previews: getBuildPreviews(data),
396
388
  },
397
389
  handoff,
398
- components
390
+ components,
391
+ inputs
399
392
  );
400
393
  }
401
394
 
@@ -428,17 +421,18 @@ export async function processComponents(
428
421
  result.push(summary);
429
422
 
430
423
  // Update cache entry for this component after successful build
431
- if (shouldUseCache) {
424
+ if (shouldUseCache && inputs) {
432
425
  if (!cache) {
433
426
  cache = createEmptyCache();
434
427
  }
435
- const fileStates = componentFileStatesMap.get(runtimeComponentId);
436
- if (fileStates) {
437
- updateComponentCacheEntry(cache, runtimeComponentId, fileStates);
428
+ // Compute file states if not already computed (e.g., when global deps changed)
429
+ const fileStates = componentFileStatesMap.get(runtimeComponentId) ?? (await computeComponentFileStates(handoff, runtimeComponentId));
430
+ const inputStates = await computeBuildInputStates(handoff, runtimeComponentId, inputs);
431
+ if (inputStates) {
432
+ updateComponentCacheEntry(cache, runtimeComponentId, fileStates, inputStates);
438
433
  } else {
439
- // Compute file states if not already computed (e.g., when global deps changed)
440
- const computedFileStates = await computeComponentFileStates(handoff, runtimeComponentId);
441
- updateComponentCacheEntry(cache, runtimeComponentId, computedFileStates);
434
+ // An incomplete list can miss a changed file, so the next run builds this component again.
435
+ delete cache.components[runtimeComponentId];
442
436
  }
443
437
  }
444
438
  }