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,6 @@
1
+ "use strict";
2
+ /**
3
+ * Server-only email provider contract. Handoff renders every message, and a provider only delivers it.
4
+ * SMTP is built in. Anything else is a {@link defineEmailProvider} module.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,4 +1,6 @@
1
1
  export declare const isSafeRelativePath: (value: string) => boolean;
2
2
  export declare const normalizeRelativePath: (value: string) => string;
3
+ /** Unlike {@link isSafeRelativePath}, it can start with `../`. */
4
+ export declare const isWorkspaceRelativePath: (value: unknown) => value is string;
3
5
  export declare const isSafePathSegment: (value: string) => boolean;
4
6
  export declare const resolvePathWithin: (root: string, value: string) => string | null;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.resolvePathWithin = exports.isSafePathSegment = exports.normalizeRelativePath = exports.isSafeRelativePath = void 0;
6
+ exports.resolvePathWithin = exports.isSafePathSegment = exports.isWorkspaceRelativePath = exports.normalizeRelativePath = exports.isSafeRelativePath = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const isSafeRelativePath = (value) => {
9
9
  if (typeof value !== 'string' || value.trim() === '') {
@@ -18,6 +18,16 @@ const isSafeRelativePath = (value) => {
18
18
  exports.isSafeRelativePath = isSafeRelativePath;
19
19
  const normalizeRelativePath = (value) => value.replace(/\\/g, '/');
20
20
  exports.normalizeRelativePath = normalizeRelativePath;
21
+ /** Unlike {@link isSafeRelativePath}, it can start with `../`. */
22
+ const isWorkspaceRelativePath = (value) => typeof value === 'string' &&
23
+ value !== '' &&
24
+ value !== '.' &&
25
+ !value.includes('\0') &&
26
+ !value.includes('\\') &&
27
+ !value.startsWith('/') &&
28
+ !/^[a-zA-Z]:/.test(value) &&
29
+ path_1.default.posix.normalize(value) === value;
30
+ exports.isWorkspaceRelativePath = isWorkspaceRelativePath;
21
31
  const isSafePathSegment = (value) => typeof value === 'string' && value === value.trim() && (0, exports.isSafeRelativePath)(value) && !(0, exports.normalizeRelativePath)(value).includes('/');
22
32
  exports.isSafePathSegment = isSafePathSegment;
23
33
  const resolvePathWithin = (root, value) => {
@@ -97,6 +97,12 @@ const enrichItem = (handoff, kind, id, summary) => __awaiter(void 0, void 0, voi
97
97
  }
98
98
  return item;
99
99
  });
100
+ const findSourceDir = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
101
+ const runtime = kind === 'component' ? yield handoff.store.components.get(id) : yield handoff.store.patterns.get(id);
102
+ const sourceDir = runtime === null || runtime === void 0 ? void 0 : runtime.path;
103
+ const relativeDir = sourceDir ? path_1.default.relative(handoff.workingPath, sourceDir) : '';
104
+ return relativeDir && !path_1.default.isAbsolute(relativeDir) ? relativeDir.split(path_1.default.sep).join('/') : undefined;
105
+ });
100
106
  /** Map an entity's related source files to transfer files, dropping workspace-only declarations. */
101
107
  const collectSourceFiles = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
102
108
  const store = kind === 'component' ? handoff.store.components : kind === 'pattern' ? handoff.store.patterns : handoff.store.pages;
