@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.
- package/README.md +4 -0
- package/dist/web/assets/abap-DsBKuouk.js +1 -0
- package/dist/web/assets/actionscript-3-D_z4Izcz.js +1 -0
- package/dist/web/assets/ada-727ZlQH0.js +1 -0
- package/dist/web/assets/andromeeda-C3khCPGq.js +1 -0
- package/dist/web/assets/angular-html-4alyEGLm.js +1 -0
- package/dist/web/assets/angular-ts-BixEUTMq.js +1 -0
- package/dist/web/assets/apache-Dn00JSTd.js +1 -0
- package/dist/web/assets/apex-COJ4H7py.js +1 -0
- package/dist/web/assets/apl-BBq3IX1j.js +1 -0
- package/dist/web/assets/applescript-Bu5BbsvL.js +1 -0
- package/dist/web/assets/ara-7O62HKoU.js +1 -0
- package/dist/web/assets/asciidoc-BPT9niGB.js +1 -0
- package/dist/web/assets/asm-Dhn9LcZ4.js +1 -0
- package/dist/web/assets/astro-CqkE3fuf.js +1 -0
- package/dist/web/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/web/assets/awk-eg146-Ew.js +1 -0
- package/dist/web/assets/ayu-dark-Cv9koXgw.js +1 -0
- package/dist/web/assets/ballerina-Du268qiB.js +1 -0
- package/dist/web/assets/bat-fje9CFhw.js +1 -0
- package/dist/web/assets/beancount-BwXTMy5W.js +1 -0
- package/dist/web/assets/berry-3xVqZejG.js +1 -0
- package/dist/web/assets/bibtex-xW4inM5L.js +1 -0
- package/dist/web/assets/bicep-DHo0CJ0O.js +1 -0
- package/dist/web/assets/blade-a8OxSdnT.js +1 -0
- package/dist/web/assets/bsl-Dgyn0ogV.js +1 -0
- package/dist/web/assets/c-C3t2pwGQ.js +1 -0
- package/dist/web/assets/cadence-DNquZEk8.js +1 -0
- package/dist/web/assets/cairo--RitsXJZ.js +1 -0
- package/dist/web/assets/catppuccin-frappe-CD_QflpE.js +1 -0
- package/dist/web/assets/catppuccin-latte-DRW-0cLl.js +1 -0
- package/dist/web/assets/catppuccin-macchiato-C-_shW-Y.js +1 -0
- package/dist/web/assets/catppuccin-mocha-LGGdnPYs.js +1 -0
- package/dist/web/assets/chart-8DxAnLoD.js +73 -0
- package/dist/web/assets/clarity-BHOwM8T6.js +1 -0
- package/dist/web/assets/clojure-DxSadP1t.js +1 -0
- package/dist/web/assets/cmake-DbXoA79R.js +1 -0
- package/dist/web/assets/cobol-PTqiYgYu.js +1 -0
- package/dist/web/assets/code-block-cx7LPXvE.js +13 -0
- package/dist/web/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/web/assets/codeql-sacFqUAJ.js +1 -0
- package/dist/web/assets/coffee-dyiR41kL.js +1 -0
- package/dist/web/assets/common-lisp-C7gG9l05.js +1 -0
- package/dist/web/assets/coq-Dsg_Bt_b.js +1 -0
- package/dist/web/assets/cpp-BksuvNSY.js +1 -0
- package/dist/web/assets/crystal-DtDmRg-F.js +1 -0
- package/dist/web/assets/csharp-D9R-vmeu.js +1 -0
- package/dist/web/assets/css-BPhBrDlE.js +1 -0
- package/dist/web/assets/csv-B0qRVHPH.js +1 -0
- package/dist/web/assets/cue-DtFQj3wx.js +1 -0
- package/dist/web/assets/cypher-m2LEI-9-.js +1 -0
- package/dist/web/assets/d-BoXegm-a.js +1 -0
- package/dist/web/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/web/assets/dart-B9wLZaAG.js +1 -0
- package/dist/web/assets/dax-ClGRhx96.js +1 -0
- package/dist/web/assets/desktop-DEIpsLCJ.js +1 -0
- package/dist/web/assets/diff-BgYniUM_.js +1 -0
- package/dist/web/assets/docker-COcR7UxN.js +1 -0
- package/dist/web/assets/dotenv-BjQB5zDj.js +1 -0
- package/dist/web/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/web/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/web/assets/dream-maker-C-nORZOA.js +1 -0
- package/dist/web/assets/edge-D5gP-w-T.js +1 -0
- package/dist/web/assets/elixir-CLiX3zqd.js +1 -0
- package/dist/web/assets/elm-CmHSxxaM.js +1 -0
- package/dist/web/assets/emacs-lisp-BX77sIaO.js +1 -0
- package/dist/web/assets/erb-BYTLMnw6.js +1 -0
- package/dist/web/assets/erlang-B-DoSBHF.js +1 -0
- package/dist/web/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/web/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/web/assets/fennel-bCA53EVm.js +1 -0
- package/dist/web/assets/fish-w-ucz2PV.js +1 -0
- package/dist/web/assets/fluent-Dayu4EKP.js +1 -0
- package/dist/web/assets/fortran-fixed-form-TqA4NnZg.js +1 -0
- package/dist/web/assets/fortran-free-form-DKXYxT9g.js +1 -0
- package/dist/web/assets/fsharp-XplgxFYe.js +1 -0
- package/dist/web/assets/gdresource-BHYsBjWJ.js +1 -0
- package/dist/web/assets/gdscript-DfxzS6Rs.js +1 -0
- package/dist/web/assets/gdshader-SKMF96pI.js +1 -0
- package/dist/web/assets/genie-ajMbGru0.js +1 -0
- package/dist/web/assets/gherkin--30QC5Em.js +1 -0
- package/dist/web/assets/git-commit-i4q6IMui.js +1 -0
- package/dist/web/assets/git-rebase-B-v9cOL2.js +1 -0
- package/dist/web/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/web/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/web/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/web/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/web/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/web/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/web/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/web/assets/gleam-B430Bg39.js +1 -0
- package/dist/web/assets/glimmer-js-D-cwc0-E.js +1 -0
- package/dist/web/assets/glimmer-ts-pgjy16dm.js +1 -0
- package/dist/web/assets/glsl-DBO2IWDn.js +1 -0
- package/dist/web/assets/gnuplot-CM8KxXT1.js +1 -0
- package/dist/web/assets/go-B1SYOhNW.js +1 -0
- package/dist/web/assets/graphql-cDcHW_If.js +1 -0
- package/dist/web/assets/groovy-DkBy-JyN.js +1 -0
- package/dist/web/assets/hack-D1yCygmZ.js +1 -0
- package/dist/web/assets/haml-B2EZWmdv.js +1 -0
- package/dist/web/assets/handlebars-BQGss363.js +1 -0
- package/dist/web/assets/haskell-BILxekzW.js +1 -0
- package/dist/web/assets/haxe-C5wWYbrZ.js +1 -0
- package/dist/web/assets/hcl-HzYwdGDm.js +1 -0
- package/dist/web/assets/hjson-T-Tgc4AT.js +1 -0
- package/dist/web/assets/hlsl-ifBTmRxC.js +1 -0
- package/dist/web/assets/houston-DnULxvSX.js +1 -0
- package/dist/web/assets/html-C2L_23MC.js +1 -0
- package/dist/web/assets/html-derivative-CSfWNPLT.js +1 -0
- package/dist/web/assets/http-FRrOvY1W.js +1 -0
- package/dist/web/assets/hxml-TIA70rKU.js +1 -0
- package/dist/web/assets/hy-BMj5Y0dO.js +1 -0
- package/dist/web/assets/imba-bv_oIlVt.js +1 -0
- package/dist/web/assets/index-CeLShda7.css +1 -0
- package/dist/web/assets/index-Dp4QwEl0.js +8608 -0
- package/dist/web/assets/{index.es-CVm3MRo3.js → index.es-C-uXEdZB.js} +1 -1
- package/dist/web/assets/ini-BjABl1g7.js +1 -0
- package/dist/web/assets/java-xI-RfyKK.js +1 -0
- package/dist/web/assets/javascript-ySlJ1b_l.js +1 -0
- package/dist/web/assets/jinja-DGy0s7-h.js +1 -0
- package/dist/web/assets/jison-BqZprYcd.js +1 -0
- package/dist/web/assets/json-BQoSv7ci.js +1 -0
- package/dist/web/assets/json5-w8dY5SsB.js +1 -0
- package/dist/web/assets/jsonc-TU54ms6u.js +1 -0
- package/dist/web/assets/jsonl-DREVFZK8.js +1 -0
- package/dist/web/assets/jsonnet-BfivnA6A.js +1 -0
- package/dist/web/assets/{jspdf.es.min-C06OvDJX.js → jspdf.es.min-BFQufOcQ.js} +3 -3
- package/dist/web/assets/jssm-P4WzXJd0.js +1 -0
- package/dist/web/assets/jsx-BAng5TT0.js +1 -0
- package/dist/web/assets/julia-BBuGR-5E.js +1 -0
- package/dist/web/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/dist/web/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/dist/web/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/dist/web/assets/kotlin-B5lbUyaz.js +1 -0
- package/dist/web/assets/kusto-mebxcVVE.js +1 -0
- package/dist/web/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/web/assets/latex-C-cWTeAZ.js +1 -0
- package/dist/web/assets/lean-XBlWyCtg.js +1 -0
- package/dist/web/assets/less-BfCpw3nA.js +1 -0
- package/dist/web/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/web/assets/liquid-D3W5UaiH.js +1 -0
- package/dist/web/assets/log-Cc5clBb7.js +1 -0
- package/dist/web/assets/logo-IuBKFhSY.js +1 -0
- package/dist/web/assets/lua-CvWAzNxB.js +1 -0
- package/dist/web/assets/luau-Du5NY7AG.js +1 -0
- package/dist/web/assets/make-Bvotw-X0.js +1 -0
- package/dist/web/assets/markdown-DK_1WFMa.js +1 -0
- package/dist/web/assets/markdown-UIAJJxZW.js +1 -0
- package/dist/web/assets/marko-z0MBrx5-.js +1 -0
- package/dist/web/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/web/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/web/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/web/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/web/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/web/assets/matlab-D9-PGadD.js +1 -0
- package/dist/web/assets/mdc-DB_EDNY_.js +1 -0
- package/dist/web/assets/mdx-sdHcTMYB.js +1 -0
- package/dist/web/assets/mermaid-Ci6OQyBP.js +1 -0
- package/dist/web/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/web/assets/min-light-CTRr51gU.js +1 -0
- package/dist/web/assets/mipsasm-BC5c_5Pe.js +1 -0
- package/dist/web/assets/mojo-Tz6hzZYG.js +1 -0
- package/dist/web/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/web/assets/move-DB_GagMm.js +1 -0
- package/dist/web/assets/narrat-DLbgOhZU.js +1 -0
- package/dist/web/assets/nextflow-B0XVJmRM.js +1 -0
- package/dist/web/assets/nginx-D_VnBJ67.js +1 -0
- package/dist/web/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/web/assets/nim-ZlGxZxc3.js +1 -0
- package/dist/web/assets/nix-shcSOmrb.js +1 -0
- package/dist/web/assets/nord-Ddv68eIx.js +1 -0
- package/dist/web/assets/nushell-D4Tzg5kh.js +1 -0
- package/dist/web/assets/objective-c-Deuh7S70.js +1 -0
- package/dist/web/assets/objective-cpp-BUEGK8hf.js +1 -0
- package/dist/web/assets/ocaml-BNioltXt.js +1 -0
- package/dist/web/assets/one-dark-pro-GBQ2dnAY.js +1 -0
- package/dist/web/assets/one-light-PoHY5YXO.js +1 -0
- package/dist/web/assets/pascal-JqZropPD.js +1 -0
- package/dist/web/assets/perl-CHQXSrWU.js +1 -0
- package/dist/web/assets/php-B5ebYQev.js +1 -0
- package/dist/web/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/web/assets/plsql-LKU2TuZ1.js +1 -0
- package/dist/web/assets/po-BFLt1xDp.js +1 -0
- package/dist/web/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/web/assets/polar-DKykz6zU.js +1 -0
- package/dist/web/assets/postcss-B3ZDOciz.js +1 -0
- package/dist/web/assets/powerquery-CSHBycmS.js +1 -0
- package/dist/web/assets/powershell-BIEUsx6d.js +1 -0
- package/dist/web/assets/prisma-B48N-Iqd.js +1 -0
- package/dist/web/assets/prolog-BY-TUvya.js +1 -0
- package/dist/web/assets/proto-zocC4JxJ.js +1 -0
- package/dist/web/assets/pug-CM9l7STV.js +1 -0
- package/dist/web/assets/puppet-Cza_XSSt.js +1 -0
- package/dist/web/assets/purescript-Bg-kzb6g.js +1 -0
- package/dist/web/assets/python-DhUJRlN_.js +1 -0
- package/dist/web/assets/qml-D8XfuvdV.js +1 -0
- package/dist/web/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/web/assets/qss-DhMKtDLN.js +1 -0
- package/dist/web/assets/r-CwjWoCRV.js +1 -0
- package/dist/web/assets/racket-CzouJOBO.js +1 -0
- package/dist/web/assets/raku-B1bQXN8T.js +1 -0
- package/dist/web/assets/razor-CNLDkMZG.js +1 -0
- package/dist/web/assets/red-bN70gL4F.js +1 -0
- package/dist/web/assets/reg-5LuOXUq_.js +1 -0
- package/dist/web/assets/regexp-DWJ3fJO_.js +1 -0
- package/dist/web/assets/rel-DJlmqQ1C.js +1 -0
- package/dist/web/assets/riscv-QhoSD0DR.js +1 -0
- package/dist/web/assets/rose-pine-CmCqftbK.js +1 -0
- package/dist/web/assets/rose-pine-dawn-Ds-gbosJ.js +1 -0
- package/dist/web/assets/rose-pine-moon-CjDtw9vr.js +1 -0
- package/dist/web/assets/rst-4NLicBqY.js +1 -0
- package/dist/web/assets/ruby-DeZ3UC14.js +1 -0
- package/dist/web/assets/rust-Be6lgOlo.js +1 -0
- package/dist/web/assets/sas-BmTFh92c.js +1 -0
- package/dist/web/assets/sass-BJ4Li9vH.js +1 -0
- package/dist/web/assets/scala-DQVVAn-B.js +1 -0
- package/dist/web/assets/scheme-BJGe-b2p.js +1 -0
- package/dist/web/assets/scss-C31hgJw-.js +1 -0
- package/dist/web/assets/sdbl-BLhTXw86.js +1 -0
- package/dist/web/assets/shaderlab-B7qAK45m.js +1 -0
- package/dist/web/assets/shellscript-atvbtKCR.js +1 -0
- package/dist/web/assets/shellsession-C_rIy8kc.js +1 -0
- package/dist/web/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/web/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/web/assets/smalltalk-DkLiglaE.js +1 -0
- package/dist/web/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/web/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/dist/web/assets/solarized-light-L9t79GZl.js +1 -0
- package/dist/web/assets/solidity-C1w2a3ep.js +1 -0
- package/dist/web/assets/soy-C-lX7w71.js +1 -0
- package/dist/web/assets/sparql-bYkjHRlG.js +1 -0
- package/dist/web/assets/splunk-Cf8iN4DR.js +1 -0
- package/dist/web/assets/sql-COK4E0Yg.js +1 -0
- package/dist/web/assets/ssh-config-BknIz3MU.js +1 -0
- package/dist/web/assets/stata-DorPZHa4.js +1 -0
- package/dist/web/assets/stylus-BeQkCIfX.js +1 -0
- package/dist/web/assets/svelte-MSaWC3Je.js +1 -0
- package/dist/web/assets/swift-BSxZ-RaX.js +1 -0
- package/dist/web/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/web/assets/system-verilog-C7L56vO4.js +1 -0
- package/dist/web/assets/systemd-CUnW07Te.js +1 -0
- package/dist/web/assets/talonscript-C1XDQQGZ.js +1 -0
- package/dist/web/assets/tasl-CQjiPCtT.js +1 -0
- package/dist/web/assets/tcl-DQ1-QYvQ.js +1 -0
- package/dist/web/assets/templ-dwX3ZSMB.js +1 -0
- package/dist/web/assets/terraform-BbSNqyBO.js +1 -0
- package/dist/web/assets/tex-rYs2v40G.js +1 -0
- package/dist/web/assets/tokyo-night-DBQeEorK.js +1 -0
- package/dist/web/assets/toml-CB2ApiWb.js +1 -0
- package/dist/web/assets/ts-tags-CipyTH0X.js +1 -0
- package/dist/web/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/web/assets/tsx-B6W0miNI.js +1 -0
- package/dist/web/assets/turtle-BMR_PYu6.js +1 -0
- package/dist/web/assets/twig-NC5TFiHP.js +1 -0
- package/dist/web/assets/typescript-Dj6nwHGl.js +1 -0
- package/dist/web/assets/typespec-BpWG_bgh.js +1 -0
- package/dist/web/assets/typst-BVUVsWT6.js +1 -0
- package/dist/web/assets/v-CAQ2eGtk.js +1 -0
- package/dist/web/assets/vala-BFOHcciG.js +1 -0
- package/dist/web/assets/vb-CdO5JTpU.js +1 -0
- package/dist/web/assets/verilog-CJaU5se_.js +1 -0
- package/dist/web/assets/vesper-BEBZ7ncR.js +1 -0
- package/dist/web/assets/vhdl-DYoNaHQp.js +1 -0
- package/dist/web/assets/viml-m4uW47V2.js +1 -0
- package/dist/web/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/web/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/web/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/web/assets/vue-BuYVFjOK.js +1 -0
- package/dist/web/assets/vue-html-xdeiXROB.js +1 -0
- package/dist/web/assets/vyper-nyqBNV6O.js +1 -0
- package/dist/web/assets/wasm-C6j12Q_x.js +1 -0
- package/dist/web/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/web/assets/wenyan-7A4Fjokl.js +1 -0
- package/dist/web/assets/wgsl-CB0Krxn9.js +1 -0
- package/dist/web/assets/wikitext-DCE3LsBG.js +1 -0
- package/dist/web/assets/wolfram-C3FkfJm5.js +1 -0
- package/dist/web/assets/xml-e3z08dGr.js +1 -0
- package/dist/web/assets/xsl-Dd0NUgwM.js +1 -0
- package/dist/web/assets/yaml-CVw76BM1.js +1 -0
- package/dist/web/assets/zenscript-HnGAYVZD.js +1 -0
- package/dist/web/assets/zig-BVz_zdnA.js +1 -0
- package/dist/web/index.html +2 -2
- package/examples/component-showcase/README.md +29 -0
- package/examples/component-showcase/flow.json +40 -0
- package/examples/component-showcase/nodes/chart/spec.json +66 -0
- package/examples/component-showcase/nodes/counter/spec.json +57 -0
- package/examples/component-showcase/nodes/fetcher/actions/refresh.ts +35 -0
- package/examples/component-showcase/nodes/fetcher/spec.json +68 -0
- package/examples/component-showcase/nodes/form/spec.json +87 -0
- package/examples/component-showcase/package.json +6 -0
- package/examples/component-showcase/style.json +28 -0
- package/package.json +2 -1
- package/src/api.ts +54 -0
- package/src/cli.ts +1 -0
- package/src/component-action-runner.ts +188 -0
- package/src/component-spec-resolver.ts +60 -0
- package/src/layout.ts +1 -0
- package/src/merge.ts +4 -0
- package/src/operations.ts +43 -4
- package/src/schema.ts +120 -2
- package/src/watcher.ts +39 -3
- package/dist/web/assets/index-CwfFCUzZ.css +0 -1
- 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'
|
|
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
|
-
|
|
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
|
-
|
|
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([
|
|
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 {
|
|
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
|
|
229
|
-
|
|
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 => {
|