handoff-app 1.2.2-7 → 2.0.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/Changelog.md +37 -15
  2. package/README.md +372 -118
  3. package/dist/app-builder/build.d.ts +41 -2
  4. package/dist/app-builder/build.js +455 -18
  5. package/dist/app-builder/client-config.d.ts +15 -2
  6. package/dist/app-builder/client-config.js +43 -4
  7. package/dist/app-builder/config-diff/snapshot.d.ts +1 -6
  8. package/dist/app-builder/config-diff/snapshot.js +2 -9
  9. package/dist/app-builder/index.d.ts +1 -1
  10. package/dist/app-builder/index.js +3 -1
  11. package/dist/app-builder/nav-shell.d.ts +23 -0
  12. package/dist/app-builder/nav-shell.js +80 -0
  13. package/dist/app-builder/static-export.d.ts +17 -0
  14. package/dist/app-builder/static-export.js +181 -0
  15. package/dist/app-builder/vercel-output.d.ts +39 -0
  16. package/dist/app-builder/vercel-output.js +259 -0
  17. package/dist/app-builder/watchers/utils.d.ts +2 -0
  18. package/dist/app-builder/watchers.d.ts +14 -4
  19. package/dist/app-builder/watchers.js +123 -28
  20. package/dist/app-builder/websocket.js +1 -1
  21. package/dist/artifacts/index.d.ts +2 -0
  22. package/dist/artifacts/index.js +13 -0
  23. package/dist/artifacts/types.d.ts +136 -0
  24. package/dist/artifacts/types.js +24 -0
  25. package/dist/artifacts/url.d.ts +49 -0
  26. package/dist/artifacts/url.js +86 -0
  27. package/dist/cache/build-cache.js +48 -4
  28. package/dist/cli.js +0 -0
  29. package/dist/commands/build/app.js +1 -2
  30. package/dist/commands/build/index.d.ts +17 -0
  31. package/dist/commands/build/index.js +66 -0
  32. package/dist/commands/checkout/index.d.ts +24 -0
  33. package/dist/commands/checkout/index.js +71 -0
  34. package/dist/commands/db/migrate.d.ts +11 -0
  35. package/dist/commands/db/migrate.js +33 -0
  36. package/dist/commands/index.js +8 -0
  37. package/dist/commands/publish/index.d.ts +21 -0
  38. package/dist/commands/publish/index.js +68 -0
  39. package/dist/config/defaults.js +15 -1
  40. package/dist/config/loader.js +6 -1
  41. package/dist/config/normalizers/declaration.js +4 -1
  42. package/dist/config/normalizers/page.d.ts +18 -0
  43. package/dist/config/normalizers/page.js +34 -0
  44. package/dist/config/normalizers/pattern.js +7 -1
  45. package/dist/config/runtime.d.ts +4 -0
  46. package/dist/config/runtime.js +53 -5
  47. package/dist/index.d.ts +65 -1
  48. package/dist/index.js +123 -3
  49. package/dist/nav/index.d.ts +83 -0
  50. package/dist/nav/index.js +9 -0
  51. package/dist/nav/registry.d.ts +15 -0
  52. package/dist/nav/registry.js +42 -0
  53. package/dist/nav/resolver.d.ts +8 -0
  54. package/dist/nav/resolver.js +192 -0
  55. package/dist/nav/workspace.d.ts +18 -0
  56. package/dist/nav/workspace.js +103 -0
  57. package/dist/pipeline/styles.js +7 -35
  58. package/dist/pipeline/token-transformers.d.ts +45 -0
  59. package/dist/pipeline/token-transformers.js +71 -0
  60. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +17 -0
  61. package/dist/registry/asset-storage/adapters/vercel-blob.js +87 -0
  62. package/dist/registry/asset-storage/define.d.ts +10 -0
  63. package/dist/registry/asset-storage/define.js +13 -0
  64. package/dist/registry/asset-storage/resolve.d.ts +45 -0
  65. package/dist/registry/asset-storage/resolve.js +36 -0
  66. package/dist/registry/asset-storage/types.d.ts +51 -0
  67. package/dist/registry/asset-storage/types.js +9 -0
  68. package/dist/registry/assets/layout.d.ts +22 -0
  69. package/dist/registry/assets/layout.js +30 -0
  70. package/dist/registry/assets/sets.d.ts +18 -0
  71. package/dist/registry/assets/sets.js +44 -0
  72. package/dist/registry/assets/transfer.d.ts +45 -0
  73. package/dist/registry/assets/transfer.js +11 -0
  74. package/dist/registry/checkout/assets.d.ts +16 -0
  75. package/dist/registry/checkout/assets.js +150 -0
  76. package/dist/registry/checkout/index.d.ts +33 -0
  77. package/dist/registry/checkout/index.js +610 -0
  78. package/dist/registry/checkout/tokens.d.ts +15 -0
  79. package/dist/registry/checkout/tokens.js +171 -0
  80. package/dist/registry/client.d.ts +61 -0
  81. package/dist/registry/client.js +211 -0
  82. package/dist/registry/connection.d.ts +30 -0
  83. package/dist/registry/connection.js +36 -0
  84. package/dist/registry/db/adapter.d.ts +43 -0
  85. package/dist/registry/db/adapter.js +59 -0
  86. package/dist/registry/db/client.d.ts +35 -0
  87. package/dist/registry/db/client.js +94 -0
  88. package/dist/registry/db/index.d.ts +5 -0
  89. package/dist/registry/db/index.js +24 -0
  90. package/dist/registry/db/migrate.d.ts +23 -0
  91. package/dist/registry/db/migrate.js +64 -0
  92. package/dist/registry/db/schema.d.ts +4869 -0
  93. package/dist/registry/db/schema.js +347 -0
  94. package/dist/registry/path.d.ts +4 -0
  95. package/dist/registry/path.js +31 -0
  96. package/dist/registry/publish/artifacts.d.ts +16 -0
  97. package/dist/registry/publish/artifacts.js +132 -0
  98. package/dist/registry/publish/assets.d.ts +16 -0
  99. package/dist/registry/publish/assets.js +129 -0
  100. package/dist/registry/publish/errors.d.ts +5 -0
  101. package/dist/registry/publish/errors.js +26 -0
  102. package/dist/registry/publish/index.d.ts +27 -0
  103. package/dist/registry/publish/index.js +126 -0
  104. package/dist/registry/publish/package.d.ts +27 -0
  105. package/dist/registry/publish/package.js +257 -0
  106. package/dist/registry/publish/publish-build.d.ts +12 -0
  107. package/dist/registry/publish/publish-build.js +43 -0
  108. package/dist/registry/publish/tokens.d.ts +18 -0
  109. package/dist/registry/publish/tokens.js +112 -0
  110. package/dist/registry/tokens/sets.d.ts +59 -0
  111. package/dist/registry/tokens/sets.js +109 -0
  112. package/dist/registry/tokens/transfer.d.ts +48 -0
  113. package/dist/registry/tokens/transfer.js +10 -0
  114. package/dist/registry/transfer.d.ts +75 -0
  115. package/dist/registry/transfer.js +11 -0
  116. package/dist/store/filesystem-assets.d.ts +17 -0
  117. package/dist/store/filesystem-assets.js +147 -0
  118. package/dist/store/filesystem.d.ts +76 -0
  119. package/dist/store/filesystem.js +208 -0
  120. package/dist/store/index.d.ts +20 -0
  121. package/dist/store/index.js +38 -0
  122. package/dist/store/registry.d.ts +77 -0
  123. package/dist/store/registry.js +350 -0
  124. package/dist/store/source-files.d.ts +24 -0
  125. package/dist/store/source-files.js +147 -0
  126. package/dist/store/types.d.ts +185 -0
  127. package/dist/store/types.js +12 -0
  128. package/dist/transformers/plugins/csf-render.js +17 -8
  129. package/dist/transformers/plugins/handlebars-previews.js +10 -3
  130. package/dist/transformers/plugins/ssr-render.js +116 -49
  131. package/dist/transformers/preview/component/api.js +3 -1
  132. package/dist/transformers/preview/component/artifacts.js +8 -1
  133. package/dist/transformers/preview/component/builder.js +13 -5
  134. package/dist/transformers/preview/component/css.js +9 -1
  135. package/dist/transformers/preview/component/javascript.d.ts +9 -3
  136. package/dist/transformers/preview/component/javascript.js +45 -17
  137. package/dist/transformers/preview/component/shared-artifacts.d.ts +83 -0
  138. package/dist/transformers/preview/component/shared-artifacts.js +92 -0
  139. package/dist/transformers/preview/component.js +5 -2
  140. package/dist/transformers/preview/pattern/builder.js +28 -9
  141. package/dist/transformers/preview/pattern/html.d.ts +13 -10
  142. package/dist/transformers/preview/pattern/html.js +62 -27
  143. package/dist/transformers/preview/types.d.ts +38 -0
  144. package/dist/transformers/utils/handlebars.d.ts +3 -0
  145. package/dist/transformers/utils/handlebars.js +19 -5
  146. package/dist/types/config.d.ts +118 -25
  147. package/dist/utils/menu-shell.d.ts +60 -0
  148. package/dist/utils/menu-shell.js +221 -0
  149. package/dist/utils/stable-stringify.d.ts +2 -0
  150. package/dist/utils/stable-stringify.js +8 -0
  151. package/docs/cli.md +4 -2
  152. package/drizzle/0000_init_registry_storage.sql +95 -0
  153. package/drizzle/0001_add_review_metadata.sql +2 -0
  154. package/drizzle/0002_add_pages.sql +28 -0
  155. package/drizzle/0003_add_tokens.sql +29 -0
  156. package/drizzle/0004_add_assets.sql +37 -0
  157. package/drizzle/meta/0000_snapshot.json +642 -0
  158. package/drizzle/meta/0001_snapshot.json +654 -0
  159. package/drizzle/meta/0002_snapshot.json +835 -0
  160. package/drizzle/meta/0003_snapshot.json +1023 -0
  161. package/drizzle/meta/0004_snapshot.json +1252 -0
  162. package/drizzle/meta/_journal.json +41 -0
  163. package/drizzle.config.ts +16 -0
  164. package/package.json +10 -1
  165. package/src/app/components/Component/Preview.tsx +3 -2
  166. package/src/app/components/Component/RegistryActions.tsx +150 -0
  167. package/src/app/components/Foundations/DisplayLogo.tsx +5 -3
  168. package/src/app/components/Layout/Header.tsx +21 -3
  169. package/src/app/components/Layout/Main.tsx +11 -3
  170. package/src/app/components/Navigation/MainNav.tsx +4 -2
  171. package/src/app/components/Navigation/MobileNav.tsx +4 -2
  172. package/src/app/components/Navigation/SideNav.tsx +46 -11
  173. package/src/app/components/NotFound.tsx +24 -0
  174. package/src/app/components/context/NavProvider.tsx +71 -0
  175. package/src/app/components/old/Header.tsx +3 -3
  176. package/src/app/components/util/index.ts +203 -380
  177. package/src/app/components/util/useCollectionAssets.ts +125 -0
  178. package/src/app/components/util/useFoundationTokens.ts +67 -0
  179. package/src/app/generated/default-pages.json +1 -0
  180. package/src/app/generated/nav-shell.json +1 -0
  181. package/src/app/lib/api/query.ts +6 -0
  182. package/src/app/lib/api/redact.ts +46 -0
  183. package/src/app/lib/asset-storage.ts +81 -0
  184. package/src/app/lib/docs-api/artifacts.ts +116 -0
  185. package/src/app/lib/docs-api/backend.ts +151 -0
  186. package/src/app/lib/docs-api/errors.ts +52 -0
  187. package/src/app/lib/docs-api/handler.ts +34 -0
  188. package/src/app/lib/docs-api/index.ts +7 -0
  189. package/src/app/lib/docs-api/records.ts +189 -0
  190. package/src/app/lib/docs-api/registry.ts +130 -0
  191. package/src/app/lib/docs-api/runtime-config.ts +173 -0
  192. package/src/app/lib/docs-api/serve.ts +37 -0
  193. package/src/app/lib/docs-api/token-detail.ts +19 -0
  194. package/src/app/lib/registry-api/allowlist.ts +163 -0
  195. package/src/app/lib/registry-api/asset-transfer.ts +363 -0
  196. package/src/app/lib/registry-api/auth.ts +68 -0
  197. package/src/app/lib/registry-api/entity-routes.ts +197 -0
  198. package/src/app/lib/registry-api/errors.ts +51 -0
  199. package/src/app/lib/registry-api/files.ts +135 -0
  200. package/src/app/lib/registry-api/handler.ts +96 -0
  201. package/src/app/lib/registry-api/index.ts +4 -0
  202. package/src/app/lib/registry-api/meta.ts +87 -0
  203. package/src/app/lib/registry-api/revalidate.ts +36 -0
  204. package/src/app/lib/registry-api/store.ts +348 -0
  205. package/src/app/lib/registry-api/token-transfer.ts +231 -0
  206. package/src/app/lib/registry-api/transfer.ts +513 -0
  207. package/src/app/lib/registry-api/validation.ts +74 -0
  208. package/src/app/lib/registry-connection.ts +58 -0
  209. package/src/app/lib/utils.ts +14 -0
  210. package/src/app/next.config.mjs +72 -13
  211. package/src/app/pages/[...slug]/index.tsx +74 -20
  212. package/src/app/pages/_app.tsx +5 -2
  213. package/src/app/pages/api/docs/artifacts/[...artifactPath].ts +16 -0
  214. package/src/app/pages/api/docs/assets/[collection]/[...path].ts +50 -0
  215. package/src/app/pages/api/docs/assets/[collection]/index.ts +22 -0
  216. package/src/app/pages/api/docs/components/[id]/assets/[asset].ts +33 -0
  217. package/src/app/pages/api/docs/components/[id]/inspect/[previewId].ts +20 -0
  218. package/src/app/pages/api/docs/components/[id]/previews/[previewId].ts +21 -0
  219. package/src/app/pages/api/docs/components/[id].ts +24 -0
  220. package/src/app/pages/api/docs/components.json.ts +12 -0
  221. package/src/app/pages/api/docs/nav.json.ts +48 -0
  222. package/src/app/pages/api/docs/patterns/[id]/preview.ts +19 -0
  223. package/src/app/pages/api/docs/patterns/[id].ts +24 -0
  224. package/src/app/pages/api/docs/patterns.json.ts +12 -0
  225. package/src/app/pages/api/docs/tokens/components/[id].ts +30 -0
  226. package/src/app/pages/api/docs/tokens/foundations/[type].ts +32 -0
  227. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +19 -0
  228. package/src/app/pages/api/registry/components/[id]/files/index.ts +19 -0
  229. package/src/app/pages/api/registry/components/[id].ts +20 -0
  230. package/src/app/pages/api/registry/components/index.ts +19 -0
  231. package/src/app/pages/api/registry/health.ts +20 -0
  232. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +19 -0
  233. package/src/app/pages/api/registry/patterns/[id]/files/index.ts +19 -0
  234. package/src/app/pages/api/registry/patterns/[id].ts +20 -0
  235. package/src/app/pages/api/registry/patterns/index.ts +19 -0
  236. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +39 -0
  237. package/src/app/pages/api/registry/transfer/assets/[collection].ts +24 -0
  238. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +23 -0
  239. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +11 -0
  240. package/src/app/pages/api/registry/transfer/assets/index.ts +10 -0
  241. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +26 -0
  242. package/src/app/pages/api/registry/transfer/tokens/index.ts +11 -0
  243. package/src/app/pages/assets/fonts.tsx +41 -17
  244. package/src/app/pages/assets/index.tsx +2 -2
  245. package/src/app/pages/assets/logos.tsx +2 -2
  246. package/src/app/pages/foundations/colors.tsx +18 -8
  247. package/src/app/pages/foundations/effects.tsx +18 -8
  248. package/src/app/pages/foundations/icons/[name]/index.tsx +26 -14
  249. package/src/app/pages/foundations/icons/index.tsx +13 -9
  250. package/src/app/pages/foundations/index.tsx +1 -1
  251. package/src/app/pages/foundations/logo.tsx +7 -4
  252. package/src/app/pages/foundations/typography.tsx +19 -9
  253. package/src/app/pages/index.tsx +1 -1
  254. package/src/app/pages/system/component/[component]/index.tsx +87 -17
  255. package/src/app/pages/system/index.tsx +13 -4
  256. package/src/app/pages/system/pattern/[pattern]/index.tsx +70 -14
  257. package/src/app/pages/system/tokens/components/[component]/index.tsx +42 -14
  258. package/src/app/pages/system/tokens/foundations/colors.tsx +7 -5
  259. package/src/app/pages/system/tokens/foundations/effects.tsx +7 -5
  260. package/src/app/pages/system/tokens/foundations/typography.tsx +8 -4
  261. package/src/app/tsconfig.json +2 -3
  262. package/src/app-builder/build.ts +555 -11
  263. package/src/app-builder/client-config.ts +61 -4
  264. package/src/app-builder/config-diff/snapshot.ts +1 -11
  265. package/src/app-builder/index.ts +1 -1
  266. package/src/app-builder/nav-shell.ts +74 -0
  267. package/src/app-builder/static-export.ts +215 -0
  268. package/src/app-builder/vercel-output.ts +297 -0
  269. package/src/app-builder/watchers/utils.ts +2 -0
  270. package/src/app-builder/watchers.ts +149 -25
  271. package/src/app-builder/websocket.ts +2 -2
  272. package/src/artifacts/README.md +14 -0
  273. package/src/artifacts/index.ts +17 -0
  274. package/src/artifacts/types.ts +146 -0
  275. package/src/artifacts/url.ts +90 -0
  276. package/src/cache/build-cache.ts +47 -2
  277. package/src/commands/build/app.ts +1 -2
  278. package/src/commands/build/index.ts +61 -0
  279. package/src/commands/checkout/index.ts +66 -0
  280. package/src/commands/db/migrate.ts +25 -0
  281. package/src/commands/index.ts +8 -0
  282. package/src/commands/publish/index.ts +63 -0
  283. package/src/config/defaults.ts +15 -0
  284. package/src/config/loader.ts +10 -1
  285. package/src/config/normalizers/declaration.ts +3 -0
  286. package/src/config/normalizers/page.ts +45 -0
  287. package/src/config/normalizers/pattern.ts +4 -1
  288. package/src/config/runtime.ts +48 -2
  289. package/src/index.ts +126 -3
  290. package/src/nav/index.ts +94 -0
  291. package/src/nav/registry.ts +54 -0
  292. package/src/nav/resolver.ts +217 -0
  293. package/src/nav/workspace.ts +87 -0
  294. package/src/pipeline/styles.ts +4 -35
  295. package/src/pipeline/token-transformers.ts +97 -0
  296. package/src/registry/asset-storage/adapters/vercel-blob.ts +51 -0
  297. package/src/registry/asset-storage/define.ts +15 -0
  298. package/src/registry/asset-storage/resolve.ts +72 -0
  299. package/src/registry/asset-storage/types.ts +46 -0
  300. package/src/registry/assets/layout.ts +38 -0
  301. package/src/registry/assets/sets.ts +48 -0
  302. package/src/registry/assets/transfer.ts +50 -0
  303. package/src/registry/checkout/assets.ts +126 -0
  304. package/src/registry/checkout/index.ts +596 -0
  305. package/src/registry/checkout/tokens.ts +152 -0
  306. package/src/registry/client.ts +234 -0
  307. package/src/registry/connection.ts +50 -0
  308. package/src/registry/db/adapter.ts +74 -0
  309. package/src/registry/db/client.ts +88 -0
  310. package/src/registry/db/index.ts +35 -0
  311. package/src/registry/db/migrate.ts +56 -0
  312. package/src/registry/db/schema.ts +444 -0
  313. package/src/registry/path.ts +26 -0
  314. package/src/registry/publish/artifacts.ts +147 -0
  315. package/src/registry/publish/assets.ts +129 -0
  316. package/src/registry/publish/errors.ts +34 -0
  317. package/src/registry/publish/index.ts +128 -0
  318. package/src/registry/publish/package.ts +272 -0
  319. package/src/registry/publish/publish-build.ts +51 -0
  320. package/src/registry/publish/tokens.ts +119 -0
  321. package/src/registry/tokens/sets.ts +132 -0
  322. package/src/registry/tokens/transfer.ts +53 -0
  323. package/src/registry/transfer.ts +82 -0
  324. package/src/store/filesystem-assets.ts +152 -0
  325. package/src/store/filesystem.ts +237 -0
  326. package/src/store/index.ts +62 -0
  327. package/src/store/registry.ts +334 -0
  328. package/src/store/source-files.ts +155 -0
  329. package/src/store/types.ts +210 -0
  330. package/src/transformers/plugins/csf-render.ts +31 -7
  331. package/src/transformers/plugins/handlebars-previews.ts +22 -3
  332. package/src/transformers/plugins/ssr-render.ts +145 -57
  333. package/src/transformers/preview/component/api.ts +2 -1
  334. package/src/transformers/preview/component/artifacts.ts +10 -1
  335. package/src/transformers/preview/component/builder.ts +9 -2
  336. package/src/transformers/preview/component/css.ts +10 -1
  337. package/src/transformers/preview/component/javascript.ts +55 -20
  338. package/src/transformers/preview/component/shared-artifacts.ts +146 -0
  339. package/src/transformers/preview/component.ts +5 -2
  340. package/src/transformers/preview/pattern/builder.ts +33 -7
  341. package/src/transformers/preview/pattern/html.ts +71 -27
  342. package/src/transformers/preview/types.ts +42 -0
  343. package/src/transformers/utils/handlebars.ts +31 -5
  344. package/src/types/config.ts +123 -25
  345. package/src/utils/menu-shell.ts +241 -0
  346. package/src/utils/stable-stringify.ts +7 -0
  347. package/templates/default/package.json.tpl +1 -1
  348. package/handoff.config.js +0 -90
  349. package/src/app/components/ComponentSearch.tsx +0 -93
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Registry migration runner.
3
+ *
4
+ * `handoff-app db:migrate` reads the consuming project's config + DB env vars, resolves the same
5
+ * `runtime.registry.database.adapter` used at runtime/build, and applies the package-owned
6
+ * migration set with the bundled Drizzle migrator. Migrations are resolved relative to the
7
+ * installed package (not the consuming project), so the schema is versioned with the tool. This
8
+ * runs independently of `build`.
9
+ */
10
+
11
+ import fs from 'fs-extra';
12
+ import path from 'path';
13
+ import type Handoff from '../../';
14
+ import { Logger } from '../../utils/logger';
15
+ import { resolveRegistryDatabase } from './adapter';
16
+ import { createRegistryDbConnection } from './client';
17
+
18
+ /**
19
+ * Absolute path to the package-owned migrations folder. Resolved relative to the installed
20
+ * `handoff-app` package root (`<package>/drizzle`) via {@link Handoff.modulePath} so it is
21
+ * independent of the consuming project's working directory.
22
+ */
23
+ export const getMigrationsFolder = (handoff: Handoff): string => path.resolve(handoff.modulePath, 'drizzle');
24
+
25
+ /**
26
+ * Run the package-owned registry migrations against the configured database.
27
+ *
28
+ * @throws when Handoff is uninitialized, the database URL env var is unset, or the packaged
29
+ * migrations are missing — each with an actionable message.
30
+ */
31
+ export const runRegistryMigrations = async (handoff: Handoff): Promise<void> => {
32
+ if (!handoff.config) {
33
+ throw new Error('Handoff is not initialized; cannot resolve registry database configuration.');
34
+ }
35
+
36
+ const { adapter, databaseUrlEnv, connectionString } = resolveRegistryDatabase(handoff.config);
37
+
38
+ const migrationsFolder = getMigrationsFolder(handoff);
39
+ const journalPath = path.join(migrationsFolder, 'meta', '_journal.json');
40
+ if (!fs.existsSync(journalPath)) {
41
+ throw new Error(
42
+ `Packaged registry migrations were not found at "${migrationsFolder}". This indicates a broken ` +
43
+ `handoff-app installation — reinstall the package to restore the bundled migration set.`
44
+ );
45
+ }
46
+
47
+ Logger.info(`Running registry migrations (adapter: "${adapter}", database url from "${databaseUrlEnv}").`);
48
+
49
+ const connection = await createRegistryDbConnection({ adapter, connectionString });
50
+ try {
51
+ await connection.migrate(migrationsFolder);
52
+ Logger.success('Registry migrations applied successfully.');
53
+ } finally {
54
+ await connection.close();
55
+ }
56
+ };
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Drizzle ORM schema for registry storage.
3
+ *
4
+ * The registry is database-backed (PostgreSQL, including PG-compatible managed services such as
5
+ * Neon/Vercel). This schema declares the six record groups the registry persists:
6
+ *
7
+ * - `components` / `component_files`
8
+ * - `patterns` / `pattern_files`
9
+ * - `docs_artifacts` (docs read-model artifacts)
10
+ * - `build_metadata` (build/artifact metadata)
11
+ *
12
+ * Schema notes:
13
+ * - Artifact and build-metadata columns mirror the structured artifact model
14
+ * (`ArtifactDescriptor`, `ArtifactBuildMetadata`, `ArtifactReference`) so the database can
15
+ * validate, serve, and debug an artifact without ever parsing HTML.
16
+ * - Metadata-only records are supported: a component/pattern row may exist with no
17
+ * `docs_artifacts` rows and a `build_metadata` row reporting `status: 'missing'`.
18
+ * - Text file records (`component_files` / `pattern_files`) cover checkout/inspection/management
19
+ * only and **exclude declaration files** — a CHECK constraint rejects `kind = 'declaration'`,
20
+ * so declarations have no representation in registry text file records.
21
+ * - `content` and `storage_ref` are both nullable so the model leaves room for future object
22
+ * storage of large/binary assets (a content-reference field) rather than assuming inline content.
23
+ */
24
+
25
+ import { sql } from 'drizzle-orm';
26
+ import { check, customType, index, integer, jsonb, pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core';
27
+ import type { ArtifactBuildStatus, ArtifactKind, ArtifactOwnerKind, ArtifactReference } from '../../artifacts/types';
28
+ import type { RegistryTextFileKind } from '../../store/types';
29
+ import type { TokenSetKind } from '../tokens/sets';
30
+
31
+ /**
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
34
+ * bytes inline in this column; larger assets are offloaded to an object-storage `storageRef` instead.
35
+ */
36
+ const bytea = customType<{ data: Buffer; driverData: Buffer }>({
37
+ dataType() {
38
+ return 'bytea';
39
+ },
40
+ });
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 & {});
44
+ import type { ComponentListObject, PageListObject, PatternComponentEntry, PatternListObject } from '../../transformers/preview/types';
45
+
46
+ /**
47
+ * Entity a docs read-model artifact is associated with. `page` is included only for type-compat with
48
+ * the generic management delete path (which filters `docs_artifacts.entityKind` by kind); pages emit
49
+ * no artifacts, so no page-owned `docs_artifacts` rows ever exist.
50
+ */
51
+ export type DocsArtifactEntityKind = 'component' | 'pattern' | 'page' | 'summary' | 'asset';
52
+
53
+ /** Entity a build-metadata record is associated with. A page publish writes one `build_metadata` row. */
54
+ export type BuildMetadataEntityKind = 'component' | 'pattern' | 'page' | 'asset' | 'summary';
55
+
56
+ /**
57
+ * Registry-only review/catalog metadata maintained through the management API.
58
+ *
59
+ * These fields are review state, not render/build inputs — the management API's metadata allowlist
60
+ * is the only writer, and updating them never touches `docs_artifacts` or `build_metadata`. Stored
61
+ * in a dedicated column rather than the normalized `record` jsonb so the served record stays
62
+ * identical in shape to what the filesystem store produces.
63
+ */
64
+ export interface RegistryReviewMetadata {
65
+ /** Free-form review status (e.g. `draft`, `in-review`, `approved`). */
66
+ reviewStatus?: string;
67
+ /** Reviewer notes. */
68
+ notes?: string;
69
+ /** Owner identifier (name, handle, or email). */
70
+ owner?: string;
71
+ }
72
+
73
+ /** Build status mirrored from {@link import('../../artifacts/types').ArtifactBuildStatus}. */
74
+ export type RegistryBuildStatus = ArtifactBuildStatus;
75
+
76
+ /**
77
+ * Components record group. `record` holds the full normalized {@link ComponentListObject} so the
78
+ * registry serves the same record shape as the filesystem store; the promoted columns exist for
79
+ * querying/sorting the catalog.
80
+ */
81
+ export const components = pgTable(
82
+ 'components',
83
+ {
84
+ /** Stable component id (explicit `id`, else directory basename). Join key across stores. */
85
+ id: text('id').primaryKey(),
86
+ /** Logical docs path for the component. */
87
+ path: text('path').notNull(),
88
+ title: text('title'),
89
+ description: text('description'),
90
+ group: text('group'),
91
+ type: text('type'),
92
+ renderer: text('renderer'),
93
+ tags: jsonb('tags').$type<string[]>(),
94
+ categories: jsonb('categories').$type<string[]>(),
95
+ /** Full normalized component record. */
96
+ record: jsonb('record').$type<ComponentListObject>().notNull(),
97
+ /** Registry-only review/catalog metadata (management-API allowlist; never a render input). */
98
+ metadata: jsonb('metadata').$type<RegistryReviewMetadata>(),
99
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
100
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
101
+ },
102
+ (table) => [index('components_group_idx').on(table.group)]
103
+ );
104
+
105
+ /**
106
+ * Component source files. Used for checkout/inspection/management only — never to rebuild
107
+ * previews. Declaration files are rejected at the schema level.
108
+ */
109
+ export const componentFiles = pgTable(
110
+ 'component_files',
111
+ {
112
+ componentId: text('component_id')
113
+ .notNull()
114
+ .references(() => components.id, { onDelete: 'cascade' }),
115
+ /** Registry-safe relative path within the entity (e.g. `Badge.tsx`). */
116
+ path: text('path').notNull(),
117
+ kind: text('kind').$type<RegistryTextFileKind>().notNull(),
118
+ /** Inline content; null when stored externally via `storageRef`. */
119
+ content: text('content'),
120
+ /** Reference to externally stored content (future object storage). */
121
+ storageRef: text('storage_ref'),
122
+ contentType: text('content_type').notNull(),
123
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
124
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
125
+ },
126
+ (table) => [
127
+ primaryKey({ columns: [table.componentId, table.path] }),
128
+ check('component_files_kind_not_declaration', sql`${table.kind} <> 'declaration'`),
129
+ ]
130
+ );
131
+
132
+ /**
133
+ * Patterns record group. `record` holds the full normalized {@link PatternListObject};
134
+ * `components` promotes the pattern's component refs for querying.
135
+ */
136
+ export const patterns = pgTable(
137
+ 'patterns',
138
+ {
139
+ /** Stable pattern id. Join key across stores. */
140
+ id: text('id').primaryKey(),
141
+ path: text('path').notNull(),
142
+ title: text('title'),
143
+ description: text('description'),
144
+ group: text('group'),
145
+ tags: jsonb('tags').$type<string[]>(),
146
+ /** Pattern component references. */
147
+ components: jsonb('components').$type<PatternComponentEntry[]>(),
148
+ /** Full normalized pattern record. */
149
+ record: jsonb('record').$type<PatternListObject>().notNull(),
150
+ /** Registry-only review/catalog metadata (management-API allowlist; never a render input). */
151
+ metadata: jsonb('metadata').$type<RegistryReviewMetadata>(),
152
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
153
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
154
+ },
155
+ (table) => [index('patterns_group_idx').on(table.group)]
156
+ );
157
+
158
+ /**
159
+ * Pattern source files. Same checkout/inspection/management role as {@link componentFiles};
160
+ * declaration files are rejected at the schema level.
161
+ */
162
+ export const patternFiles = pgTable(
163
+ 'pattern_files',
164
+ {
165
+ patternId: text('pattern_id')
166
+ .notNull()
167
+ .references(() => patterns.id, { onDelete: 'cascade' }),
168
+ path: text('path').notNull(),
169
+ kind: text('kind').$type<RegistryTextFileKind>().notNull(),
170
+ content: text('content'),
171
+ storageRef: text('storage_ref'),
172
+ contentType: text('content_type').notNull(),
173
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
174
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
175
+ },
176
+ (table) => [
177
+ primaryKey({ columns: [table.patternId, table.path] }),
178
+ check('pattern_files_kind_not_declaration', sql`${table.kind} <> 'declaration'`),
179
+ ]
180
+ );
181
+
182
+ /**
183
+ * Pages record group. `record` holds the full normalized {@link PageListObject} (frontmatter
184
+ * metadata) so the registry serves the same record shape the filesystem store produces; promoted
185
+ * columns exist for catalog querying/sorting. The markdown body travels as a `page_files` row, not in
186
+ * the record — see {@link pageFiles}.
187
+ */
188
+ export const pages = pgTable(
189
+ 'pages',
190
+ {
191
+ /** Stable page id (slug path, e.g. `guides/setup`). Join key across stores. */
192
+ id: text('id').primaryKey(),
193
+ /** Logical route the page is served at (e.g. `/guides/setup`). */
194
+ path: text('path').notNull(),
195
+ title: text('title'),
196
+ description: text('description'),
197
+ group: text('group'),
198
+ /** Sort weight within the nav section. */
199
+ weight: integer('weight'),
200
+ /** Full normalized page record. */
201
+ record: jsonb('record').$type<PageListObject>().notNull(),
202
+ /** Registry-only review/catalog metadata (management-API allowlist; never a render input). */
203
+ metadata: jsonb('metadata').$type<RegistryReviewMetadata>(),
204
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
205
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
206
+ },
207
+ (table) => [index('pages_group_idx').on(table.group)]
208
+ );
209
+
210
+ /**
211
+ * Page source files. A page has a single verbatim `.md` (kind `markdown`) stored for byte-exact
212
+ * checkout and for resolving the rendered body at request time. Declaration files are rejected at the
213
+ * schema level (consistent with the other file tables, though pages never carry declarations).
214
+ */
215
+ export const pageFiles = pgTable(
216
+ 'page_files',
217
+ {
218
+ pageId: text('page_id')
219
+ .notNull()
220
+ .references(() => pages.id, { onDelete: 'cascade' }),
221
+ /** Registry-safe relative path within the entity (e.g. `guides/setup.md`). */
222
+ path: text('path').notNull(),
223
+ kind: text('kind').$type<RegistryTextFileKind>().notNull(),
224
+ content: text('content'),
225
+ storageRef: text('storage_ref'),
226
+ contentType: text('content_type').notNull(),
227
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
228
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
229
+ },
230
+ (table) => [
231
+ primaryKey({ columns: [table.pageId, table.path] }),
232
+ check('page_files_kind_not_declaration', sql`${table.kind} <> 'declaration'`),
233
+ ]
234
+ );
235
+
236
+ /**
237
+ * Docs read-model artifacts. Path-keyed so shared/global artifacts (`component/main.css`,
238
+ * `component/main.js`, `component/shared.css`) are upserted by stable logical path. Mirrors the
239
+ * `ArtifactDescriptor` field set.
240
+ */
241
+ export const docsArtifacts = pgTable(
242
+ 'docs_artifacts',
243
+ {
244
+ /** Logical artifact path (e.g. `component/badge-primary.html`). Stable serving key. */
245
+ path: text('path').primaryKey(),
246
+ entityKind: text('entity_kind').$type<DocsArtifactEntityKind>().notNull(),
247
+ entityId: text('entity_id'),
248
+ artifactKind: text('artifact_kind').$type<ArtifactKind>().notNull(),
249
+ /** Inline content; null when stored externally via `storageRef`. */
250
+ content: text('content'),
251
+ /** Reference to externally stored content (future object storage). */
252
+ storageRef: text('storage_ref'),
253
+ contentType: text('content_type').notNull(),
254
+ ownerKind: text('owner_kind').$type<ArtifactOwnerKind>(),
255
+ ownerId: text('owner_id'),
256
+ /** Structured required/optional references (drives validation/serving/dedup). */
257
+ references: jsonb('references').$type<ArtifactReference[]>(),
258
+ formatVersion: text('format_version'),
259
+ buildId: text('build_id'),
260
+ hash: text('hash'),
261
+ size: integer('size'),
262
+ /** Gzipped byte size diagnostic, when measured. */
263
+ gzipSize: integer('gzip_size'),
264
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
265
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
266
+ },
267
+ (table) => [
268
+ index('docs_artifacts_entity_idx').on(table.entityKind, table.entityId),
269
+ index('docs_artifacts_owner_idx').on(table.ownerKind, table.ownerId),
270
+ ]
271
+ );
272
+
273
+ /**
274
+ * Build/artifact metadata per entity. Mirrors the `ArtifactBuildMetadata` field set.
275
+ * A successful publish records `status: 'current'` with `builtAt` + `builderVersion` +
276
+ * `artifactHash`; metadata-only records report `status: 'missing'`.
277
+ */
278
+ export const buildMetadata = pgTable(
279
+ 'build_metadata',
280
+ {
281
+ entityKind: text('entity_kind').$type<BuildMetadataEntityKind>().notNull(),
282
+ entityId: text('entity_id').notNull(),
283
+ status: text('status').$type<RegistryBuildStatus>().notNull(),
284
+ builtAt: timestamp('built_at', { withTimezone: true }),
285
+ builderVersion: text('builder_version'),
286
+ artifactHash: text('artifact_hash'),
287
+ sourceHash: text('source_hash'),
288
+ warnings: jsonb('warnings').$type<string[]>(),
289
+ error: text('error'),
290
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
291
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
292
+ },
293
+ (table) => [primaryKey({ columns: [table.entityKind, table.entityId] })]
294
+ );
295
+
296
+ /**
297
+ * Token sets record group. One row per logical token set (`foundation/colors`, `component/<id>`).
298
+ * `record` holds the exact extracted token slice as JSONB so no token property is lost and new ones
299
+ * flow through without a migration. Build/publish metadata (status/source_hash/built_at) lives on
300
+ * this row — tokens are self-contained and do not use the shared `build_metadata` table.
301
+ */
302
+ export const tokenSets = pgTable(
303
+ 'token_sets',
304
+ {
305
+ /** Stable logical set id (`foundation/colors` | `component/button`). Join key across stores. */
306
+ id: text('id').primaryKey(),
307
+ /** Set kind (`foundation` | `component`). */
308
+ kind: text('kind').$type<TokenSetKind>().notNull(),
309
+ /** Full extracted token slice (`IColorObject[]`/…/`IFileComponentObject`). */
310
+ record: jsonb('record').notNull(),
311
+ /** Deterministic content hash over the record + generated artifacts (drives skip-unchanged). */
312
+ sourceHash: text('source_hash'),
313
+ /** Build/publish status for the set. */
314
+ status: text('status').$type<RegistryBuildStatus>(),
315
+ builtAt: timestamp('built_at', { withTimezone: true }),
316
+ builderVersion: text('builder_version'),
317
+ /** Registry-only review/catalog metadata (parity with other entities; management-API only). */
318
+ metadata: jsonb('metadata').$type<RegistryReviewMetadata>(),
319
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
320
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
321
+ },
322
+ (table) => [index('token_sets_kind_idx').on(table.kind)]
323
+ );
324
+
325
+ /**
326
+ * Generated token artifacts owned by a token set (CSS/SCSS/Style Dictionary/types/custom transformer
327
+ * output). Served byte-for-byte; the registry never re-runs transformers. Keyed by `(token_set_id,
328
+ * path)` where `path` is the registry-safe output path relative to `getVariablesFilePath()`.
329
+ */
330
+ export const tokenArtifacts = pgTable(
331
+ 'token_artifacts',
332
+ {
333
+ tokenSetId: text('token_set_id')
334
+ .notNull()
335
+ .references(() => tokenSets.id, { onDelete: 'cascade' }),
336
+ /** Registry-safe relative output path (e.g. `css/colors.css`, `sd/button/button.tokens.json`). */
337
+ path: text('path').notNull(),
338
+ /** Logical format label (`css`|`scss`|`types`|`styleDictionary`|custom outDir). */
339
+ format: text('format').notNull(),
340
+ /** Inline content; null when stored externally via `storageRef`. */
341
+ content: text('content'),
342
+ /** Reference to externally stored content (future object storage). */
343
+ storageRef: text('storage_ref'),
344
+ contentType: text('content_type').notNull(),
345
+ hash: text('hash'),
346
+ size: integer('size'),
347
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
348
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
349
+ },
350
+ (table) => [primaryKey({ columns: [table.tokenSetId, table.path] })]
351
+ );
352
+
353
+ /**
354
+ * Asset collections. Owns a collection (`icons`|`logos`|`fonts`) and its build/publish metadata
355
+ * (mirrors {@link tokenSets}): `sourceHash` drives skip-unchanged, and the set of `assets` rows for a
356
+ * collection is its current manifest. Publish replaces those rows atomically, so readers always see a
357
+ * complete manifest.
358
+ */
359
+ export const assetCollections = pgTable(
360
+ 'asset_collections',
361
+ {
362
+ /** Stable collection id (`icons`|`logos`|`fonts`). Join key across stores. */
363
+ collection: text('collection').primaryKey(),
364
+ /** Deterministic content hash over the collection's `(path, contentHash)` manifest (skip-unchanged). */
365
+ sourceHash: text('source_hash'),
366
+ /** Build/publish status for the collection. */
367
+ status: text('status').$type<RegistryBuildStatus>(),
368
+ builtAt: timestamp('built_at', { withTimezone: true }),
369
+ builderVersion: text('builder_version'),
370
+ /** Registry-only review/catalog metadata (parity with other entities; management-API only). */
371
+ metadata: jsonb('metadata').$type<RegistryReviewMetadata>(),
372
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
373
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
374
+ }
375
+ );
376
+
377
+ /**
378
+ * Logical asset metadata - the collection manifest. One row per asset (icon/logo SVG, sprite,
379
+ * sprite manifest, ZIP archive, font archive), keyed by `(collection, path)`. Binary content is not
380
+ * stored here; `blobHash` references the content-addressed {@link assetBlobs} store, so identical
381
+ * bytes across paths/collections dedupe to one blob.
382
+ */
383
+ export const assets = pgTable(
384
+ 'assets',
385
+ {
386
+ collection: text('collection')
387
+ .notNull()
388
+ .references(() => assetCollections.collection, { onDelete: 'cascade' }),
389
+ /** Registry-safe logical path within the collection (e.g. `assets/icons/add.svg`, `icons.zip`). */
390
+ path: text('path').notNull(),
391
+ /** Human-facing asset name. */
392
+ name: text('name').notNull(),
393
+ contentType: text('content_type').notNull(),
394
+ size: integer('size'),
395
+ /** SHA-256 of the asset bytes - the blob identity, and the ETag for content responses. */
396
+ contentHash: text('content_hash').notNull(),
397
+ /** Free-form asset metadata carried from extraction (icon index, description, …). */
398
+ metadata: jsonb('metadata'),
399
+ /** References {@link assetBlobs.hash}; equals `contentHash`. */
400
+ blobHash: text('blob_hash')
401
+ .notNull()
402
+ .references(() => assetBlobs.hash),
403
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
404
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
405
+ },
406
+ (table) => [primaryKey({ columns: [table.collection, table.path] })]
407
+ );
408
+
409
+ /**
410
+ * Content-addressed binary blob store, shared across assets/collections (the `hash` is the blob
411
+ * identity, so an unchanged file re-references the same row). Exactly one location is active per row:
412
+ * inline `content` (bytea, default DB adapter) or an external `storageRef` resolved through the
413
+ * recorded `storageProvider`. Both nullable so DB- and object-backed blobs coexist.
414
+ */
415
+ export const assetBlobs = pgTable('asset_blobs', {
416
+ /** SHA-256 of the blob bytes (hex). */
417
+ hash: text('hash').primaryKey(),
418
+ /** Which storage backed this blob when it was written (`database`|`vercel-blob`|custom id). */
419
+ storageProvider: text('storage_provider').$type<AssetStorageProvider>().notNull(),
420
+ /** Inline bytes; null when stored externally via `storageRef`. */
421
+ content: bytea('content'),
422
+ /** External object reference/key; null when stored inline in `content`. */
423
+ storageRef: text('storage_ref'),
424
+ contentType: text('content_type').notNull(),
425
+ size: integer('size').notNull(),
426
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
427
+ });
428
+
429
+ /** All registry tables, for typed Drizzle clients and migration tooling. */
430
+ export const registrySchema = {
431
+ components,
432
+ componentFiles,
433
+ patterns,
434
+ patternFiles,
435
+ pages,
436
+ pageFiles,
437
+ docsArtifacts,
438
+ buildMetadata,
439
+ tokenSets,
440
+ tokenArtifacts,
441
+ assetCollections,
442
+ assets,
443
+ assetBlobs,
444
+ };
@@ -0,0 +1,26 @@
1
+ import path from 'path';
2
+
3
+ export const isSafeRelativePath = (value: string): boolean => {
4
+ if (typeof value !== 'string' || value.trim() === '') {
5
+ return false;
6
+ }
7
+ const normalized = normalizeRelativePath(value);
8
+ if (normalized.startsWith('/') || /^[a-zA-Z]:/.test(normalized)) {
9
+ return false;
10
+ }
11
+ return normalized.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..');
12
+ };
13
+
14
+ export const normalizeRelativePath = (value: string): string => value.replace(/\\/g, '/');
15
+
16
+ export const isSafePathSegment = (value: string): boolean =>
17
+ typeof value === 'string' && value === value.trim() && isSafeRelativePath(value) && !normalizeRelativePath(value).includes('/');
18
+
19
+ export const resolvePathWithin = (root: string, value: string): string | null => {
20
+ if (!isSafeRelativePath(value)) {
21
+ return null;
22
+ }
23
+ const absolutePath = path.resolve(root, ...normalizeRelativePath(value).split('/'));
24
+ const relativePath = path.relative(root, absolutePath);
25
+ return relativePath.startsWith('..') || path.isAbsolute(relativePath) ? null : absolutePath;
26
+ };
@@ -0,0 +1,147 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import {
4
+ ARTIFACTS_ROUTE_SEGMENT,
5
+ SHARED_MAIN_CSS_ARTIFACT_PATH,
6
+ SHARED_MAIN_JS_ARTIFACT_PATH,
7
+ SHARED_STYLES_CSS_ARTIFACT_PATH,
8
+ } from '../../artifacts';
9
+ import type { ArtifactKind, ArtifactReference } from '../../artifacts/types';
10
+ import Handoff from '../../index';
11
+ import type { TransferArtifact } from '../transfer';
12
+
13
+ const SHARED_ARTIFACT_PATHS = new Set([SHARED_MAIN_CSS_ARTIFACT_PATH, SHARED_MAIN_JS_ARTIFACT_PATH, SHARED_STYLES_CSS_ARTIFACT_PATH]);
14
+
15
+ const CLIENT_ARTIFACT_SUFFIX = '.client.js';
16
+
17
+ const ARTIFACT_KIND_BY_EXT: Record<string, ArtifactKind> = {
18
+ '.json': 'json',
19
+ '.html': 'html',
20
+ '.css': 'css',
21
+ '.js': 'javascript',
22
+ };
23
+
24
+ const CONTENT_TYPE_BY_EXT: Record<string, string> = {
25
+ '.json': 'application/json',
26
+ '.html': 'text/html; charset=utf-8',
27
+ '.css': 'text/css; charset=utf-8',
28
+ '.js': 'application/javascript; charset=utf-8',
29
+ };
30
+
31
+ const artifactKindForPath = (artifactPath: string): ArtifactKind =>
32
+ ARTIFACT_KIND_BY_EXT[path.extname(artifactPath).toLowerCase()] ?? 'other';
33
+
34
+ const contentTypeForPath = (artifactPath: string): string =>
35
+ CONTENT_TYPE_BY_EXT[path.extname(artifactPath).toLowerCase()] ?? 'text/plain; charset=utf-8';
36
+
37
+ /** Absolute path of the generated artifact root the build writes to (`<workspace>/public/api`). */
38
+ export const getArtifactRoot = (handoff: Handoff): string => path.resolve(handoff.workingPath, 'public', 'api');
39
+
40
+ export const isSharedArtifactPath = (artifactPath: string): boolean => SHARED_ARTIFACT_PATHS.has(artifactPath);
41
+
42
+ /**
43
+ * Resolve the owning component id for a file under `public/api/component`, or `null` when it is a
44
+ * shared artifact or owned by no known component. Exact CSS, JavaScript, JSON, and client bundle
45
+ * filenames map directly. HTML filenames use the longest known component id prefix, which prevents
46
+ * a component such as `button group` from being read as `button`.
47
+ */
48
+ export const resolveComponentArtifactOwner = (fileName: string, componentIds: Set<string>): string | null => {
49
+ if (isSharedArtifactPath(`component/${fileName}`)) {
50
+ return null;
51
+ }
52
+ if (fileName.endsWith(CLIENT_ARTIFACT_SUFFIX)) {
53
+ const id = fileName.slice(0, -CLIENT_ARTIFACT_SUFFIX.length);
54
+ return componentIds.has(id) ? id : null;
55
+ }
56
+ const ext = path.extname(fileName).toLowerCase();
57
+ if (ext === '.css' || ext === '.js' || ext === '.json') {
58
+ const id = fileName.slice(0, -ext.length);
59
+ return componentIds.has(id) ? id : null;
60
+ }
61
+ if (ext === '.html') {
62
+ let longest: string | null = null;
63
+ for (const id of Array.from(componentIds)) {
64
+ if (fileName.startsWith(`${id}-`) && (!longest || id.length > longest.length)) {
65
+ longest = id;
66
+ }
67
+ }
68
+ return longest;
69
+ }
70
+ return null;
71
+ };
72
+
73
+ /** Extract the logical artifact paths an HTML document references through the canonical route. */
74
+ const extractReferencedArtifactPaths = (html: string): string[] => {
75
+ const pattern = new RegExp(`(?:href|src)=["'][^"']*?${ARTIFACTS_ROUTE_SEGMENT}/([^"']+)["']`, 'g');
76
+ const paths = new Set<string>();
77
+ let match: RegExpExecArray | null;
78
+ while ((match = pattern.exec(html)) !== null) {
79
+ const encoded = match[1].split(/[?#]/)[0];
80
+ const segments = encoded.split('/').filter(Boolean);
81
+ try {
82
+ const decoded = segments.map((segment) => decodeURIComponent(segment));
83
+ if (decoded.length > 0) {
84
+ paths.add(decoded.join('/'));
85
+ }
86
+ } catch {
87
+ paths.add(encoded);
88
+ }
89
+ }
90
+ return Array.from(paths);
91
+ };
92
+
93
+ /** Build a structured reference for a referenced artifact path (only the client bundle is required). */
94
+ const toArtifactReference = (referencedPath: string): ArtifactReference => {
95
+ if (referencedPath.endsWith(CLIENT_ARTIFACT_SUFFIX)) {
96
+ const ownerId = path.basename(referencedPath).slice(0, -CLIENT_ARTIFACT_SUFFIX.length);
97
+ return { path: referencedPath, kind: 'client', required: true, ownerKind: 'component', ownerId };
98
+ }
99
+ if (isSharedArtifactPath(referencedPath)) {
100
+ return { path: referencedPath, kind: 'shared', required: false, ownerKind: 'asset', ownerId: null };
101
+ }
102
+ const ext = path.extname(referencedPath).toLowerCase();
103
+ if (referencedPath.startsWith('component/') && (ext === '.css' || ext === '.js')) {
104
+ const ownerId = path.basename(referencedPath, ext);
105
+ return { path: referencedPath, kind: ext === '.css' ? 'style' : 'script', required: false, ownerKind: 'component', ownerId };
106
+ }
107
+ return { path: referencedPath, kind: 'other', required: false };
108
+ };
109
+
110
+ /** Read a generated artifact file into a {@link TransferArtifact}, or `null` when it does not exist. */
111
+ export const readArtifact = (
112
+ root: string,
113
+ artifactPath: string,
114
+ ownerKind: TransferArtifact['ownerKind'],
115
+ ownerId: string | null
116
+ ): TransferArtifact | null => {
117
+ const absolutePath = path.resolve(root, ...artifactPath.split('/'));
118
+ if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
119
+ return null;
120
+ }
121
+ const content = fs.readFileSync(absolutePath, 'utf8');
122
+ const references = artifactPath.endsWith('.html') ? extractReferencedArtifactPaths(content).map(toArtifactReference) : undefined;
123
+ return {
124
+ path: artifactPath,
125
+ artifactKind: artifactKindForPath(artifactPath),
126
+ content,
127
+ contentType: contentTypeForPath(artifactPath),
128
+ ownerKind,
129
+ ownerId,
130
+ references,
131
+ size: Buffer.byteLength(content, 'utf8'),
132
+ };
133
+ };
134
+
135
+ /** Add a referenced shared/component artifact to the map when present on disk (deduped by path). */
136
+ export const addReferencedArtifact = (root: string, artifactPath: string, byPath: Map<string, TransferArtifact>): void => {
137
+ if (byPath.has(artifactPath)) {
138
+ return;
139
+ }
140
+ const reference = toArtifactReference(artifactPath);
141
+ const ownerKind = reference.ownerKind ?? (isSharedArtifactPath(artifactPath) ? 'asset' : 'component');
142
+ const ownerId = reference.ownerKind === 'asset' ? null : (reference.ownerId ?? null);
143
+ const artifact = readArtifact(root, artifactPath, ownerKind, ownerId);
144
+ if (artifact) {
145
+ byPath.set(artifactPath, artifact);
146
+ }
147
+ };