@@ -164,6 +170,7 @@ const buildComponentPackage = (handoff, id) => __awaiter(void 0, void 0, void 0,
164
170
  const artifacts = Array.from(byPath.values());
165
171
  return {
166
172
  item: yield enrichItem(handoff, 'component', id, summary),
173
+ sourceDir: yield findSourceDir(handoff, 'component', id),
167
174
  files: yield collectSourceFiles(handoff, 'component', id),
168
175
  artifacts,
169
176
  build: createArtifactBuild(artifacts),
@@ -206,6 +213,7 @@ const buildPatternPackage = (handoff, id) => __awaiter(void 0, void 0, void 0, f
206
213
  const artifacts = Array.from(byPath.values());
207
214
  return {
208
215
  item: yield enrichItem(handoff, 'pattern', id, summary),
216
+ sourceDir: yield findSourceDir(handoff, 'pattern', id),
209
217
  files: yield collectSourceFiles(handoff, 'pattern', id),
210
218
  artifacts,
211
219
  build: createArtifactBuild(artifacts),
@@ -59,6 +59,8 @@ export interface TransferBuild {
59
59
  export interface TransferPackage {
60
60
  /** Normalized component, pattern, or page record stored as the served record. */
61
61
  item: Record<string, unknown>;
62
+ /** Workspace-relative entity directory, for components and patterns. It can start with `../`. */
63
+ sourceDir?: string;
62
64
  files: TransferFile[];
63
65
  artifacts: TransferArtifact[];
64
66
  build: TransferBuild;
@@ -71,6 +73,8 @@ export interface CheckoutPayload {
71
73
  kind: TransferEntityKind;
72
74
  /** Normalized record used to reconstruct the entity. */
73
75
  item: Record<string, unknown>;
76
+ /** The published {@link TransferPackage.sourceDir}. */
77
+ sourceDir?: string;
74
78
  files: TransferFile[];
75
79
  }
76
80
  /**
@@ -13,7 +13,7 @@
13
13
  * - Reads never materialize source files or run the build pipeline — every record and file is
14
14
  * returned exactly as it was published.
15
15
  */
16
- import type { AssetStorage } from '../registry/asset-storage/types';
16
+ import type { StorageProvider } from '../registry/asset-storage/types';
17
17
  import type { RegistryDatabase } from '../registry/db/client';
18
18
  import { type AssetStorageProvider } from '../registry/db/schema';
19
19
  import type { ComponentListObject, PageListObject, PatternListObject } from '../transformers/preview/types';
@@ -22,11 +22,11 @@ import type { AssetContentResource, AssetMetadata, AssetStore, ComponentStore, H
22
22
  export interface RegistryStoreContext {
23
23
  db: RegistryDatabase;
24
24
  /**
25
- * Resolve the {@link AssetStorage} adapter for a blob's recorded provider (or `null` for the inline
25
+ * Resolve the {@link StorageProvider} for a blob's recorded provider (or `null` for the inline
26
26
  * `database` provider). Injected by the app runtime so `src/store` stays free of app-side imports;
27
27
  * when omitted, only inline DB-backed asset content is resolvable.
28
28
  */
29
- resolveAssetAdapter?: (provider: AssetStorageProvider) => Promise<AssetStorage | null>;
29
+ resolveStorageProvider?: (provider: AssetStorageProvider) => Promise<StorageProvider | null>;
30
30
  }
31
31
  export declare class RegistryComponentStore implements ComponentStore {
32
32
  private readonly context;
@@ -404,14 +404,14 @@ class RegistryAssetStore {
404
404
  return Object.assign(Object.assign({}, base), { body: Buffer.from(row.blobContent) });
405
405
  }
406
406
  // Object-backed - resolve through the provider that stored it.
407
- if (!row.storageRef || !this.context.resolveAssetAdapter) {
407
+ if (!row.storageRef || !this.context.resolveStorageProvider) {
408
408
  return null;
409
409
  }
410
- const adapter = yield this.context.resolveAssetAdapter(row.storageProvider);
411
- if (!adapter) {
410
+ const storage = yield this.context.resolveStorageProvider(row.storageProvider);
411
+ if (!storage) {
412
412
  return null;
413
413
  }
414
- const result = yield adapter.get(row.storageRef);
414
+ const result = yield storage.get(row.storageRef);
415
415
  if (result.kind === 'redirect') {
416
416
  return Object.assign(Object.assign({}, base), { redirectUrl: result.url });
417
417
  }
@@ -121,12 +121,25 @@ function loadComponentSchemaAndModule(componentData, componentPath, handoff) {
121
121
  * @param componentExport - Export holding the implementation; the default export when absent
122
122
  * @returns Client-side hydration source code
123
123
  */
124
- function generateClientHydrationSource(componentId, componentPath, componentExport) {
124
+ function generateClientHydrationSource(componentId, componentPath, componentExport, declarationPath) {
125
125
  const importClause = componentExport && componentExport !== 'default' ? `{ ${componentExport} as Component }` : 'Component';
126
+ // A function cannot travel in the JSON props, so the client re-reads `render` from the module that
127
+ // defines it. Conditional, so an unaffected component does not ship its declaration's imports.
128
+ const declarationImport = declarationPath ? `import * as previewDeclaration from '${(0, vite_1.normalizePath)(declarationPath)}';` : '';
129
+ const resolveComponent = declarationPath
130
+ ? `(mount) => {
131
+ const previewKey = mount.getAttribute('data-handoff-preview');
132
+ const render = previewKey ? previewDeclaration[previewKey]?.render : undefined;
133
+ return typeof render === 'function' ? render : Component;
134
+ }`
135
+ : '() => Component';
126
136
  return `
127
137
  import React from 'react';
128
138
  import { hydrateRoot } from 'react-dom/client';
129
139
  import ${importClause} from '${(0, vite_1.normalizePath)(componentPath)}';
140
+ ${declarationImport}
141
+
142
+ const resolveComponent = ${resolveComponent};
130
143
 
131
144
  const parseProps = (propsId) => {
132
145
  const raw = propsId ? document.getElementById(propsId)?.textContent : '{}';
@@ -139,12 +152,14 @@ function generateClientHydrationSource(componentId, componentPath, componentExpo
139
152
 
140
153
  const standaloneRoot = document.getElementById('${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}');
141
154
  if (standaloneRoot && !standaloneRoot.hasAttribute('data-handoff-component')) {
142
- hydrateRoot(standaloneRoot, <Component {...parseProps('${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}')} />);
155
+ const StandaloneComponent = resolveComponent(standaloneRoot);
156
+ hydrateRoot(standaloneRoot, <StandaloneComponent {...parseProps('${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}')} />);
143
157
  }
144
158
 
145
159
  const patternMounts = document.querySelectorAll('[data-handoff-component=' + JSON.stringify('${componentId}') + ']');
146
160
  patternMounts.forEach((mount) => {
147
- hydrateRoot(mount, <Component {...parseProps(mount.getAttribute('data-handoff-props'))} />);
161
+ const MountComponent = resolveComponent(mount);
162
+ hydrateRoot(mount, <MountComponent {...parseProps(mount.getAttribute('data-handoff-props'))} />);
148
163
  });
149
164
  `;
150
165
  }
@@ -156,11 +171,11 @@ function generateClientHydrationSource(componentId, componentPath, componentExpo
156
171
  * artifact cannot be loaded.
157
172
  * @param componentId - Component identifier
158
173
  * @param previewTitle - Title for the preview
159
- * @param renderedHtml - Server-rendered HTML content
174
+ * @param renderedHtml - Server-rendered HTML, embedded verbatim so the client can hydrate it
160
175
  * @param props - Component props as JSON
161
176
  * @returns Complete HTML document
162
177
  */
163
- function generateHtmlDocument(componentId, previewTitle, renderedHtml, props, sharedArtifacts, componentArtifacts) {
178
+ function generateHtmlDocument(componentId, previewKey, previewTitle, renderedHtml, props, sharedArtifacts, componentArtifacts) {
164
179
  var _a;
165
180
  const basePath = (_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : '';
166
181
  const clientArtifactUrl = (0, url_1.buildArtifactUrl)(clientArtifactPath(componentId), basePath);
@@ -193,7 +208,7 @@ function generateHtmlDocument(componentId, previewTitle, renderedHtml, props, sh
193
208
  <title>${previewTitle}</title>
194
209
  </head>
195
210
  <body>
196
- <div id="${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}">${renderedHtml}</div>
211
+ <div id="${PLUGIN_CONSTANTS.ROOT_ELEMENT_ID}" data-handoff-preview="${previewKey}">${renderedHtml}</div>
197
212
  </body>
198
213
  </html>`;
199
214
  }
@@ -224,7 +239,7 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
224
239
  },
225
240
  generateBundle(_, bundle) {
226
241
  return __awaiter(this, void 0, void 0, function* () {
227
- var _a, _b;
242
+ var _a, _b, _c, _d, _e;
228
243
  // Remove all JS chunks to prevent conflicts
229
244
  for (const [fileName, chunkInfo] of Object.entries(bundle)) {
230
245
  if (chunkInfo.type === 'chunk' && fileName.includes(PLUGIN_CONSTANTS.SCRIPT_ID)) {
@@ -275,15 +290,19 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
275
290
  // `component/<id>.client.js` artifact. The hydration source only imports the component and
276
291
  // reads props from the in-document `__APP_PROPS__` element, so it is identical across every
277
292
  // preview of this component.
278
- const clientHydrationSource = generateClientHydrationSource(componentId, componentPath, componentData.componentExport);
293
+ const declarationPath = Object.values((_a = componentData.previews) !== null && _a !== void 0 ? _a : {}).some((preview) => typeof preview.render === 'function')
294
+ ? (_b = componentData.entries) === null || _b === void 0 ? void 0 : _b.declaration
295
+ : undefined;
296
+ const clientHydrationSource = generateClientHydrationSource(componentId, componentPath, componentData.componentExport, declarationPath);
279
297
  const clientBuildConfig = Object.assign(Object.assign({}, build_1.DEFAULT_CLIENT_BUILD_CONFIG), { logLevel: 'silent', stdin: {
280
298
  contents: clientHydrationSource,
281
299
  resolveDir: process.cwd(),
282
300
  loader: 'tsx',
283
301
  }, plugins: [(0, build_1.createReactResolvePlugin)(handoff.workingPath, handoff.modulePath)] });
284
- const finalClientBuildConfig = ((_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.hooks) === null || _b === void 0 ? void 0 : _b.clientBuildConfig)
302
+ const hookedClientBuildConfig = ((_d = (_c = handoff.config) === null || _c === void 0 ? void 0 : _c.hooks) === null || _d === void 0 ? void 0 : _d.clientBuildConfig)
285
303
  ? handoff.config.hooks.clientBuildConfig(clientBuildConfig)
286
304
  : clientBuildConfig;
305
+ const finalClientBuildConfig = (0, build_1.withStyleLoaders)(hookedClientBuildConfig);
287
306
  let clientBundleJs;
288
307
  try {
289
308
  const bundledClient = yield esbuild_1.default.build(finalClientBuildConfig);
@@ -315,11 +334,17 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
315
334
  const componentArtifacts = (0, shared_artifacts_1.resolveComponentArtifactPresence)(handoff, componentId);
316
335
  // Generate previews for each variation
317
336
  for (const previewKey in componentData.previews) {
318
- const previewProps = componentData.previews[previewKey].values;
319
- // Server-side render the component
320
- const serverRenderedHtml = server_1.default.renderToString(react_1.default.createElement(ReactComponent, previewProps));
321
- const formattedHtml = yield (0, html_1.formatHtml)(serverRenderedHtml);
322
- finalHtml = generateHtmlDocument(componentId, componentData.previews[previewKey].title, formattedHtml, previewProps, sharedArtifacts, componentArtifacts);
337
+ const preview = componentData.previews[previewKey];
338
+ const previewProps = preview.values;
339
+ // A synthetic pattern preview is not a named export, so the client looks up its
340
+ // `sourcePreview` instead. A preview without one has no `render` either.
341
+ const hydrationPreviewKey = (_e = preview.sourcePreview) !== null && _e !== void 0 ? _e : previewKey;
342
+ // Rendered as a component rather than called, so a preview can use hooks and own state.
343
+ const PreviewComponent = (typeof preview.render === 'function' ? preview.render : ReactComponent);
344
+ // The markup must reach the document exactly as rendered. Formatting it adds text nodes React
345
+ // does not expect, so it discards the server markup and re-renders the preview on the client.
346
+ const serverRenderedHtml = server_1.default.renderToString(react_1.default.createElement(PreviewComponent, previewProps));
347
+ finalHtml = generateHtmlDocument(componentId, hydrationPreviewKey, preview.title, serverRenderedHtml, previewProps, sharedArtifacts, componentArtifacts);
323
348
  // Emit preview files
324
349
  this.emitFile({
325
350
  type: 'asset',
@@ -340,7 +365,7 @@ function ssrRenderPlugin(componentData, documentationComponents, handoff) {
340
365
  templateFileName: path_1.default.basename(componentPath),
341
366
  });
342
367
  }
343
- // Format final HTML and update component data
368
+ // The previews are already emitted, so this formats only the docs code view.
344
369
  finalHtml = yield (0, html_1.formatHtml)(finalHtml);
345
370
  componentData.format = 'react';
346
371
  componentData.preview = '';
@@ -9,20 +9,12 @@ export declare enum ComponentSegment {
9
9
  Previews = "previews",
10
10
  Validation = "validation"
11
11
  }
12
- /**
13
- * Options for processing components
14
- */
15
- export interface ProcessComponentsOptions {
16
- /** Enable caching to skip unchanged components */
17
- useCache?: boolean;
18
- }
19
12
  /**
20
13
  * Process components and generate their code, styles, and previews
21
14
  * @param handoff - The Handoff instance containing configuration and state
22
15
  * @param id - Optional component ID to process a specific component
23
16
  * @param segmentToProcess - Optional segment to update
24
- * @param options - Optional processing options including cache settings
25
17
  * @returns Promise resolving to an array of processed components
26
18
  */
27
- export declare function processComponents(handoff: Handoff, id?: string, segmentToProcess?: ComponentSegment, options?: ProcessComponentsOptions): Promise<ComponentListObject[]>;
19
+ export declare function processComponents(handoff: Handoff, id?: string, segmentToProcess?: ComponentSegment): Promise<ComponentListObject[]>;
28
20
  export default processComponents;
@@ -153,12 +153,11 @@ const createComponentBuildPlan = (segmentToProcess, existingData) => {
153
153
  * @param handoff - The Handoff instance containing configuration and state
154
154
  * @param id - Optional component ID to process a specific component
155
155
  * @param segmentToProcess - Optional segment to update
156
- * @param options - Optional processing options including cache settings
157
156
  * @returns Promise resolving to an array of processed components
158
157
  */
159
- function processComponents(handoff, id, segmentToProcess, options) {
158
+ function processComponents(handoff, id, segmentToProcess) {
160
159
  return __awaiter(this, void 0, void 0, function* () {
161
- var _a, _b, _c, _d;
160
+ var _a, _b, _c, _d, _e;
162
161
  const result = [];
163
162
  const documentationObject = yield handoff.getDocumentationObject();
164
163
  const components = (_a = documentationObject === null || documentationObject === void 0 ? void 0 : documentationObject.components) !== null && _a !== void 0 ? _a : {};
@@ -177,17 +176,16 @@ function processComponents(handoff, id, segmentToProcess, options) {
177
176
  yield (0, artifacts_1.syncComponentArtifacts)(handoff, [id]);
178
177
  return [];
179
178
  }
180
- // Determine which components need building based on cache (when enabled)
179
+ // Determine which components need building based on cache
181
180
  let componentsToBuild;
182
181
  let cache = null;
183
182
  let currentGlobalDeps = {};
184
183
  const componentFileStatesMap = new Map();
185
184
  // Only use caching when:
186
- // - useCache option is enabled
187
185
  // - No specific component ID is requested (full build scenario)
188
186
  // - No specific segment is requested (full build scenario)
189
187
  // - Force flag is not set
190
- const shouldUseCache = (options === null || options === void 0 ? void 0 : options.useCache) && !id && !segmentToProcess && !handoff.force;
188
+ const shouldUseCache = !id && !segmentToProcess && !handoff.force;
191
189
  if (shouldUseCache) {
192
190
  logger_1.Logger.debug('Loading build cache...');
193
191
  cache = yield (0, cache_1.loadBuildCache)(handoff);
@@ -209,7 +207,7 @@ function processComponents(handoff, id, segmentToProcess, options) {
209
207
  logger_1.Logger.info(`Component '${componentId}': new component, will build`);
210
208
  componentsToBuild.add(componentId);
211
209
  }
212
- else if ((0, cache_1.hasComponentChanged)(cachedEntry, currentFileStates)) {
210
+ else if (yield (0, cache_1.hasComponentChanged)(cachedEntry, currentFileStates)) {
213
211
  logger_1.Logger.info(`Component '${componentId}': source files changed, will rebuild`);
214
212
  componentsToBuild.add(componentId);
215
213
  }
@@ -329,6 +327,7 @@ function processComponents(handoff, id, segmentToProcess, options) {
329
327
  }
330
328
  // Components should always have at least one preview variation.
331
329
  ensureDefaultPreview(data);
330
+ const inputs = shouldUseCache ? (0, cache_1.createBuildInputs)() : undefined;
332
331
  const runsViteBuildSteps = buildPlan.js || buildPlan.css || buildPlan.previews;
333
332
  let viteBuildStartedAtMs = 0;
334
333
  if (runsViteBuildSteps) {
@@ -337,15 +336,15 @@ function processComponents(handoff, id, segmentToProcess, options) {
337
336
  }
338
337
  // Build JS if needed (new build, validation missing, or explicit segment request).
339
338
  if (buildPlan.js) {
340
- data = yield (0, javascript_1.default)(data, handoff);
339
+ data = yield (0, javascript_1.default)(data, handoff, inputs);
341
340
  }
342
341
  // Build CSS if needed.
343
342
  if (buildPlan.css) {
344
- data = yield (0, css_1.default)(data, handoff);
343
+ data = yield (0, css_1.default)(data, handoff, inputs);
345
344
  }
346
345
  // Build previews (HTML, snapshots, etc) if needed.
347
346
  if (buildPlan.previews) {
348
- data = yield (0, html_1.default)(Object.assign(Object.assign({}, data), { previews: getBuildPreviews(data) }), handoff, components);
347
+ data = yield (0, html_1.default)(Object.assign(Object.assign({}, data), { previews: getBuildPreviews(data) }), handoff, components, inputs);
349
348
  }
350
349
  if (runsViteBuildSteps) {
351
350
  const elapsedMs = Date.now() - viteBuildStartedAtMs;
@@ -371,18 +370,19 @@ function processComponents(handoff, id, segmentToProcess, options) {
371
370
  // Add to the cumulative results, to later update the global components summary file.
372
371
  result.push(summary);
373
372
  // Update cache entry for this component after successful build
374
- if (shouldUseCache) {
373
+ if (shouldUseCache && inputs) {
375
374
  if (!cache) {
376
375
  cache = (0, cache_1.createEmptyCache)();
377
376
  }
378
- const fileStates = componentFileStatesMap.get(runtimeComponentId);
379
- if (fileStates) {
380
- (0, cache_1.updateComponentCacheEntry)(cache, runtimeComponentId, fileStates);
377
+ // Compute file states if not already computed (e.g., when global deps changed)
378
+ const fileStates = (_e = componentFileStatesMap.get(runtimeComponentId)) !== null && _e !== void 0 ? _e : (yield (0, cache_1.computeComponentFileStates)(handoff, runtimeComponentId));
379
+ const inputStates = yield (0, cache_1.computeBuildInputStates)(handoff, runtimeComponentId, inputs);
380
+ if (inputStates) {
381
+ (0, cache_1.updateComponentCacheEntry)(cache, runtimeComponentId, fileStates, inputStates);
381
382
  }
382
383
  else {
383
- // Compute file states if not already computed (e.g., when global deps changed)
384
- const computedFileStates = yield (0, cache_1.computeComponentFileStates)(handoff, runtimeComponentId);
385
- (0, cache_1.updateComponentCacheEntry)(cache, runtimeComponentId, computedFileStates);
384
+ // An incomplete list can miss a changed file, so the next run builds this component again.
385
+ delete cache.components[runtimeComponentId];
386
386
  }
387
387
  }
388
388
  }
@@ -1,8 +1,9 @@
1
+ import { BuildInputs } from '../../../cache/build-inputs';
1
2
  import Handoff from '../../../index';
2
3
  import { TransformComponentTokensResult } from '../types';
3
4
  export declare const MAIN_COMPONENT_CSS_FILE = "main.css";
4
5
  export declare const SHARED_COMPONENT_CSS_FILE = "shared.css";
5
- declare const buildComponentCss: (data: TransformComponentTokensResult, handoff: Handoff) => Promise<{
6
+ declare const buildComponentCss: (data: TransformComponentTokensResult, handoff: Handoff, inputs?: BuildInputs) => Promise<{
6
7
  id: string;
7
8
  type?: import("../types").ComponentType;
8
9
  image?: string;
@@ -42,6 +43,7 @@ declare const buildComponentCss: (data: TransformComponentTokensResult, handoff:
42
43
  story?: string;
43
44
  schema?: string;
44
45
  templates?: string;
46
+ declaration?: string;
45
47
  };
46
48
  renderer?: import("../../../index").RendererKind;
47
49
  sourceFormat?: import("../../../index").SourceFormat;
@@ -16,8 +16,10 @@ exports.buildMainCss = exports.SHARED_COMPONENT_CSS_FILE = exports.MAIN_COMPONEN
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const vite_1 = require("vite");
19
+ const build_inputs_1 = require("../../../cache/build-inputs");
19
20
  const duration_1 = require("../../../utils/duration");
20
21
  const logger_1 = require("../../../utils/logger");
22
+ const build_1 = require("../../utils/build");
21
23
  const vite_config_1 = __importDefault(require("../../vite-config"));
22
24
  const component_1 = require("../component");
23
25
  const { pathToFileURL } = require('url');
@@ -32,8 +34,9 @@ exports.SHARED_COMPONENT_CSS_FILE = 'shared.css';
32
34
  * @param options.outputFilename - The name of the output file
33
35
  * @param options.loadPaths - Array of paths for SASS to look for imports
34
36
  * @param options.handoff - The Handoff configuration object
37
+ * @param options.inputs - Collects the files that the build reads, for the build cache
35
38
  */
36
- const buildCssBundle = (_a) => __awaiter(void 0, [_a], void 0, function* ({ entry, outputPath, outputFilename, loadPaths, handoff, }) {
39
+ const buildCssBundle = (_a) => __awaiter(void 0, [_a], void 0, function* ({ entry, outputPath, outputFilename, loadPaths, handoff, inputs, }) {
37
40
  var _b, _c;
38
41
  // Store the current NODE_ENV value
39
42
  const oldNodeEnv = process.env.NODE_ENV;
@@ -72,7 +75,7 @@ const buildCssBundle = (_a) => __awaiter(void 0, [_a], void 0, function* ({ entr
72
75
  if ((_c = (_b = handoff === null || handoff === void 0 ? void 0 : handoff.config) === null || _b === void 0 ? void 0 : _b.hooks) === null || _c === void 0 ? void 0 : _c.cssBuildConfig) {
73
76
  viteConfig = handoff.config.hooks.cssBuildConfig(viteConfig);
74
77
  }
75
- yield (0, vite_1.build)(viteConfig);
78
+ yield (0, vite_1.build)((0, build_inputs_1.withBuildInputs)(viteConfig, inputs));
76
79
  }
77
80
  catch (e) {
78
81
  logger_1.Logger.error(`Failed to build CSS for "${entry}"`);
@@ -88,14 +91,49 @@ const buildCssBundle = (_a) => __awaiter(void 0, [_a], void 0, function* ({ entr
88
91
  }
89
92
  }
90
93
  });
91
- const buildComponentCss = (data, handoff) => __awaiter(void 0, void 0, void 0, function* () {
94
+ const STYLE_SOURCE_EXTENSIONS = new Set(['.scss', '.sass', '.css']);
95
+ /**
96
+ * The stylesheets of one item, in cascade order and deduplicated by resolved path.
97
+ *
98
+ * Discovered imports come first and the declared entry last, so a declared entry can override what
99
+ * the component imports. A project that both declares and imports one file must build it once.
100
+ */
101
+ const resolveStyleSources = (data, handoff) => __awaiter(void 0, void 0, void 0, function* () {
102
+ var _a, _b;
103
+ const template = (_a = data.entries) === null || _a === void 0 ? void 0 : _a.template;
104
+ const discovered = template ? yield (0, build_1.collectStyleImports)(path_1.default.resolve(template), handoff) : [];
105
+ const declared = ((_b = data.entries) === null || _b === void 0 ? void 0 : _b.scss) ? [path_1.default.resolve(data.entries.scss)] : [];
106
+ const ordered = [];
107
+ const seen = new Set();
108
+ for (const source of [...discovered, ...declared]) {
109
+ if (!seen.has(source) && STYLE_SOURCE_EXTENSIONS.has(path_1.default.extname(source)) && fs_extra_1.default.existsSync(source)) {
110
+ seen.add(source);
111
+ ordered.push(source);
112
+ }
113
+ }
114
+ return ordered;
115
+ });
116
+ /**
117
+ * A single Sass entry that `@use`s every source in order.
118
+ *
119
+ * `@use` emits a repeated file once and inlines a plain `.css` source, which `@import` would leave
120
+ * as an unresolvable URL. Namespaces are explicit because two sources sharing a basename would
121
+ * claim the same default namespace and fail the compile.
122
+ */
123
+ const writeCombinedStyleEntry = (sources, outputPath, id) => __awaiter(void 0, void 0, void 0, function* () {
124
+ const entryPath = path_1.default.resolve(outputPath, `.${id}.entry.scss`);
125
+ const rules = sources.map((source, index) => `@use '${source.split(path_1.default.sep).join('/')}' as ns${index};`);
126
+ yield fs_extra_1.default.outputFile(entryPath, `${rules.join('\n')}\n`);
127
+ return entryPath;
128
+ });
129
+ const buildComponentCss = (data, handoff, inputs) => __awaiter(void 0, void 0, void 0, function* () {
92
130
  var _a, _b, _c;
93
131
  const id = data.id;
94
132
  logger_1.Logger.debug(`buildComponentCss`, id);
95
133
  const outputPath = (0, component_1.getComponentOutputPath)(handoff);
96
134
  const builtCssPath = path_1.default.resolve(outputPath, `${id}.css`);
97
- const entry = (_a = data.entries) === null || _a === void 0 ? void 0 : _a.scss;
98
- if (!entry) {
135
+ const sources = yield resolveStyleSources(data, handoff);
136
+ if (sources.length === 0) {
99
137
  // Keep generated output aligned with the current component declaration.
100
138
  yield fs_extra_1.default.remove(builtCssPath);
101
139
  delete data['css'];
@@ -103,59 +141,62 @@ const buildComponentCss = (data, handoff) => __awaiter(void 0, void 0, void 0, f
103
141
  delete data['sharedStyles'];
104
142
  return data;
105
143
  }
106
- const extension = path_1.default.extname(entry);
107
- if (!extension) {
108
- return data;
109
- }
144
+ // A single source compiles directly, so its relative asset URLs and documented Sass stay as authored.
145
+ const combinedEntry = sources.length > 1 ? yield writeCombinedStyleEntry(sources, outputPath, id) : undefined;
146
+ const entry = combinedEntry !== null && combinedEntry !== void 0 ? combinedEntry : sources[0];
110
147
  try {
111
- if (extension === '.scss' || extension === '.css') {
112
- // Remove the previous artifact before rebuilding so "no emitted CSS"
113
- // doesn't silently preserve stale output from an earlier build.
114
- yield fs_extra_1.default.remove(builtCssPath);
115
- // Read the original source
116
- const sourceContent = yield fs_extra_1.default.readFile(entry, 'utf8');
117
- if (extension === '.scss') {
118
- data['sass'] = sourceContent;
119
- }
120
- // Setup SASS load paths
121
- const loadPaths = [
122
- path_1.default.resolve(handoff.workingPath),
123
- path_1.default.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId()),
124
- path_1.default.resolve(handoff.workingPath, 'node_modules'),
125
- ];
126
- if ((_c = (_b = handoff.runtimeConfig) === null || _b === void 0 ? void 0 : _b.entries) === null || _c === void 0 ? void 0 : _c.scss) {
127
- loadPaths.unshift(path_1.default.dirname(handoff.runtimeConfig.entries.scss));
128
- }
129
- yield buildCssBundle({
130
- entry,
131
- outputPath,
132
- outputFilename: `${id}.css`,
133
- loadPaths,
134
- handoff,
135
- });
136
- // Read the built CSS
137
- if (fs_extra_1.default.existsSync(builtCssPath)) {
138
- const builtCss = yield fs_extra_1.default.readFile(builtCssPath, 'utf8');
139
- data['css'] = builtCss;
140
- // Handle shared styles — if the component SCSS uses the split marker,
141
- // extract shared styles from the portion above it
142
- const splitCSS = builtCss.split('/* COMPONENT STYLES*/');
143
- if (splitCSS && splitCSS.length > 1) {
144
- data['css'] = splitCSS[1];
145
- data['sharedStyles'] = splitCSS[0];
146
- yield fs_extra_1.default.writeFile(path_1.default.resolve(outputPath, exports.SHARED_COMPONENT_CSS_FILE), data['sharedStyles']);
147
- }
148
- }
149
- else {
150
- delete data['css'];
151
- delete data['sharedStyles'];
148
+ // Remove the previous artifact before rebuilding so "no emitted CSS"
149
+ // doesn't silently preserve stale output from an earlier build.
150
+ yield fs_extra_1.default.remove(builtCssPath);
151
+ // Read the original source
152
+ const primarySource = ((_a = data.entries) === null || _a === void 0 ? void 0 : _a.scss) ? path_1.default.resolve(data.entries.scss) : sources[0];
153
+ if (path_1.default.extname(primarySource) === '.scss') {
154
+ data['sass'] = yield fs_extra_1.default.readFile(primarySource, 'utf8');
155
+ }
156
+ // Setup SASS load paths
157
+ const loadPaths = [
158
+ path_1.default.resolve(handoff.workingPath),
159
+ handoff.getOutputPath(),
160
+ path_1.default.resolve(handoff.workingPath, 'node_modules'),
161
+ ];
162
+ if ((_c = (_b = handoff.runtimeConfig) === null || _b === void 0 ? void 0 : _b.entries) === null || _c === void 0 ? void 0 : _c.scss) {
163
+ loadPaths.unshift(path_1.default.dirname(handoff.runtimeConfig.entries.scss));
164
+ }
165
+ yield buildCssBundle({
166
+ entry,
167
+ outputPath,
168
+ outputFilename: `${id}.css`,
169
+ loadPaths,
170
+ handoff,
171
+ inputs,
172
+ });
173
+ // Read the built CSS
174
+ if (fs_extra_1.default.existsSync(builtCssPath)) {
175
+ const builtCss = yield fs_extra_1.default.readFile(builtCssPath, 'utf8');
176
+ data['css'] = builtCss;
177
+ // Handle shared styles — if the component SCSS uses the split marker,
178
+ // extract shared styles from the portion above it
179
+ const splitCSS = builtCss.split('/* COMPONENT STYLES*/');
180
+ if (splitCSS && splitCSS.length > 1) {
181
+ data['css'] = splitCSS[1];
182
+ data['sharedStyles'] = splitCSS[0];
183
+ yield fs_extra_1.default.writeFile(path_1.default.resolve(outputPath, exports.SHARED_COMPONENT_CSS_FILE), data['sharedStyles']);
152
184
  }
153
185
  }
186
+ else {
187
+ delete data['css'];
188
+ delete data['sharedStyles'];
189
+ }
154
190
  }
155
191
  catch (e) {
156
192
  logger_1.Logger.error(`Failed to build CSS for "${id}"`);
157
193
  throw e;
158
194
  }
195
+ finally {
196
+ if (combinedEntry) {
197
+ yield fs_extra_1.default.remove(combinedEntry);
198
+ }
199
+ }
159
200
  return data;
160
201
  });
161
202
  /**
@@ -179,7 +220,7 @@ const buildMainCss = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
179
220
  const loadPaths = [
180
221
  path_1.default.dirname(scssEntry),
181
222
  path_1.default.resolve(handoff.workingPath),
182
- path_1.default.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId()),
223
+ handoff.getOutputPath(),
183
224
  path_1.default.resolve(handoff.workingPath, 'node_modules'),
184
225
  ];
185
226
  yield buildCssBundle({
@@ -1,4 +1,5 @@
1
1
  import { Types as CoreTypes } from 'handoff-core';
2
+ import { BuildInputs } from '../../../cache/build-inputs';
2
3
  import Handoff from '../../../index';
3
4
  import { TransformComponentTokensResult } from '../types';
4
5
  /**
@@ -7,6 +8,7 @@ import { TransformComponentTokensResult } from '../types';
7
8
  * @param data - The result of transforming component tokens.
8
9
  * @param handoff - The Handoff configuration object.
9
10
  * @param components - Optional file components object.
11
+ * @param inputs - Collects the files that the build reads, for the build cache.
10
12
  * @returns A promise that resolves to the transformed component tokens result.
11
13
  * @throws Will throw an error if the Vite build process fails.
12
14
  *
@@ -15,5 +17,5 @@ import { TransformComponentTokensResult } from '../types';
15
17
  * const result = await buildPreviews(transformedData, handoffConfig, fileComponents);
16
18
  * ```
17
19
  */
18
- export declare const buildPreviews: (data: TransformComponentTokensResult, handoff: Handoff, components?: CoreTypes.IDocumentationObject["components"]) => Promise<TransformComponentTokensResult>;
20
+ export declare const buildPreviews: (data: TransformComponentTokensResult, handoff: Handoff, components?: CoreTypes.IDocumentationObject["components"], inputs?: BuildInputs) => Promise<TransformComponentTokensResult>;
19
21
  export default buildPreviews;