@tuongaz/seeflow 0.1.65 → 0.1.68

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 (303) hide show
  1. package/README.md +4 -0
  2. package/dist/web/assets/abap-DsBKuouk.js +1 -0
  3. package/dist/web/assets/actionscript-3-D_z4Izcz.js +1 -0
  4. package/dist/web/assets/ada-727ZlQH0.js +1 -0
  5. package/dist/web/assets/andromeeda-C3khCPGq.js +1 -0
  6. package/dist/web/assets/angular-html-4alyEGLm.js +1 -0
  7. package/dist/web/assets/angular-ts-BixEUTMq.js +1 -0
  8. package/dist/web/assets/apache-Dn00JSTd.js +1 -0
  9. package/dist/web/assets/apex-COJ4H7py.js +1 -0
  10. package/dist/web/assets/apl-BBq3IX1j.js +1 -0
  11. package/dist/web/assets/applescript-Bu5BbsvL.js +1 -0
  12. package/dist/web/assets/ara-7O62HKoU.js +1 -0
  13. package/dist/web/assets/asciidoc-BPT9niGB.js +1 -0
  14. package/dist/web/assets/asm-Dhn9LcZ4.js +1 -0
  15. package/dist/web/assets/astro-CqkE3fuf.js +1 -0
  16. package/dist/web/assets/aurora-x-D-2ljcwZ.js +1 -0
  17. package/dist/web/assets/awk-eg146-Ew.js +1 -0
  18. package/dist/web/assets/ayu-dark-Cv9koXgw.js +1 -0
  19. package/dist/web/assets/ballerina-Du268qiB.js +1 -0
  20. package/dist/web/assets/bat-fje9CFhw.js +1 -0
  21. package/dist/web/assets/beancount-BwXTMy5W.js +1 -0
  22. package/dist/web/assets/berry-3xVqZejG.js +1 -0
  23. package/dist/web/assets/bibtex-xW4inM5L.js +1 -0
  24. package/dist/web/assets/bicep-DHo0CJ0O.js +1 -0
  25. package/dist/web/assets/blade-a8OxSdnT.js +1 -0
  26. package/dist/web/assets/bsl-Dgyn0ogV.js +1 -0
  27. package/dist/web/assets/c-C3t2pwGQ.js +1 -0
  28. package/dist/web/assets/cadence-DNquZEk8.js +1 -0
  29. package/dist/web/assets/cairo--RitsXJZ.js +1 -0
  30. package/dist/web/assets/catppuccin-frappe-CD_QflpE.js +1 -0
  31. package/dist/web/assets/catppuccin-latte-DRW-0cLl.js +1 -0
  32. package/dist/web/assets/catppuccin-macchiato-C-_shW-Y.js +1 -0
  33. package/dist/web/assets/catppuccin-mocha-LGGdnPYs.js +1 -0
  34. package/dist/web/assets/chart-8DxAnLoD.js +73 -0
  35. package/dist/web/assets/clarity-BHOwM8T6.js +1 -0
  36. package/dist/web/assets/clojure-DxSadP1t.js +1 -0
  37. package/dist/web/assets/cmake-DbXoA79R.js +1 -0
  38. package/dist/web/assets/cobol-PTqiYgYu.js +1 -0
  39. package/dist/web/assets/code-block-cx7LPXvE.js +13 -0
  40. package/dist/web/assets/codeowners-Bp6g37R7.js +1 -0
  41. package/dist/web/assets/codeql-sacFqUAJ.js +1 -0
  42. package/dist/web/assets/coffee-dyiR41kL.js +1 -0
  43. package/dist/web/assets/common-lisp-C7gG9l05.js +1 -0
  44. package/dist/web/assets/coq-Dsg_Bt_b.js +1 -0
  45. package/dist/web/assets/cpp-BksuvNSY.js +1 -0
  46. package/dist/web/assets/crystal-DtDmRg-F.js +1 -0
  47. package/dist/web/assets/csharp-D9R-vmeu.js +1 -0
  48. package/dist/web/assets/css-BPhBrDlE.js +1 -0
  49. package/dist/web/assets/csv-B0qRVHPH.js +1 -0
  50. package/dist/web/assets/cue-DtFQj3wx.js +1 -0
  51. package/dist/web/assets/cypher-m2LEI-9-.js +1 -0
  52. package/dist/web/assets/d-BoXegm-a.js +1 -0
  53. package/dist/web/assets/dark-plus-C3mMm8J8.js +1 -0
  54. package/dist/web/assets/dart-B9wLZaAG.js +1 -0
  55. package/dist/web/assets/dax-ClGRhx96.js +1 -0
  56. package/dist/web/assets/desktop-DEIpsLCJ.js +1 -0
  57. package/dist/web/assets/diff-BgYniUM_.js +1 -0
  58. package/dist/web/assets/docker-COcR7UxN.js +1 -0
  59. package/dist/web/assets/dotenv-BjQB5zDj.js +1 -0
  60. package/dist/web/assets/dracula-BzJJZx-M.js +1 -0
  61. package/dist/web/assets/dracula-soft-BXkSAIEj.js +1 -0
  62. package/dist/web/assets/dream-maker-C-nORZOA.js +1 -0
  63. package/dist/web/assets/edge-D5gP-w-T.js +1 -0
  64. package/dist/web/assets/elixir-CLiX3zqd.js +1 -0
  65. package/dist/web/assets/elm-CmHSxxaM.js +1 -0
  66. package/dist/web/assets/emacs-lisp-BX77sIaO.js +1 -0
  67. package/dist/web/assets/erb-BYTLMnw6.js +1 -0
  68. package/dist/web/assets/erlang-B-DoSBHF.js +1 -0
  69. package/dist/web/assets/everforest-dark-BgDCqdQA.js +1 -0
  70. package/dist/web/assets/everforest-light-C8M2exoo.js +1 -0
  71. package/dist/web/assets/fennel-bCA53EVm.js +1 -0
  72. package/dist/web/assets/fish-w-ucz2PV.js +1 -0
  73. package/dist/web/assets/fluent-Dayu4EKP.js +1 -0
  74. package/dist/web/assets/fortran-fixed-form-TqA4NnZg.js +1 -0
  75. package/dist/web/assets/fortran-free-form-DKXYxT9g.js +1 -0
  76. package/dist/web/assets/fsharp-XplgxFYe.js +1 -0
  77. package/dist/web/assets/gdresource-BHYsBjWJ.js +1 -0
  78. package/dist/web/assets/gdscript-DfxzS6Rs.js +1 -0
  79. package/dist/web/assets/gdshader-SKMF96pI.js +1 -0
  80. package/dist/web/assets/genie-ajMbGru0.js +1 -0
  81. package/dist/web/assets/gherkin--30QC5Em.js +1 -0
  82. package/dist/web/assets/git-commit-i4q6IMui.js +1 -0
  83. package/dist/web/assets/git-rebase-B-v9cOL2.js +1 -0
  84. package/dist/web/assets/github-dark-DHJKELXO.js +1 -0
  85. package/dist/web/assets/github-dark-default-Cuk6v7N8.js +1 -0
  86. package/dist/web/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  87. package/dist/web/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  88. package/dist/web/assets/github-light-DAi9KRSo.js +1 -0
  89. package/dist/web/assets/github-light-default-D7oLnXFd.js +1 -0
  90. package/dist/web/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  91. package/dist/web/assets/gleam-B430Bg39.js +1 -0
  92. package/dist/web/assets/glimmer-js-D-cwc0-E.js +1 -0
  93. package/dist/web/assets/glimmer-ts-pgjy16dm.js +1 -0
  94. package/dist/web/assets/glsl-DBO2IWDn.js +1 -0
  95. package/dist/web/assets/gnuplot-CM8KxXT1.js +1 -0
  96. package/dist/web/assets/go-B1SYOhNW.js +1 -0
  97. package/dist/web/assets/graphql-cDcHW_If.js +1 -0
  98. package/dist/web/assets/groovy-DkBy-JyN.js +1 -0
  99. package/dist/web/assets/hack-D1yCygmZ.js +1 -0
  100. package/dist/web/assets/haml-B2EZWmdv.js +1 -0
  101. package/dist/web/assets/handlebars-BQGss363.js +1 -0
  102. package/dist/web/assets/haskell-BILxekzW.js +1 -0
  103. package/dist/web/assets/haxe-C5wWYbrZ.js +1 -0
  104. package/dist/web/assets/hcl-HzYwdGDm.js +1 -0
  105. package/dist/web/assets/hjson-T-Tgc4AT.js +1 -0
  106. package/dist/web/assets/hlsl-ifBTmRxC.js +1 -0
  107. package/dist/web/assets/houston-DnULxvSX.js +1 -0
  108. package/dist/web/assets/html-C2L_23MC.js +1 -0
  109. package/dist/web/assets/html-derivative-CSfWNPLT.js +1 -0
  110. package/dist/web/assets/http-FRrOvY1W.js +1 -0
  111. package/dist/web/assets/hxml-TIA70rKU.js +1 -0
  112. package/dist/web/assets/hy-BMj5Y0dO.js +1 -0
  113. package/dist/web/assets/imba-bv_oIlVt.js +1 -0
  114. package/dist/web/assets/index-CeLShda7.css +1 -0
  115. package/dist/web/assets/index-Dp4QwEl0.js +8608 -0
  116. package/dist/web/assets/{index.es-CVm3MRo3.js → index.es-C-uXEdZB.js} +1 -1
  117. package/dist/web/assets/ini-BjABl1g7.js +1 -0
  118. package/dist/web/assets/java-xI-RfyKK.js +1 -0
  119. package/dist/web/assets/javascript-ySlJ1b_l.js +1 -0
  120. package/dist/web/assets/jinja-DGy0s7-h.js +1 -0
  121. package/dist/web/assets/jison-BqZprYcd.js +1 -0
  122. package/dist/web/assets/json-BQoSv7ci.js +1 -0
  123. package/dist/web/assets/json5-w8dY5SsB.js +1 -0
  124. package/dist/web/assets/jsonc-TU54ms6u.js +1 -0
  125. package/dist/web/assets/jsonl-DREVFZK8.js +1 -0
  126. package/dist/web/assets/jsonnet-BfivnA6A.js +1 -0
  127. package/dist/web/assets/{jspdf.es.min-C06OvDJX.js → jspdf.es.min-BFQufOcQ.js} +3 -3
  128. package/dist/web/assets/jssm-P4WzXJd0.js +1 -0
  129. package/dist/web/assets/jsx-BAng5TT0.js +1 -0
  130. package/dist/web/assets/julia-BBuGR-5E.js +1 -0
  131. package/dist/web/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  132. package/dist/web/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  133. package/dist/web/assets/kanagawa-wave-DWedfzmr.js +1 -0
  134. package/dist/web/assets/kotlin-B5lbUyaz.js +1 -0
  135. package/dist/web/assets/kusto-mebxcVVE.js +1 -0
  136. package/dist/web/assets/laserwave-DUszq2jm.js +1 -0
  137. package/dist/web/assets/latex-C-cWTeAZ.js +1 -0
  138. package/dist/web/assets/lean-XBlWyCtg.js +1 -0
  139. package/dist/web/assets/less-BfCpw3nA.js +1 -0
  140. package/dist/web/assets/light-plus-B7mTdjB0.js +1 -0
  141. package/dist/web/assets/liquid-D3W5UaiH.js +1 -0
  142. package/dist/web/assets/log-Cc5clBb7.js +1 -0
  143. package/dist/web/assets/logo-IuBKFhSY.js +1 -0
  144. package/dist/web/assets/lua-CvWAzNxB.js +1 -0
  145. package/dist/web/assets/luau-Du5NY7AG.js +1 -0
  146. package/dist/web/assets/make-Bvotw-X0.js +1 -0
  147. package/dist/web/assets/markdown-DK_1WFMa.js +1 -0
  148. package/dist/web/assets/markdown-UIAJJxZW.js +1 -0
  149. package/dist/web/assets/marko-z0MBrx5-.js +1 -0
  150. package/dist/web/assets/material-theme-D5KoaKCx.js +1 -0
  151. package/dist/web/assets/material-theme-darker-BfHTSMKl.js +1 -0
  152. package/dist/web/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  153. package/dist/web/assets/material-theme-ocean-CyktbL80.js +1 -0
  154. package/dist/web/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  155. package/dist/web/assets/matlab-D9-PGadD.js +1 -0
  156. package/dist/web/assets/mdc-DB_EDNY_.js +1 -0
  157. package/dist/web/assets/mdx-sdHcTMYB.js +1 -0
  158. package/dist/web/assets/mermaid-Ci6OQyBP.js +1 -0
  159. package/dist/web/assets/min-dark-CafNBF8u.js +1 -0
  160. package/dist/web/assets/min-light-CTRr51gU.js +1 -0
  161. package/dist/web/assets/mipsasm-BC5c_5Pe.js +1 -0
  162. package/dist/web/assets/mojo-Tz6hzZYG.js +1 -0
  163. package/dist/web/assets/monokai-D4h5O-jR.js +1 -0
  164. package/dist/web/assets/move-DB_GagMm.js +1 -0
  165. package/dist/web/assets/narrat-DLbgOhZU.js +1 -0
  166. package/dist/web/assets/nextflow-B0XVJmRM.js +1 -0
  167. package/dist/web/assets/nginx-D_VnBJ67.js +1 -0
  168. package/dist/web/assets/night-owl-C39BiMTA.js +1 -0
  169. package/dist/web/assets/nim-ZlGxZxc3.js +1 -0
  170. package/dist/web/assets/nix-shcSOmrb.js +1 -0
  171. package/dist/web/assets/nord-Ddv68eIx.js +1 -0
  172. package/dist/web/assets/nushell-D4Tzg5kh.js +1 -0
  173. package/dist/web/assets/objective-c-Deuh7S70.js +1 -0
  174. package/dist/web/assets/objective-cpp-BUEGK8hf.js +1 -0
  175. package/dist/web/assets/ocaml-BNioltXt.js +1 -0
  176. package/dist/web/assets/one-dark-pro-GBQ2dnAY.js +1 -0
  177. package/dist/web/assets/one-light-PoHY5YXO.js +1 -0
  178. package/dist/web/assets/pascal-JqZropPD.js +1 -0
  179. package/dist/web/assets/perl-CHQXSrWU.js +1 -0
  180. package/dist/web/assets/php-B5ebYQev.js +1 -0
  181. package/dist/web/assets/plastic-3e1v2bzS.js +1 -0
  182. package/dist/web/assets/plsql-LKU2TuZ1.js +1 -0
  183. package/dist/web/assets/po-BFLt1xDp.js +1 -0
  184. package/dist/web/assets/poimandres-CS3Unz2-.js +1 -0
  185. package/dist/web/assets/polar-DKykz6zU.js +1 -0
  186. package/dist/web/assets/postcss-B3ZDOciz.js +1 -0
  187. package/dist/web/assets/powerquery-CSHBycmS.js +1 -0
  188. package/dist/web/assets/powershell-BIEUsx6d.js +1 -0
  189. package/dist/web/assets/prisma-B48N-Iqd.js +1 -0
  190. package/dist/web/assets/prolog-BY-TUvya.js +1 -0
  191. package/dist/web/assets/proto-zocC4JxJ.js +1 -0
  192. package/dist/web/assets/pug-CM9l7STV.js +1 -0
  193. package/dist/web/assets/puppet-Cza_XSSt.js +1 -0
  194. package/dist/web/assets/purescript-Bg-kzb6g.js +1 -0
  195. package/dist/web/assets/python-DhUJRlN_.js +1 -0
  196. package/dist/web/assets/qml-D8XfuvdV.js +1 -0
  197. package/dist/web/assets/qmldir-C8lEn-DE.js +1 -0
  198. package/dist/web/assets/qss-DhMKtDLN.js +1 -0
  199. package/dist/web/assets/r-CwjWoCRV.js +1 -0
  200. package/dist/web/assets/racket-CzouJOBO.js +1 -0
  201. package/dist/web/assets/raku-B1bQXN8T.js +1 -0
  202. package/dist/web/assets/razor-CNLDkMZG.js +1 -0
  203. package/dist/web/assets/red-bN70gL4F.js +1 -0
  204. package/dist/web/assets/reg-5LuOXUq_.js +1 -0
  205. package/dist/web/assets/regexp-DWJ3fJO_.js +1 -0
  206. package/dist/web/assets/rel-DJlmqQ1C.js +1 -0
  207. package/dist/web/assets/riscv-QhoSD0DR.js +1 -0
  208. package/dist/web/assets/rose-pine-CmCqftbK.js +1 -0
  209. package/dist/web/assets/rose-pine-dawn-Ds-gbosJ.js +1 -0
  210. package/dist/web/assets/rose-pine-moon-CjDtw9vr.js +1 -0
  211. package/dist/web/assets/rst-4NLicBqY.js +1 -0
  212. package/dist/web/assets/ruby-DeZ3UC14.js +1 -0
  213. package/dist/web/assets/rust-Be6lgOlo.js +1 -0
  214. package/dist/web/assets/sas-BmTFh92c.js +1 -0
  215. package/dist/web/assets/sass-BJ4Li9vH.js +1 -0
  216. package/dist/web/assets/scala-DQVVAn-B.js +1 -0
  217. package/dist/web/assets/scheme-BJGe-b2p.js +1 -0
  218. package/dist/web/assets/scss-C31hgJw-.js +1 -0
  219. package/dist/web/assets/sdbl-BLhTXw86.js +1 -0
  220. package/dist/web/assets/shaderlab-B7qAK45m.js +1 -0
  221. package/dist/web/assets/shellscript-atvbtKCR.js +1 -0
  222. package/dist/web/assets/shellsession-C_rIy8kc.js +1 -0
  223. package/dist/web/assets/slack-dark-BthQWCQV.js +1 -0
  224. package/dist/web/assets/slack-ochin-DqwNpetd.js +1 -0
  225. package/dist/web/assets/smalltalk-DkLiglaE.js +1 -0
  226. package/dist/web/assets/snazzy-light-Bw305WKR.js +1 -0
  227. package/dist/web/assets/solarized-dark-DXbdFlpD.js +1 -0
  228. package/dist/web/assets/solarized-light-L9t79GZl.js +1 -0
  229. package/dist/web/assets/solidity-C1w2a3ep.js +1 -0
  230. package/dist/web/assets/soy-C-lX7w71.js +1 -0
  231. package/dist/web/assets/sparql-bYkjHRlG.js +1 -0
  232. package/dist/web/assets/splunk-Cf8iN4DR.js +1 -0
  233. package/dist/web/assets/sql-COK4E0Yg.js +1 -0
  234. package/dist/web/assets/ssh-config-BknIz3MU.js +1 -0
  235. package/dist/web/assets/stata-DorPZHa4.js +1 -0
  236. package/dist/web/assets/stylus-BeQkCIfX.js +1 -0
  237. package/dist/web/assets/svelte-MSaWC3Je.js +1 -0
  238. package/dist/web/assets/swift-BSxZ-RaX.js +1 -0
  239. package/dist/web/assets/synthwave-84-CbfX1IO0.js +1 -0
  240. package/dist/web/assets/system-verilog-C7L56vO4.js +1 -0
  241. package/dist/web/assets/systemd-CUnW07Te.js +1 -0
  242. package/dist/web/assets/talonscript-C1XDQQGZ.js +1 -0
  243. package/dist/web/assets/tasl-CQjiPCtT.js +1 -0
  244. package/dist/web/assets/tcl-DQ1-QYvQ.js +1 -0
  245. package/dist/web/assets/templ-dwX3ZSMB.js +1 -0
  246. package/dist/web/assets/terraform-BbSNqyBO.js +1 -0
  247. package/dist/web/assets/tex-rYs2v40G.js +1 -0
  248. package/dist/web/assets/tokyo-night-DBQeEorK.js +1 -0
  249. package/dist/web/assets/toml-CB2ApiWb.js +1 -0
  250. package/dist/web/assets/ts-tags-CipyTH0X.js +1 -0
  251. package/dist/web/assets/tsv-B_m7g4N7.js +1 -0
  252. package/dist/web/assets/tsx-B6W0miNI.js +1 -0
  253. package/dist/web/assets/turtle-BMR_PYu6.js +1 -0
  254. package/dist/web/assets/twig-NC5TFiHP.js +1 -0
  255. package/dist/web/assets/typescript-Dj6nwHGl.js +1 -0
  256. package/dist/web/assets/typespec-BpWG_bgh.js +1 -0
  257. package/dist/web/assets/typst-BVUVsWT6.js +1 -0
  258. package/dist/web/assets/v-CAQ2eGtk.js +1 -0
  259. package/dist/web/assets/vala-BFOHcciG.js +1 -0
  260. package/dist/web/assets/vb-CdO5JTpU.js +1 -0
  261. package/dist/web/assets/verilog-CJaU5se_.js +1 -0
  262. package/dist/web/assets/vesper-BEBZ7ncR.js +1 -0
  263. package/dist/web/assets/vhdl-DYoNaHQp.js +1 -0
  264. package/dist/web/assets/viml-m4uW47V2.js +1 -0
  265. package/dist/web/assets/vitesse-black-Bkuqu6BP.js +1 -0
  266. package/dist/web/assets/vitesse-dark-D0r3Knsf.js +1 -0
  267. package/dist/web/assets/vitesse-light-CVO1_9PV.js +1 -0
  268. package/dist/web/assets/vue-BuYVFjOK.js +1 -0
  269. package/dist/web/assets/vue-html-xdeiXROB.js +1 -0
  270. package/dist/web/assets/vyper-nyqBNV6O.js +1 -0
  271. package/dist/web/assets/wasm-C6j12Q_x.js +1 -0
  272. package/dist/web/assets/wasm-CG6Dc4jp.js +1 -0
  273. package/dist/web/assets/wenyan-7A4Fjokl.js +1 -0
  274. package/dist/web/assets/wgsl-CB0Krxn9.js +1 -0
  275. package/dist/web/assets/wikitext-DCE3LsBG.js +1 -0
  276. package/dist/web/assets/wolfram-C3FkfJm5.js +1 -0
  277. package/dist/web/assets/xml-e3z08dGr.js +1 -0
  278. package/dist/web/assets/xsl-Dd0NUgwM.js +1 -0
  279. package/dist/web/assets/yaml-CVw76BM1.js +1 -0
  280. package/dist/web/assets/zenscript-HnGAYVZD.js +1 -0
  281. package/dist/web/assets/zig-BVz_zdnA.js +1 -0
  282. package/dist/web/index.html +2 -2
  283. package/examples/component-showcase/README.md +29 -0
  284. package/examples/component-showcase/flow.json +40 -0
  285. package/examples/component-showcase/nodes/chart/spec.json +66 -0
  286. package/examples/component-showcase/nodes/counter/spec.json +57 -0
  287. package/examples/component-showcase/nodes/fetcher/actions/refresh.ts +35 -0
  288. package/examples/component-showcase/nodes/fetcher/spec.json +68 -0
  289. package/examples/component-showcase/nodes/form/spec.json +87 -0
  290. package/examples/component-showcase/package.json +6 -0
  291. package/examples/component-showcase/style.json +28 -0
  292. package/package.json +2 -1
  293. package/src/api.ts +54 -0
  294. package/src/cli.ts +1 -0
  295. package/src/component-action-runner.ts +188 -0
  296. package/src/component-spec-resolver.ts +60 -0
  297. package/src/layout.ts +1 -0
  298. package/src/merge.ts +4 -0
  299. package/src/operations.ts +43 -4
  300. package/src/schema.ts +120 -2
  301. package/src/watcher.ts +39 -3
  302. package/dist/web/assets/index-CwfFCUzZ.css +0 -1
  303. package/dist/web/assets/index-DL6aaddE.js +0 -7838
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Dispatches a component node's `script`-kind ComponentAction over HTTP:
3
+ * resolves `scriptPath` under `<cwd>/nodes/<nodeId>/` with the same realpath
4
+ * escape check used by `runPlay` (see proxy.ts), spawns the interpreter via the
5
+ * injectable `ProcessSpawner` seam, pipes the request payload to stdin as JSON,
6
+ * and parses stdout back as JSON (falling back to the raw string).
7
+ *
8
+ * `set`-kind actions are intentionally rejected with statusHint 400: those
9
+ * mutate canvas state locally and never round-trip through the API. The runner
10
+ * is the single seam the API route calls; HTTP status mapping lives in the
11
+ * route handler via `statusHint`.
12
+ */
13
+
14
+ import { realpathSync } from 'node:fs';
15
+ import { join, resolve, sep } from 'node:path';
16
+ import type { EventBus } from './events.ts';
17
+ import { type ProcessSpawner, type SpawnHandle, defaultProcessSpawner } from './process-spawner.ts';
18
+ import type { ComponentAction } from './schema.ts';
19
+ import { shortId } from './short-id.ts';
20
+
21
+ const DEFAULT_TIMEOUT_MS = 5_000;
22
+ const SIGKILL_GRACE_MS = 2_000;
23
+ const SCRIPT_PATH_ESCAPE = 'scriptPath escapes node root';
24
+ const SET_KIND_REJECTION = 'Only script actions are dispatched over HTTP';
25
+
26
+ export interface RunComponentActionOptions {
27
+ events: EventBus;
28
+ flowId: string;
29
+ nodeId: string;
30
+ /** Project root (`<repoPath>`). Script resolves under `<cwd>/nodes/<nodeId>/`. */
31
+ cwd: string;
32
+ actionName: string;
33
+ action: ComponentAction;
34
+ payload: unknown;
35
+ /** Injectable for tests; defaults to `defaultProcessSpawner`. */
36
+ spawner?: ProcessSpawner;
37
+ }
38
+
39
+ export interface ComponentActionResult {
40
+ ok: boolean;
41
+ body?: unknown;
42
+ error?: string;
43
+ /** Suggested HTTP status for the API handler. */
44
+ statusHint: number;
45
+ }
46
+
47
+ type Resolved = { ok: true; absPath: string } | { ok: false };
48
+
49
+ // Resolve `<cwd>/nodes/<nodeId>/<scriptPath>` and verify via realpath it stays
50
+ // inside the node folder. Mirrors proxy.ts:resolveScript — symlink-escape
51
+ // defense in line with `resolveProjectFile` in api.ts.
52
+ function resolveScript(cwd: string, nodeId: string, scriptPath: string): Resolved {
53
+ const nodeRoot = join(cwd, 'nodes', nodeId);
54
+ let realRoot: string;
55
+ try {
56
+ realRoot = realpathSync(nodeRoot);
57
+ } catch {
58
+ return { ok: false };
59
+ }
60
+ const target = resolve(nodeRoot, scriptPath);
61
+ let realTarget: string;
62
+ try {
63
+ realTarget = realpathSync(target);
64
+ } catch {
65
+ return { ok: false };
66
+ }
67
+ const rootWithSep = realRoot.endsWith(sep) ? realRoot : realRoot + sep;
68
+ if (realTarget !== realRoot && !realTarget.startsWith(rootWithSep)) {
69
+ return { ok: false };
70
+ }
71
+ return { ok: true, absPath: realTarget };
72
+ }
73
+
74
+ // Copy `process.env` into a string-only record, then layer the per-run extras.
75
+ // Bun.spawn's env contract is `Record<string, string>` so the undefineds that
76
+ // `process.env` advertises in its type must be filtered out first.
77
+ function buildChildEnv(extra: Record<string, string>): Record<string, string> {
78
+ const env: Record<string, string> = {};
79
+ for (const [k, v] of Object.entries(process.env)) {
80
+ if (typeof v === 'string') env[k] = v;
81
+ }
82
+ return { ...env, ...extra };
83
+ }
84
+
85
+ async function writeStdinPayload(handle: SpawnHandle, payload: unknown): Promise<void> {
86
+ if (!handle.stdin) return;
87
+ const writer = handle.stdin.getWriter();
88
+ try {
89
+ await writer.write(new TextEncoder().encode(JSON.stringify(payload)));
90
+ } finally {
91
+ await writer.close().catch(() => {
92
+ /* stdin already closed by child — not fatal */
93
+ });
94
+ }
95
+ }
96
+
97
+ async function killWithGrace(handle: SpawnHandle): Promise<void> {
98
+ handle.kill('SIGTERM');
99
+ let graceTimer: ReturnType<typeof setTimeout> | undefined;
100
+ const gracePromise = new Promise<'grace'>((res) => {
101
+ graceTimer = setTimeout(() => res('grace'), SIGKILL_GRACE_MS);
102
+ });
103
+ const winner = await Promise.race([handle.exited.then(() => 'exited' as const), gracePromise]);
104
+ if (graceTimer) clearTimeout(graceTimer);
105
+ if (winner === 'grace') {
106
+ handle.kill('SIGKILL');
107
+ await handle.exited;
108
+ }
109
+ }
110
+
111
+ export async function runComponentAction(
112
+ opts: RunComponentActionOptions,
113
+ ): Promise<ComponentActionResult> {
114
+ if (opts.action.kind !== 'script') {
115
+ return { ok: false, error: SET_KIND_REJECTION, statusHint: 400 };
116
+ }
117
+ const spawner = opts.spawner ?? defaultProcessSpawner;
118
+ const resolved = resolveScript(opts.cwd, opts.nodeId, opts.action.scriptPath);
119
+ if (!resolved.ok) {
120
+ return { ok: false, error: SCRIPT_PATH_ESCAPE, statusHint: 400 };
121
+ }
122
+
123
+ const env = buildChildEnv({
124
+ SEEFLOW_DEMO_ID: opts.flowId,
125
+ SEEFLOW_NODE_ID: opts.nodeId,
126
+ SEEFLOW_ACTION_NAME: opts.actionName,
127
+ SEEFLOW_RUN_ID: shortId(),
128
+ });
129
+
130
+ let handle: SpawnHandle;
131
+ try {
132
+ handle = spawner.spawn({
133
+ cmd: [opts.action.interpreter, ...(opts.action.args ?? []), resolved.absPath],
134
+ cwd: opts.cwd,
135
+ env,
136
+ stdin: 'pipe',
137
+ });
138
+ } catch (err) {
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ return { ok: false, error: message, statusHint: 500 };
141
+ }
142
+
143
+ // Drain stdout AND stderr CONCURRENTLY with the process running so OS pipe
144
+ // buffers (~64 KB) don't fill up and deadlock the child.
145
+ const stdoutPromise = new Response(handle.stdout).text();
146
+ const stderrPromise = new Response(handle.stderr).text();
147
+
148
+ // Write stdin and close BEFORE awaiting exit (otherwise a child blocked on
149
+ // `read(stdin)` and a parent blocked on `exited` deadlock each other).
150
+ await writeStdinPayload(handle, opts.payload);
151
+
152
+ const timeoutMs = opts.action.timeoutMs ?? DEFAULT_TIMEOUT_MS;
153
+ let timer: ReturnType<typeof setTimeout> | undefined;
154
+ const timeoutPromise = new Promise<'timeout'>((res) => {
155
+ timer = setTimeout(() => res('timeout'), timeoutMs);
156
+ });
157
+ const exitPromise = handle.exited.then((code) => ({ code }) as const);
158
+
159
+ const race = await Promise.race([exitPromise, timeoutPromise]);
160
+ if (timer) clearTimeout(timer);
161
+
162
+ if (race === 'timeout') {
163
+ await killWithGrace(handle);
164
+ await Promise.allSettled([stdoutPromise, stderrPromise]);
165
+ return {
166
+ ok: false,
167
+ error: `action timed out after ${timeoutMs}ms`,
168
+ statusHint: 504,
169
+ };
170
+ }
171
+
172
+ const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
173
+ if (race.code !== 0) {
174
+ return {
175
+ ok: false,
176
+ error: stderr.trim() || `exit ${race.code}`,
177
+ statusHint: 500,
178
+ };
179
+ }
180
+
181
+ let body: unknown;
182
+ try {
183
+ body = JSON.parse(stdout);
184
+ } catch {
185
+ body = stdout;
186
+ }
187
+ return { ok: true, body, statusHint: 200 };
188
+ }
@@ -0,0 +1,60 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { ResolvedFlow } from './schema.ts';
4
+
5
+ export interface SpecInlineError {
6
+ /** Logical path into the merged flow shape, like 'nodes/<id>/data/spec'. */
7
+ path: string;
8
+ message: string;
9
+ }
10
+
11
+ export interface InlineComponentSpecsResult {
12
+ flow: ResolvedFlow;
13
+ errors: SpecInlineError[];
14
+ /** Project-root-relative paths the watcher should track for live reload. */
15
+ refs: string[];
16
+ }
17
+
18
+ /**
19
+ * For every `'component'` node in `flow`, read `nodes/<id>/spec.json`,
20
+ * JSON.parse, and attach the result as `data.spec`. Missing files surface
21
+ * as a SpecInlineError; malformed JSON surfaces likewise. Non-component
22
+ * nodes pass through untouched.
23
+ *
24
+ * Returns a NEW flow object (no mutation of the input) so the watcher's
25
+ * snapshot caching stays safe.
26
+ */
27
+ export function inlineComponentSpecs(
28
+ flow: ResolvedFlow,
29
+ projectRoot: string,
30
+ ): InlineComponentSpecsResult {
31
+ const errors: SpecInlineError[] = [];
32
+ const refs: string[] = [];
33
+
34
+ const nodes = flow.nodes.map((node) => {
35
+ if (node.type !== 'component') return node;
36
+ const relPath = `nodes/${node.id}/spec.json`;
37
+ const absPath = join(projectRoot, relPath);
38
+ if (!existsSync(absPath)) {
39
+ errors.push({
40
+ path: `nodes/${node.id}/data/spec`,
41
+ message: `Missing spec file: ${relPath}`,
42
+ });
43
+ return node;
44
+ }
45
+ let parsed: unknown;
46
+ try {
47
+ parsed = JSON.parse(readFileSync(absPath, 'utf8'));
48
+ } catch (err) {
49
+ errors.push({
50
+ path: `nodes/${node.id}/data/spec`,
51
+ message: `Invalid JSON in ${relPath}: ${err instanceof Error ? err.message : String(err)}`,
52
+ });
53
+ return node;
54
+ }
55
+ refs.push(relPath);
56
+ return { ...node, data: { ...node.data, spec: parsed } } as typeof node;
57
+ });
58
+
59
+ return { flow: { ...flow, nodes } as ResolvedFlow, errors, refs };
60
+ }
package/src/layout.ts CHANGED
@@ -76,6 +76,7 @@ const DEFAULT_DIMENSIONS: Record<FlowNode['type'], { width: number; height: numb
76
76
  image: { width: 200, height: 150 },
77
77
  html: { width: 320, height: 200 },
78
78
  icon: { width: 80, height: 80 },
79
+ component: { width: 320, height: 240 },
79
80
  };
