handoff-app 2.0.0-9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/AGENTS.md +90 -1
  2. package/{Changelog.md → CHANGELOG.md} +105 -1
  3. package/CLAUDE.md +92 -1
  4. package/{License.md → LICENSE.md} +1 -1
  5. package/README.md +818 -406
  6. package/RECIPES.md +975 -0
  7. package/UPGRADE.md +57 -9
  8. package/components.json +20 -20
  9. package/config/docs/foundations/colors/guidelines.md +0 -1
  10. package/config/docs/foundations/colors.md +0 -1
  11. package/config/docs/foundations/effects/guidelines.md +0 -1
  12. package/config/docs/foundations/effects/resources.md +0 -1
  13. package/config/docs/foundations/effects.md +0 -1
  14. package/config/docs/foundations/fonts.md +0 -1
  15. package/config/docs/foundations/grid.md +0 -1
  16. package/config/docs/foundations/icon.md +0 -1
  17. package/config/docs/foundations/icons/guidelines.md +0 -1
  18. package/config/docs/foundations/icons.md +0 -1
  19. package/config/docs/foundations/images/guidelines.md +0 -1
  20. package/config/docs/foundations/images/resources.md +0 -1
  21. package/config/docs/foundations/images.md +0 -1
  22. package/config/docs/foundations/logo/resources.md +0 -1
  23. package/config/docs/foundations/logo.md +0 -1
  24. package/config/docs/foundations/typography/guidelines.md +0 -1
  25. package/config/docs/foundations/typography/resources.md +0 -1
  26. package/config/docs/foundations/typography.md +0 -1
  27. package/config/docs/foundations.md +0 -1
  28. package/config/docs/guidelines.md +0 -1
  29. package/config/docs/index.md +0 -1
  30. package/config/docs/system/tokens/foundations/colors.md +0 -4
  31. package/config/docs/system/tokens/foundations/effects.md +0 -4
  32. package/config/docs/system/tokens/foundations/typography.md +0 -2
  33. package/config/docs/system.md +0 -2
  34. package/config/templates/page.md +2 -1
  35. package/config/templates/page.mdx +2 -1
  36. package/dist/ai/connections.d.ts +66 -0
  37. package/dist/ai/connections.js +149 -0
  38. package/dist/ai/define.d.ts +10 -0
  39. package/dist/ai/define.js +13 -0
  40. package/dist/ai/types.d.ts +26 -0
  41. package/dist/ai/types.js +10 -0
  42. package/dist/app-builder/build.d.ts +14 -7
  43. package/dist/app-builder/build.js +195 -66
  44. package/dist/app-builder/client-config.js +3 -2
  45. package/dist/app-builder/paths.d.ts +0 -12
  46. package/dist/app-builder/paths.js +3 -20
  47. package/dist/app-builder/static-export.js +1 -1
  48. package/dist/app-builder/vercel-output.d.ts +1 -1
  49. package/dist/app-builder/vercel-output.js +5 -4
  50. package/dist/cache/build-cache.d.ts +27 -4
  51. package/dist/cache/build-cache.js +114 -47
  52. package/dist/cache/build-inputs.d.ts +13 -0
  53. package/dist/cache/build-inputs.js +19 -0
  54. package/dist/cache/index.d.ts +2 -1
  55. package/dist/cache/index.js +5 -1
  56. package/dist/cli/create.js +1 -2
  57. package/dist/cli.js +1 -1
  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 +26 -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 +1 -1
  71. package/dist/index.d.ts +12 -7
  72. package/dist/index.js +18 -22
  73. package/dist/nav/index.d.ts +2 -2
  74. package/dist/nav/registry.d.ts +3 -3
  75. package/dist/nav/registry.js +1 -1
  76. package/dist/nav/workspace.d.ts +3 -3
  77. package/dist/nav/workspace.js +1 -1
  78. package/dist/pipeline/figma.d.ts +13 -2
  79. package/dist/pipeline/figma.js +86 -46
  80. package/dist/pipeline/styles.js +1 -1
  81. package/dist/pipeline/validation.d.ts +1 -1
  82. package/dist/pipeline/validation.js +3 -3
  83. package/dist/providers.d.ts +10 -0
  84. package/dist/providers.js +13 -0
  85. package/dist/registry/asset-storage/define.d.ts +5 -5
  86. package/dist/registry/asset-storage/define.js +4 -4
  87. package/dist/registry/asset-storage/resolve.d.ts +9 -19
  88. package/dist/registry/asset-storage/resolve.js +33 -21
  89. package/dist/registry/asset-storage/types.d.ts +37 -17
  90. package/dist/registry/asset-storage/types.js +5 -5
  91. package/dist/registry/auth/ai-keys.d.ts +32 -0
  92. package/dist/registry/auth/ai-keys.js +69 -0
  93. package/dist/registry/auth/crypto.d.ts +17 -0
  94. package/dist/registry/auth/crypto.js +61 -1
  95. package/dist/registry/auth/index.d.ts +2 -1
  96. package/dist/registry/auth/index.js +11 -2
  97. package/dist/registry/checkout/index.d.ts +2 -1
  98. package/dist/registry/checkout/index.js +57 -127
  99. package/dist/registry/client.js +53 -17
  100. package/dist/registry/db/driver.js +3 -3
  101. package/dist/registry/db/schema.d.ts +261 -3
  102. package/dist/registry/db/schema.js +29 -4
  103. package/dist/registry/email/define.d.ts +10 -0
  104. package/dist/registry/email/define.js +13 -0
  105. package/dist/registry/email/providers/smtp.d.ts +10 -0
  106. package/dist/registry/email/providers/smtp.js +73 -0
  107. package/dist/registry/email/resolve.d.ts +28 -0
  108. package/dist/registry/email/resolve.js +74 -0
  109. package/dist/registry/email/types.d.ts +24 -0
  110. package/dist/registry/email/types.js +6 -0
  111. package/dist/registry/path.d.ts +2 -0
  112. package/dist/registry/path.js +11 -1
  113. package/dist/registry/publish/errors.js +4 -0
  114. package/dist/registry/publish/index.js +2 -0
  115. package/dist/registry/publish/package.d.ts +6 -0
  116. package/dist/registry/publish/package.js +29 -1
  117. package/dist/registry/transfer.d.ts +4 -0
  118. package/dist/store/registry.d.ts +3 -3
  119. package/dist/store/registry.js +4 -4
  120. package/dist/transformers/preview/component/builder.d.ts +1 -9
  121. package/dist/transformers/preview/component/builder.js +17 -17
  122. package/dist/transformers/preview/component/css.d.ts +2 -1
  123. package/dist/transformers/preview/component/css.js +8 -5
  124. package/dist/transformers/preview/component/html.d.ts +3 -1
  125. package/dist/transformers/preview/component/html.js +6 -2
  126. package/dist/transformers/preview/component/javascript.d.ts +2 -1
  127. package/dist/transformers/preview/component/javascript.js +10 -4
  128. package/dist/transformers/utils/build.d.ts +9 -0
  129. package/dist/transformers/utils/build.js +27 -5
  130. package/dist/types/config.d.ts +129 -18
  131. package/dist/utils/path.d.ts +1 -5
  132. package/dist/utils/path.js +6 -23
  133. package/docs/api.md +6 -4
  134. package/docs/cli.md +275 -54
  135. package/drizzle/0007_add_ai_keys.sql +11 -0
  136. package/drizzle/0008_add_source_dir.sql +2 -0
  137. package/drizzle/meta/0007_snapshot.json +2013 -0
  138. package/drizzle/meta/0008_snapshot.json +2025 -0
  139. package/drizzle/meta/_journal.json +14 -0
  140. package/package.json +19 -8
  141. package/src/ai/README.md +42 -0
  142. package/src/ai/connections.ts +189 -0
  143. package/src/ai/define.ts +15 -0
  144. package/src/ai/types.ts +30 -0
  145. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  146. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  147. package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
  148. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  149. package/src/app/components/AiAssistant/dockState.ts +86 -0
  150. package/src/app/components/AiAssistant/toolLabel.ts +24 -0
  151. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  152. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  153. package/src/app/components/AiAssistant/usePageContext.ts +63 -0
  154. package/src/app/components/Auth/AccountLayout.tsx +58 -19
  155. package/src/app/components/Auth/AuthShell.tsx +27 -33
  156. package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
  157. package/src/app/components/Auth/installStatus.ts +29 -0
  158. package/src/app/components/Component/Preview.tsx +5 -2
  159. package/src/app/components/Foundations/ColorGrid.tsx +13 -14
  160. package/src/app/components/Layout/Header.tsx +21 -15
  161. package/src/app/components/Layout/Main.tsx +2 -2
  162. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  163. package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
  164. package/src/app/components/Navigation/MobileNav.tsx +2 -2
  165. package/src/app/components/Navigation/SideNav.tsx +1 -15
  166. package/src/app/components/context/NavProvider.tsx +7 -3
  167. package/src/app/components/ui/attachment.tsx +50 -0
  168. package/src/app/components/ui/bubble.tsx +57 -0
  169. package/src/app/components/ui/button.tsx +3 -1
  170. package/src/app/components/ui/confirm-dialog.tsx +87 -0
  171. package/src/app/components/ui/dialog.tsx +7 -2
  172. package/src/app/components/ui/marker.tsx +56 -0
  173. package/src/app/components/ui/sidebar.tsx +3 -1
  174. package/src/app/components/util/index.ts +3 -12
  175. package/src/app/css/index.css +64 -0
  176. package/src/app/lib/ai/auth.ts +33 -0
  177. package/src/app/lib/ai/context.ts +99 -0
  178. package/src/app/lib/ai/model.ts +79 -0
  179. package/src/app/lib/ai/prompt.ts +36 -0
  180. package/src/app/lib/ai/resolve.ts +97 -0
  181. package/src/app/lib/ai/tools.ts +120 -0
  182. package/src/app/lib/api/methods.ts +13 -0
  183. package/src/app/lib/asset-storage.ts +35 -36
  184. package/src/app/lib/auth/api.ts +4 -12
  185. package/src/app/lib/auth/email.ts +96 -23
  186. package/src/app/lib/docs-api/page-rendering.ts +1 -1
  187. package/src/app/lib/docs-api/registry.ts +2 -3
  188. package/src/app/lib/docs-api/runtime-config.ts +47 -21
  189. package/src/app/lib/mcp/components.ts +523 -0
  190. package/src/app/lib/mcp/page-url.ts +24 -0
  191. package/src/app/lib/mcp/pages.ts +84 -0
  192. package/src/app/lib/mcp/result.ts +33 -0
  193. package/src/app/lib/mcp/server.ts +39 -318
  194. package/src/app/lib/mcp/tokens.ts +334 -0
  195. package/src/app/lib/mcp/tool.ts +19 -0
  196. package/src/app/lib/registry-api/asset-transfer.ts +119 -11
  197. package/src/app/lib/registry-api/errors.ts +2 -0
  198. package/src/app/lib/registry-api/handler.ts +7 -1
  199. package/src/app/lib/registry-api/transfer.ts +28 -6
  200. package/src/app/lib/server-module.ts +34 -0
  201. package/src/app/lib/utils.ts +20 -0
  202. package/src/app/next.config.mjs +26 -24
  203. package/src/app/pages/[...slug]/index.tsx +2 -1
  204. package/src/app/pages/_app.tsx +6 -2
  205. package/src/app/pages/_document.tsx +6 -0
  206. package/src/app/pages/account/ai.tsx +183 -0
  207. package/src/app/pages/account/index.tsx +68 -68
  208. package/src/app/pages/account/tokens.tsx +37 -7
  209. package/src/app/pages/account/users.tsx +75 -15
  210. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  211. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  212. package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
  213. package/src/app/pages/api/admin/users/index.ts +2 -2
  214. package/src/app/pages/api/ai/chat.ts +115 -0
  215. package/src/app/pages/api/ai/connections.ts +23 -0
  216. package/src/app/pages/api/auth/request-reset.ts +14 -16
  217. package/src/app/pages/api/docs/nav.json.ts +1 -1
  218. package/src/app/pages/api/install.ts +3 -3
  219. package/src/app/pages/api/mcp.ts +6 -0
  220. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
  221. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
  222. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  223. package/src/app/pages/cli/device.tsx +10 -5
  224. package/src/app/pages/foundations/colors.tsx +9 -8
  225. package/src/app/pages/foundations/index.tsx +3 -3
  226. package/src/app/pages/index.tsx +2 -2
  227. package/src/app/pages/install.tsx +7 -2
  228. package/src/app/pages/login.tsx +32 -27
  229. package/src/app/pages/reset-password.tsx +21 -20
  230. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  231. package/src/app/pages/system/index.tsx +11 -11
  232. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  233. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  234. package/src/app-builder/README.md +2 -1
  235. package/src/app-builder/build.ts +218 -73
  236. package/src/app-builder/client-config.ts +3 -2
  237. package/src/app-builder/paths.ts +3 -22
  238. package/src/app-builder/static-export.ts +1 -1
  239. package/src/app-builder/vercel-output.ts +5 -4
  240. package/src/cache/README.md +4 -1
  241. package/src/cache/build-cache.ts +128 -43
  242. package/src/cache/build-inputs.ts +28 -0
  243. package/src/cache/index.ts +4 -0
  244. package/src/cli/create.ts +1 -2
  245. package/src/cli.ts +1 -1
  246. package/src/commands/build/index.ts +1 -1
  247. package/src/config/README.md +10 -9
  248. package/src/config/defaults.ts +7 -4
  249. package/src/config/from-env.ts +33 -0
  250. package/src/config/helpers.ts +1 -3
  251. package/src/config/index.ts +0 -3
  252. package/src/config/loader.ts +25 -3
  253. package/src/config/resolve-env.ts +28 -15
  254. package/src/config/runtime.ts +1 -1
  255. package/src/index.ts +33 -29
  256. package/src/nav/index.ts +2 -2
  257. package/src/nav/registry.ts +3 -3
  258. package/src/nav/workspace.ts +3 -3
  259. package/src/pipeline/README.md +1 -1
  260. package/src/pipeline/figma.ts +98 -49
  261. package/src/pipeline/styles.ts +1 -1
  262. package/src/pipeline/validation.ts +3 -3
  263. package/src/providers.ts +18 -0
  264. package/src/registry/asset-storage/define.ts +6 -6
  265. package/src/registry/asset-storage/resolve.ts +38 -37
  266. package/src/registry/asset-storage/types.ts +39 -17
  267. package/src/registry/auth/ai-keys.ts +62 -0
  268. package/src/registry/auth/crypto.ts +63 -1
  269. package/src/registry/auth/index.ts +11 -1
  270. package/src/registry/checkout/index.ts +60 -138
  271. package/src/registry/client.ts +60 -20
  272. package/src/registry/db/driver.ts +2 -2
  273. package/src/registry/db/schema.ts +49 -22
  274. package/src/registry/email/define.ts +15 -0
  275. package/src/registry/email/providers/smtp.ts +39 -0
  276. package/src/registry/email/resolve.ts +94 -0
  277. package/src/registry/email/types.ts +28 -0
  278. package/src/registry/path.ts +11 -0
  279. package/src/registry/publish/errors.ts +4 -0
  280. package/src/registry/publish/index.ts +3 -1
  281. package/src/registry/publish/package.ts +33 -0
  282. package/src/registry/transfer.ts +4 -0
  283. package/src/store/registry.ts +7 -7
  284. package/src/transformers/preview/component/builder.ts +19 -25
  285. package/src/transformers/preview/component/css.ts +9 -4
  286. package/src/transformers/preview/component/html.ts +6 -2
  287. package/src/transformers/preview/component/javascript.ts +14 -4
  288. package/src/transformers/utils/build.ts +22 -4
  289. package/src/types/config.ts +142 -20
  290. package/src/utils/README.md +1 -1
  291. package/src/utils/path.ts +6 -29
  292. package/templates/default/handoff.config.js.tpl +10 -2
  293. package/templates/default/handoff.config.ts.tpl +10 -2
  294. package/templates/default/handoff.config.with-examples.js.tpl +10 -2
  295. package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
  296. package/templates/default/package.json.tpl +2 -2
  297. package/.editorconfig +0 -19
  298. package/.eslintrc.json +0 -4
  299. package/.prettierignore +0 -0
  300. package/.prettierrc.json +0 -8
  301. package/.vscode/settings.json +0 -17
  302. package/dist/config/validator.d.ts +0 -9
  303. package/dist/config/validator.js +0 -24
  304. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
  305. package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
  306. package/src/app/components/old/Header.tsx +0 -89
  307. package/src/app/lib/mcp/shape.ts +0 -596
  308. package/src/app/pages/assets/fonts.tsx +0 -129
  309. package/src/app/pages/assets/index.tsx +0 -91
  310. package/src/app/pages/assets/logos.tsx +0 -75
  311. package/src/config/validator.ts +0 -22
  312. package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