80
81
 
81
82
  // Sticky / text variants are floating annotations. They never participate in
package/src/merge.ts CHANGED
@@ -133,6 +133,10 @@ export function splitFlow(resolved: {
133
133
  const flowData: Record<string, unknown> = {};
134
134
  for (const [k, v] of Object.entries(data)) {
135
135
  if (v === undefined) continue;
136
+ // Component nodes externalize `spec` to <project>/nodes/<id>/spec.json
137
+ // (the sidecar). Drop it from flow.json so the strict on-disk schema
138
+ // doesn't reject it and the spec stays single-sourced on disk.
139
+ if (node.type === 'component' && k === 'spec') continue;
136
140
  if (NODE_DATA_FLOW_KEYS.has(k)) {
137
141
  flowData[k] = v;
138
142
  } else if (NODE_STYLE_KEYS.has(k)) {
package/src/operations.ts CHANGED
@@ -11,6 +11,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSyn
11
11
  import { dirname, isAbsolute, join } from 'node:path';
12
12
  import { type ZodIssue, z } from 'zod';
13
13
  import { writeFileAtomic } from './atomic-write.ts';
14
+ import { inlineComponentSpecs } from './component-spec-resolver.ts';
14
15
  import { type LayoutOptions, computeLayout } from './layout.ts';
15
16
  import { mergeFlowAndStyle, splitFlow } from './merge.ts';
16
17
  import {
@@ -24,6 +25,7 @@ import {
24
25
  import type { Registry } from './registry.ts';
25
26
  import {
26
27
  ColorTokenSchema,
28
+ ComponentSpecSchema,
27
29
  EdgePinSchema,
28
30
  type Flow,
29
31
  FlowSchema,
@@ -141,6 +143,12 @@ export const NodePatchBodySchema = z
141
143
  playAction: PlayActionSchema.optional(),
142
144
  statusAction: StatusActionSchema.optional(),
143
145
  stateSource: StateSourceSchema.optional(),
146
+ // type:'component'-only: json-render spec describing the reactive UI.
147
+ // Externalized to `<project>/nodes/<id>/spec.json` by patchNodeImpl; the
148
+ // in-memory ResolvedFlow keeps `data.spec` populated for the post-merge
149
+ // reparse + SSE broadcast, but splitFlow strips it from flow.json so the
150
+ // sidecar is the source of truth on disk.
151
+ spec: ComponentSpecSchema.optional(),
144
152
  })
145
153
  .strict();
146
154
  export type NodePatchBody = z.infer<typeof NodePatchBodySchema>;
@@ -172,6 +180,7 @@ const NODE_DATA_PATCH_KEYS = [
172
180
  'playAction',
173
181
  'statusAction',
174
182
  'stateSource',
183
+ 'spec',
175
184
  ] as const satisfies ReadonlyArray<keyof NodePatchBody>;
176
185
 
177
186
  const EXTERNALIZED_FIELD_NAMES = new Set<string>(EXTERNALIZED_NODE_FIELDS.map((e) => e.field));
@@ -238,6 +247,10 @@ const SEMANTIC_KEYS_BY_TYPE: Record<z.infer<typeof NodeTypeSchema>, ReadonlySet<
238
247
  'statusAction',
239
248
  'alt',
240
249
  ]),
250
+ // Component nodes externalize `spec` to <project>/nodes/<id>/spec.json; the
251
+ // semantic-key set covers only the universal capability fields so retype
252
+ // never drags `spec` through `data`. (US-007 wires the sidecar writer.)
253
+ component: GEOMETRIC_SEMANTIC_KEYS,
241
254
  };
242
255
 
243
256
  // Visual data keys — routed to style.json on write by splitFlow. Kept here
@@ -321,12 +334,13 @@ export const mergeNodeUpdates = (node: Record<string, unknown>, updates: NodePat
321
334
  }
322
335
  }
323
336
 
324
- // type:'html'-only invariant enforcement:
337
+ // type:'html' + type:'component' invariant enforcement:
325
338
  // autoSize === true ⊻ (width and height set).
326
339
  // autoSize: true is the dominant signal — it strips width/height even if
327
340
  // the same patch tried to write them. Writing width/height implicitly
328
- // flips autoSize to false.
329
- if (node.type === 'html') {
341
+ // flips autoSize to false. Both node types default to autoSize:true in the
342
+ // renderer and share the same shrink-wrap-to-content mechanism.
343
+ if (node.type === 'html' || node.type === 'component') {
330
344
  // The autoSize invariant requires `width`/`height` to be ABSENT from the
331
345
  // serialized JSON when autoSize is true — not present with value
332
346
  // `undefined` (which would serialize as a stray `"width": null` or get
@@ -757,7 +771,16 @@ export async function mutateMergedFlow<E extends { kind: string }>(
757
771
  const styleParse = StyleSchema.safeParse(read.rawStyle);
758
772
  if (!styleParse.success) return { kind: 'badSchema', issues: styleParse.error.issues };
759
773
 
760
- const merged = mergeFlowAndStyle(flowParse.data, styleParse.data) as unknown as {
774
+ // Inline component spec sidecars (<project>/nodes/<id>/spec.json) before the
775
+ // mutator runs so the post-mutation ResolvedFlowSchema parse sees `data.spec`
776
+ // on every existing component node. splitFlow strips `spec` back out before
777
+ // we write flow.json, keeping the sidecar as the on-disk source of truth.
778
+ const projectRoot = dirname(flowPath);
779
+ const { flow: inlinedFlow } = inlineComponentSpecs(
780
+ mergeFlowAndStyle(flowParse.data, styleParse.data),
781
+ projectRoot,
782
+ );
783
+ const merged = inlinedFlow as unknown as {
761
784
  version: number;
762
785
  name: string;
763
786
  resetAction?: unknown;
@@ -1532,6 +1555,22 @@ export async function patchNodeImpl(
1532
1555
  }
1533
1556
  node.data = data;
1534
1557
  }
1558
+ // Component spec sidecar — write the pretty-printed JSON to
1559
+ // `<project>/nodes/<id>/spec.json` so the on-disk source of truth stays
1560
+ // in sync. mergeNodeUpdates already put data.spec on the merged tree for
1561
+ // the post-mutation ResolvedFlowSchema parse; splitFlow strips it from
1562
+ // flow.json so we don't double-store the spec.
1563
+ if (node.type === 'component' && updates.spec !== undefined) {
1564
+ const specAbs = nodeFileAbsPath(entry.repoPath, nodeId, 'spec.json');
1565
+ try {
1566
+ writeNodeFile(specAbs, `${JSON.stringify(updates.spec, null, 2)}\n`);
1567
+ } catch (err) {
1568
+ return {
1569
+ kind: 'writeFailed',
1570
+ message: err instanceof Error ? err.message : String(err),
1571
+ };
1572
+ }
1573
+ }
1535
1574
  return { kind: 'ok' };
1536
1575
  });
1537
1576
  }
package/src/schema.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { COMPONENT_NAMES, componentCatalog } from '@seeflow/canvas/catalog';
1
2
  import { z } from 'zod';
2
3
 
3
4
  const PositionSchema = z.object({
@@ -67,7 +68,7 @@ const isCleanRelativePath = (s: string): boolean => {
67
68
  // interpreter; `input` (optional) gets JSON-serialized and written to the
68
69
  // child's stdin then closed; `timeoutMs` caps execution (default applied at
69
70
  // the spawn layer, not here).
70
- const ScriptActionSchema = z.object({
71
+ export const ScriptActionSchema = z.object({
71
72
  kind: z.literal('script'),
72
73
  interpreter: z.string().min(1),
73
74
  args: z.array(z.string()).optional(),
@@ -140,7 +141,58 @@ export const GEOMETRIC_NODE_TYPES = [
140
141
  'cloud',
141
142
  ] as const;
142
143
 
143
- export const NodeTypeSchema = z.enum([...GEOMETRIC_NODE_TYPES, 'image', 'html', 'icon']);
144
+ export const NodeTypeSchema = z.enum([
145
+ ...GEOMETRIC_NODE_TYPES,
146
+ 'image',
147
+ 'html',
148
+ 'icon',
149
+ 'component',
150
+ ]);
151
+
152
+ // --- Component node spec/action schemas --------------------------------------
153
+ // The 'component' node renders a json-render-driven reactive UI on the canvas.
154
+ // `spec` is the source of truth for layout + interactivity; on disk it lives at
155
+ // `<project>/nodes/<id>/spec.json` (the resolver inlines it into data.spec for
156
+ // ResolvedFlowSchema). Element types and props are catalog-validated by a
157
+ // superRefine wired in a later story.
158
+
159
+ export const ComponentSpecElementSchema = z.object({
160
+ type: z.string().min(1),
161
+ props: z.record(z.string(), z.unknown()).optional(),
162
+ children: z.array(z.string()).optional(),
163
+ watch: z.record(z.string(), z.unknown()).optional(),
164
+ });
165
+
166
+ // Declarative state mutation. `path` is a JSON Pointer (starts with '/');
167
+ // `value` may itself carry { $param } / { $state } refs resolved by the
168
+ // runtime at dispatch time.
169
+ const SetActionSchema = z.object({
170
+ kind: z.literal('set'),
171
+ path: z
172
+ .string()
173
+ .min(1)
174
+ .startsWith('/', { message: 'path must be a JSON Pointer (start with /)' }),
175
+ value: z.unknown(),
176
+ });
177
+
178
+ // Script-kind component actions reuse the existing ScriptActionSchema shape
179
+ // (interpreter, scriptPath, timeoutMs, ...). The action runner roots scriptPath
180
+ // under `<projectRoot>/nodes/<nodeId>/`.
181
+ export const ComponentActionSchema = z.discriminatedUnion('kind', [
182
+ SetActionSchema,
183
+ ScriptActionSchema,
184
+ ]);
185
+
186
+ export const ComponentSpecSchema = z.object({
187
+ root: z.string().min(1),
188
+ elements: z.record(z.string(), ComponentSpecElementSchema),
189
+ state: z.record(z.string(), z.unknown()).optional(),
190
+ actions: z.record(z.string(), ComponentActionSchema).optional(),
191
+ });
192
+
193
+ export type ComponentSpec = z.infer<typeof ComponentSpecSchema>;
194
+ export type ComponentAction = z.infer<typeof ComponentActionSchema>;
195
+ export type ComponentSpecElement = z.infer<typeof ComponentSpecElementSchema>;
144
196
 
145
197
  // ---- Resolved (in-memory) per-type data -------------------------------------
146
198
 
@@ -188,6 +240,18 @@ const ResolvedIconNodeData = z.object({
188
240
  alt: z.string().optional(),
189
241
  });
190
242
 
243
+ // Component node — `spec` is the json-render tree. On disk the spec lives in
244
+ // `<project>/nodes/<id>/spec.json`; the resolver inlines it into data.spec
245
+ // before ResolvedFlowSchema validates the merged shape. The on-disk
246
+ // FlowComponentNodeData below has no `spec` field.
247
+ const ResolvedComponentNodeData = z.object({
248
+ ...NodeSemanticBaseShape,
249
+ ...NodeVisualBaseShape,
250
+ ...NodeCapabilitiesShape,
251
+ spec: ComponentSpecSchema,
252
+ autoSize: z.boolean().optional(),
253
+ });
254
+
191
255
  const NodeBaseShape = {
192
256
  id: z.string().min(1),
193
257
  position: PositionSchema,
@@ -213,6 +277,11 @@ const NodeSchema = z.discriminatedUnion('type', [
213
277
  z.object({ ...NodeBaseShape, type: z.literal('image'), data: ResolvedImageNodeData }),
214
278
  z.object({ ...NodeBaseShape, type: z.literal('html'), data: ResolvedHtmlNodeData }),
215
279
  z.object({ ...NodeBaseShape, type: z.literal('icon'), data: ResolvedIconNodeData }),
280
+ z.object({
281
+ ...NodeBaseShape,
282
+ type: z.literal('component'),
283
+ data: ResolvedComponentNodeData,
284
+ }),
216
285
  ]);
217
286
 
218
287
  // Connector — unchanged by the flat-types refactor.
@@ -307,6 +376,34 @@ export const ResolvedFlowSchema = z
307
376
  });
308
377
  }
309
378
  });
379
+ // type:'component' spec.elements entries are catalog-validated here so
380
+ // unknown component names and prop shape mismatches surface at flow-read
381
+ // time with paths pointing into the offending element.
382
+ resolved.nodes.forEach((node, idx) => {
383
+ if (node.type !== 'component') return;
384
+ const elements = node.data.spec.elements;
385
+ for (const [elId, entry] of Object.entries(elements)) {
386
+ const def = componentCatalog.components[entry.type];
387
+ if (!def) {
388
+ ctx.addIssue({
389
+ code: z.ZodIssueCode.custom,
390
+ path: ['nodes', idx, 'data', 'spec', 'elements', elId, 'type'],
391
+ message: `Unknown component type "${entry.type}". Valid names: ${COMPONENT_NAMES.join(', ')}`,
392
+ });
393
+ continue;
394
+ }
395
+ const propsResult = def.props.safeParse(entry.props ?? {});
396
+ if (!propsResult.success) {
397
+ for (const issue of propsResult.error.issues) {
398
+ ctx.addIssue({
399
+ code: z.ZodIssueCode.custom,
400
+ path: ['nodes', idx, 'data', 'spec', 'elements', elId, 'props', ...issue.path],
401
+ message: issue.message,
402
+ });
403
+ }
404
+ }
405
+ }
406
+ });
310
407
  });
311
408
 
312
409
  export type ResolvedFlow = z.infer<typeof ResolvedFlowSchema>;
@@ -365,6 +462,18 @@ const FlowIconNodeData = z
365
462
  })
366
463
  .strict();
367
464
 
465
+ // Component node, on-disk shape. `spec` is intentionally absent — the sidecar
466
+ // `<project>/nodes/<id>/spec.json` is the source of truth. `.strict()` rejects
467
+ // any stray spec field that slips through (the resolver layer is responsible
468
+ // for inlining + the writer for stripping it back out).
469
+ const FlowComponentNodeData = z
470
+ .object({
471
+ ...NodeSemanticBaseShape,
472
+ ...NodeCapabilitiesShape,
473
+ autoSize: z.boolean().optional(),
474
+ })
475
+ .strict();
476
+
368
477
  const FlowNodeBaseShape = {
369
478
  id: z.string().min(1),
370
479
  };
@@ -412,6 +521,14 @@ export const FlowIconNodeSchema = z
412
521
  })