@@ -36,7 +36,7 @@ export interface ResolvedRegistryDatabase {
36
36
 
37
37
  /** Resolve the env-var name holding the database URL (defaults to `DATABASE_URL`). */
38
38
  export const resolveDatabaseUrlEnv = (config: ResolvedConfig | null | undefined): string => {
39
- const configured = config?.runtime?.registry?.databaseUrl?.$env?.trim();
39
+ const configured = config?.runtime?.registry?.database?.url?.$env?.trim();
40
40
  return configured || DEFAULT_DATABASE_URL_ENV;
41
41
  };
42
42
 
@@ -58,7 +58,7 @@ export const resolveRegistryDatabase = (config: ResolvedConfig | null | undefine
58
58
  if (!connectionString) {
59
59
  throw new Error(
60
60
  `Registry database URL is not configured. Set the "${databaseUrlEnv}" environment variable to a ` +
61
- `PostgreSQL connection string (or change "runtime.registry.databaseUrl" to point at the env var you use).`
61
+ `PostgreSQL connection string (or change "runtime.registry.database.url" to point at the env var you use).`
62
62
  );
63
63
  }
64
64
 
@@ -30,7 +30,7 @@ import type { TokenSetKind } from '../tokens/sets';
30
30
 
31
31
  /**
32
32
  * PostgreSQL `bytea` column. Drizzle pg-core has no built-in binary type, so declare it once here.
33
- * Reads/writes are Node `Buffer`s. The default asset storage adapter keeps small/ordinary asset
33
+ * Reads/writes are Node `Buffer`s. The default asset storage provider keeps small/ordinary asset
34
34
  * bytes inline in this column; larger assets are offloaded to an object-storage `storageRef` instead.
35
35
  */
36
36
  const bytea = customType<{ data: Buffer; driverData: Buffer }>({
@@ -39,8 +39,8 @@ const bytea = customType<{ data: Buffer; driverData: Buffer }>({
39
39
  },
40
40
  });
41
41
 
42
- /** Where an asset blob's bytes physically live: the default DB adapter, Vercel Blob, or a custom id. */
43
- export type AssetStorageProvider = 'database' | 'vercel-blob' | (string & {});
42
+ /** Where an asset blob's bytes live: inline in the database (`database`) or in a custom provider (`custom`). */
43
+ export type AssetStorageProvider = 'database' | 'custom';
44
44
  import type { ComponentListObject, PageListObject, PatternComponentEntry, PatternListObject } from '../../transformers/preview/types';
45
45
 
46
46
  /** Registry account role. Administrators may issue write-scoped credentials. */
@@ -112,6 +112,8 @@ export const components = pgTable(
112
112
  sourceFormat: text('source_format'),
113
113
  tags: jsonb('tags').$type<string[]>(),
114
114
  categories: jsonb('categories').$type<string[]>(),
115
+ /** Workspace-relative directory the component was published from. */
116
+ sourceDir: text('source_dir'),
115
117
  /** Full normalized component record. */
116
118
  record: jsonb('record').$type<ComponentListObject>().notNull(),
117
119
  /** Registry-only review/catalog metadata (management-API allowlist; never a render input). */
@@ -165,6 +167,8 @@ export const patterns = pgTable(
165
167
  tags: jsonb('tags').$type<string[]>(),
166
168
  /** Pattern component references. */
167
169
  components: jsonb('components').$type<PatternComponentEntry[]>(),
170
+ /** Workspace-relative directory the pattern was published from. */
171
+ sourceDir: text('source_dir'),
168
172
  /** Full normalized pattern record. */
169
173
  record: jsonb('record').$type<PatternListObject>().notNull(),
170
174
  /** Registry-only review/catalog metadata (management-API allowlist; never a render input). */
@@ -378,23 +382,20 @@ export const tokenArtifacts = pgTable(
378
382
  * collection is its current manifest. Publish replaces those rows atomically, so readers always see a
379
383
  * complete manifest.
380
384
  */
381
- export const assetCollections = pgTable(
382
- 'asset_collections',
383
- {
384
- /** Stable collection id (`icons`|`logos`|`fonts`). Join key across stores. */
385
- collection: text('collection').primaryKey(),
386
- /** Deterministic content hash over the collection's `(path, contentHash)` manifest (skip-unchanged). */
387
- sourceHash: text('source_hash'),
388
- /** Build/publish status for the collection. */
389
- status: text('status').$type<RegistryBuildStatus>(),
390
- builtAt: timestamp('built_at', { withTimezone: true }),
391
- builderVersion: text('builder_version'),
392
- /** Registry-only review/catalog metadata (parity with other entities; management-API only). */
393
- metadata: jsonb('metadata').$type<RegistryReviewMetadata>(),
394
- createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
395
- updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
396
- }
397
- );
385
+ export const assetCollections = pgTable('asset_collections', {
386
+ /** Stable collection id (`icons`|`logos`|`fonts`). Join key across stores. */
387
+ collection: text('collection').primaryKey(),
388
+ /** Deterministic content hash over the collection's `(path, contentHash)` manifest (skip-unchanged). */
389
+ sourceHash: text('source_hash'),
390
+ /** Build/publish status for the collection. */
391
+ status: text('status').$type<RegistryBuildStatus>(),
392
+ builtAt: timestamp('built_at', { withTimezone: true }),
393
+ builderVersion: text('builder_version'),
394
+ /** Registry-only review/catalog metadata (parity with other entities; management-API only). */
395
+ metadata: jsonb('metadata').$type<RegistryReviewMetadata>(),
396
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
397
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
398
+ });
398
399
 
399
400
  /**
400
401
  * Logical asset metadata - the collection manifest. One row per asset (icon/logo SVG, sprite,
@@ -431,13 +432,13 @@ export const assets = pgTable(
431
432
  /**
432
433
  * Content-addressed binary blob store, shared across assets/collections (the `hash` is the blob
433
434
  * identity, so an unchanged file re-references the same row). Exactly one location is active per row:
434
- * inline `content` (bytea, default DB adapter) or an external `storageRef` resolved through the
435
+ * inline `content` (bytea, default `database` provider) or an external `storageRef` resolved through the
435
436
  * recorded `storageProvider`. Both nullable so DB- and object-backed blobs coexist.
436
437
  */
437
438
  export const assetBlobs = pgTable('asset_blobs', {
438
439
  /** SHA-256 of the blob bytes (hex). */
439
440
  hash: text('hash').primaryKey(),
440
- /** Which storage backed this blob when it was written (`database`|`vercel-blob`|custom id). */
441
+ /** Which storage backed this blob when it was written (`database`|custom id). */
441
442
  storageProvider: text('storage_provider').$type<AssetStorageProvider>().notNull(),
442
443
  /** Inline bytes; null when stored externally via `storageRef`. */
443
444
  content: bytea('content'),
@@ -575,6 +576,31 @@ export const registryDeviceAuthorizations = pgTable(
575
576
  ]
576
577
  );
577
578
 
579
+ /**
580
+ * One reader's key for one declared AI connection. Unlike every other credential here the value
581
+ * must be recoverable, because it is sent to the provider on that reader's behalf, so it is
582
+ * encrypted rather than hashed (see `../auth/crypto`). A row whose `connectionId` no longer appears
583
+ * in the config is ignored, not deleted: the connection may come back on the next deploy.
584
+ */
585
+ export const registryAiKeys = pgTable(
586
+ 'registry_ai_keys',
587
+ {
588
+ userId: text('user_id')
589
+ .notNull()
590
+ .references(() => registryUsers.id, { onDelete: 'cascade' }),
591
+ /** Id of the connection the config declares. Never a reader-supplied endpoint. */
592
+ connectionId: text('connection_id').notNull(),
593
+ /** Self-describing AES-256-GCM payload; the deployment secret it is keyed from is never stored. */
594
+ encryptedKey: text('encrypted_key').notNull(),
595
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
596
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
597
+ },
598
+ (table) => [
599
+ primaryKey({ columns: [table.userId, table.connectionId] }),
600
+ check('registry_ai_keys_connection_not_blank', sql`length(btrim(${table.connectionId})) > 0`),
601
+ ]
602
+ );
603
+
578
604
  /**
579
605
  * Fixed-window throttling counters. Callers pass a hash of the identifying value so raw emails and
580
606
  * IP addresses are not retained.
@@ -616,6 +642,7 @@ export const registrySchema = {
616
642
  registryInstallations,
617
643
  registryAuthActionTokens,
618
644
  registryAccessTokens,
645
+ registryAiKeys,
619
646
  registryDeviceAuthorizations,
620
647
  registryAuthRateLimits,
621
648
  };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Typed identity helper for custom email providers (mirrors `defineStorageProvider`): a consumer
3
+ * default-exports its result from a **server-only** module and points `runtime.registry.email.module`
4
+ * at it. It must be its own module (not an inline `defineConfig` function) so the build can trace it
5
+ * into the deployed registry. The default export may be the provider object, or a factory
6
+ * `(context) => EmailProvider` when it needs its options or env.
7
+ */
8
+
9
+ import type { EmailProvider, EmailProviderFactory } from './types';
10
+
11
+ export function defineEmailProvider(provider: EmailProvider): EmailProvider;
12
+ export function defineEmailProvider(factory: EmailProviderFactory): EmailProviderFactory;
13
+ export function defineEmailProvider(input: EmailProvider | EmailProviderFactory): EmailProvider | EmailProviderFactory {
14
+ return input;
15
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Built-in SMTP provider, selected with `runtime.registry.email.provider: "smtp"`.
3
+ *
4
+ * `nodemailer` is loaded lazily so it is only pulled into memory (and traced into the registry
5
+ * bundle) when this provider is selected. Options may come from environment variables, so numbers
6
+ * and booleans can arrive as strings.
7
+ */
8
+
9
+ import type { EmailProvider } from '../types';
10
+
11
+ const toNumber = (value: unknown): number | undefined => {
12
+ const number = typeof value === 'number' ? value : typeof value === 'string' && value.trim() ? Number(value) : NaN;
13
+ return Number.isFinite(number) ? number : undefined;
14
+ };
15
+
16
+ const toBoolean = (value: unknown): boolean | undefined =>
17
+ typeof value === 'boolean' ? value : typeof value === 'string' && value.trim() ? value.trim() === 'true' : undefined;
18
+
19
+ /** Build an SMTP provider from options whose references are already resolved. */
20
+ export const createSmtpProvider = (options: Record<string, unknown>): EmailProvider => {
21
+ const host = typeof options.host === 'string' ? options.host : '';
22
+ const configuredPort = toNumber(options.port);
23
+ const secure = toBoolean(options.secure) ?? (configuredPort === undefined || configuredPort === 465);
24
+ const port = configuredPort ?? (secure ? 465 : 587);
25
+ const user = typeof options.user === 'string' ? options.user : undefined;
26
+ const password = typeof options.password === 'string' ? options.password : undefined;
27
+
28
+ return {
29
+ async send({ from, to, subject, html, text }) {
30
+ if (!host) throw new Error('The SMTP host is not set.');
31
+ const { createTransport } = await import('nodemailer');
32
+ const transport = createTransport({ host, port, secure, auth: user ? { user, pass: password } : undefined });
33
+ const info = await transport.sendMail({ from, to, subject, html, text });
34
+ if (info.rejected?.length) {
35
+ throw new Error(`The SMTP server rejected the recipient: ${info.response}`);
36
+ }
37
+ },
38
+ };
39
+ };
@@ -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
 
@@ -8,6 +8,10 @@ export const describeUploadFailure = (
8
8
  registryUrl: string,
9
9
  rejectedPayload: RejectedPublishPayload
10
10
  ): string => {
11
+ // An oversized body is rejected by the platform or body parser, so the response has no error envelope.
12
+ if (error.status === 413) {
13
+ return `The registry at ${registryUrl} rejected the ${rejectedPayload} as too large (413). Reduce the size of its largest files and publish again.`;
14
+ }
11
15
  switch (error.code) {
12
16
  case 'runtime_mode_conflict':
13
17
  return `The registry at ${registryUrl} is not running in registry mode, so it cannot accept publishes: ${error.message}`;
@@ -21,7 +21,7 @@ import { loginCommandHint, resolveAuthenticatedRegistryConnection, unusedLoginNo
21
21
  import type { EntitySummary, TransferEntityKind } from '../transfer';
22
22
  import { quoteIds, selectIds, splitCatalogIds } from '../selection';
23
23
  import { describePublishError, describeUploadFailure } from './errors';
24
- import { assertRequiredArtifactsPresent, buildPublishPackage, PublishPackageError } from './package';
24
+ import { assertPackageWithinUploadLimit, assertRequiredArtifactsPresent, buildPublishPackage, PublishPackageError } from './package';
25
25
 
26
26
  /** A connected-workspace configuration or precondition failure surfaced to the CLI. */
27
27
  export class PublishError extends Error {
@@ -173,6 +173,7 @@ export const publishEntity = async (handoff: Handoff, kind: TransferEntityKind,
173
173
 
174
174
  const pkg = await buildPublishPackage(handoff, kind, id);
175
175
  assertRequiredArtifactsPresent(pkg);
176
+ assertPackageWithinUploadLimit(kind, id, pkg);
176
177
 
177
178
  const contents = `${pkg.artifacts.length} artifact(s), ${pkg.files.length} source file(s)`;
178
179
  if (!client) {
@@ -274,6 +275,7 @@ const uploadEntities = async (handoff: Handoff, transport: PublishTransport, kin
274
275
  try {
275
276
  const pkg = await buildPublishPackage(handoff, kind, id);
276
277
  assertRequiredArtifactsPresent(pkg);
278
+ assertPackageWithinUploadLimit(kind, id, pkg);
277
279
 
278
280
  const remoteHash = remoteHashById.get(id);
279
281
  const localHash = entityHash(pkg.build);
@@ -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),
@@ -276,6 +285,30 @@ export const assertRequiredArtifactsPresent = (pkg: TransferPackage): void => {
276
285
  }
277
286
  };
278
287
 
288
+ /**
289
+ * The largest publish request body the registry accepts. Vercel rejects a larger function body with
290
+ * `413 FUNCTION_PAYLOAD_TOO_LARGE`, and the transfer route's `bodyParser.sizeLimit` matches it.
291
+ */
292
+ export const MAX_PUBLISH_BODY_BYTES = 4_500_000;
293
+
294
+ const formatMegabytes = (bytes: number): string => `${(bytes / 1_000_000).toFixed(1)} MB`;
295
+
296
+ export const assertPackageWithinUploadLimit = (kind: TransferEntityKind, id: string, pkg: TransferPackage): void => {
297
+ const size = Buffer.byteLength(JSON.stringify(pkg));
298
+ if (size <= MAX_PUBLISH_BODY_BYTES) return;
299
+
300
+ const largest = [...pkg.artifacts, ...pkg.files]
301
+ .map((entry) => ({ path: entry.path, size: Buffer.byteLength(entry.content) }))
302
+ .sort((a, b) => b.size - a.size)
303
+ .slice(0, 3)
304
+ .map((entry) => ` - ${entry.path} (${formatMegabytes(entry.size)})`)
305
+ .join('\n');
306
+ throw new PublishPackageError(
307
+ `The ${kind} "${id}" package is ${formatMegabytes(size)}; the registry accepts at most ` +
308
+ `${formatMegabytes(MAX_PUBLISH_BODY_BYTES)} per publish. Largest entries:\n${largest}`
309
+ );
310
+ };
311
+
279
312
  /** Assemble the publish package for an entity from the generated `public/api` artifacts. */
280
313
  export const buildPublishPackage = (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<TransferPackage> =>
281
314
  kind === 'component'
@@ -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
  }
@@ -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
  }