413
522
  .strict();
414
523
 
524
+ export const FlowComponentNodeSchema = z
525
+ .object({
526
+ ...FlowNodeBaseShape,
527
+ type: z.literal('component'),
528
+ data: FlowComponentNodeData,
529
+ })
530
+ .strict();
531
+
415
532
  const FlowNodeSchema = z.discriminatedUnion('type', [
416
533
  FlowRectangleNodeSchema,
417
534
  FlowEllipseNodeSchema,
@@ -425,6 +542,7 @@ const FlowNodeSchema = z.discriminatedUnion('type', [
425
542
  FlowImageNodeSchema,
426
543
  FlowHtmlNodeSchema,
427
544
  FlowIconNodeSchema,
545
+ FlowComponentNodeSchema,
428
546
  ]);
429
547
 
430
548
  const FlowConnectorBaseShape = {
package/src/watcher.ts CHANGED
@@ -5,11 +5,18 @@ import { basename, dirname, isAbsolute, join } from 'node:path';
5
5
  // contains flow.json). The studio is path-agnostic — whatever path the caller
6
6
  // supplied is the project root.
7
7
  const projectRootForFlow = (flowPath: string): string => dirname(flowPath);
8
+ import { inlineComponentSpecs } from './component-spec-resolver.ts';
8
9
  import type { EventBus } from './events.ts';
9
10
  import { resolveFileRefs } from './file-ref.ts';
10
11
  import { mergeFlowAndStyle } from './merge.ts';
11
12
  import type { Registry } from './registry.ts';
12
- import { type Flow, FlowSchema, type ResolvedFlow, StyleSchema } from './schema.ts';
13
+ import {
14
+ type Flow,
15
+ FlowSchema,
16
+ type ResolvedFlow,
17
+ ResolvedFlowSchema,
18
+ StyleSchema,
19
+ } from './schema.ts';
13
20
 
14
21
  const DEFAULT_DEBOUNCE_MS = 100;
15
22
 
@@ -225,8 +232,37 @@ export function readMergedFlow(flowPath: string): ReadMergedFlowResult {
225
232
  };
226
233
  }
227
234
 
228
- const flow = mergeFlowAndStyle(flowParse.data as Flow, styleParse.data);
229
- return { flow, valid: true, error: null, fileRefs: refs, staticRefs };
235
+ const merged = mergeFlowAndStyle(flowParse.data as Flow, styleParse.data);
236
+ const {
237
+ flow: inlined,
238
+ errors: specErrors,
239
+ refs: specRefs,
240
+ } = inlineComponentSpecs(merged, projectRoot);
241
+ const fileRefs = [...refs, ...specRefs];
242
+
243
+ if (specErrors.length > 0) {
244
+ return {
245
+ ...empty,
246
+ error: specErrors.map((e) => `${e.path}: ${e.message}`).join('; '),
247
+ fileRefs,
248
+ staticRefs,
249
+ };
250
+ }
251
+
252
+ const resolvedParse = ResolvedFlowSchema.safeParse(inlined);
253
+ if (!resolvedParse.success) {
254
+ const message = resolvedParse.error.issues
255
+ .map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`)
256
+ .join('; ');
257
+ return {
258
+ ...empty,
259
+ error: `ResolvedFlow validation failed: ${message}`,
260
+ fileRefs,
261
+ staticRefs,
262
+ };
263
+ }
264
+
265
+ return { flow: resolvedParse.data, valid: true, error: null, fileRefs, staticRefs };
230
266
  }
231
267
 
232
268
  const closeFileWatchers = (handle: WatchHandle): void => {