@tuongaz/seeflow 0.1.65 → 0.1.69
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-DkO2vf4F.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-DU-QlURD.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-Cdou6O-P.css +1 -0
- package/dist/web/assets/index-WByZ8Huv.js +8613 -0
- package/dist/web/assets/{index.es-CVm3MRo3.js → index.es-BPwqcM9N.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-BiZ0aMok.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-UIAJJxZW.js +1 -0
- package/dist/web/assets/markdown-ZXLU2tih.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 +50 -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 +5 -0
- package/src/operations.ts +46 -4
- package/src/schema.ts +140 -5
- package/src/watcher.ts +39 -3
- package/dist/web/assets/index-CwfFCUzZ.css +0 -1
- package/dist/web/assets/index-DL6aaddE.js +0 -7838
package/src/api.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
4
|
import { streamSSE } from 'hono/streaming';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import { runComponentAction } from './component-action-runner.ts';
|
|
6
7
|
import {
|
|
7
8
|
AssembleRequestSchema,
|
|
8
9
|
ProposeScopeRequestSchema,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
} from './proxy.ts';
|
|
39
40
|
import type { Registry } from './registry.ts';
|
|
40
41
|
import { getSchemaCategory, listSchemaCategories, schemaCategoryNames } from './schema-catalog.ts';
|
|
42
|
+
import type { ComponentAction } from './schema.ts';
|
|
41
43
|
import { FlowSchema, ResolvedFlowSchema } from './schema.ts';
|
|
42
44
|
import { type Spawner, defaultSpawner } from './shellout.ts';
|
|
43
45
|
import { ID_TYPES, MAX_ID_COUNT, generateIds, isIdType } from './short-id.ts';
|
|
@@ -850,6 +852,58 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
850
852
|
return c.json(result);
|
|
851
853
|
});
|
|
852
854
|
|
|
855
|
+
// POST /api/flows/:id/nodes/:nodeId/actions/:name — dispatch a component
|
|
856
|
+
// node's named action over HTTP. Only `script`-kind actions cross this seam;
|
|
857
|
+
// `set`-kind actions mutate canvas state locally and never round-trip
|
|
858
|
+
// through the API (the runner rejects them with statusHint 400).
|
|
859
|
+
// Payload is the JSON request body (defaults to {} on parse failure) and is
|
|
860
|
+
// piped to the script's stdin by `runComponentAction`. Response is the
|
|
861
|
+
// script's parsed JSON stdout on success.
|
|
862
|
+
api.post('/flows/:id/nodes/:nodeId/actions/:name', async (c) => {
|
|
863
|
+
const id = c.req.param('id');
|
|
864
|
+
const nodeId = c.req.param('nodeId');
|
|
865
|
+
const actionName = c.req.param('name');
|
|
866
|
+
|
|
867
|
+
const entry = registry.getById(id);
|
|
868
|
+
if (!entry) return c.json({ error: 'unknown demo' }, 404);
|
|
869
|
+
if (!events) return c.json({ error: 'events not enabled' }, 500);
|
|
870
|
+
|
|
871
|
+
const fullPath = resolveFilePath(entry.repoPath, entry.flowPath);
|
|
872
|
+
if (!existsSync(fullPath)) {
|
|
873
|
+
return c.json({ error: `Flow file not found: ${fullPath}` }, 404);
|
|
874
|
+
}
|
|
875
|
+
const merged = readMergedFlow(fullPath);
|
|
876
|
+
if (!merged.flow) {
|
|
877
|
+
return c.json({ error: merged.error ?? 'Flow read failed' }, 400);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const node = merged.flow.nodes.find((n) => n.id === nodeId);
|
|
881
|
+
if (!node) return c.json({ error: `Unknown nodeId: ${nodeId}` }, 404);
|
|
882
|
+
if (node.type !== 'component') {
|
|
883
|
+
return c.json({ error: `Node ${nodeId} is not a component node` }, 400);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const action = (node.data as { spec: { actions?: Record<string, ComponentAction> } }).spec
|
|
887
|
+
.actions?.[actionName];
|
|
888
|
+
if (!action) return c.json({ error: `Unknown action: ${actionName}` }, 404);
|
|
889
|
+
|
|
890
|
+
const payload = await c.req.json().catch(() => ({}));
|
|
891
|
+
const result = await runComponentAction({
|
|
892
|
+
events,
|
|
893
|
+
flowId: id,
|
|
894
|
+
nodeId,
|
|
895
|
+
cwd: entry.repoPath,
|
|
896
|
+
actionName,
|
|
897
|
+
action,
|
|
898
|
+
payload,
|
|
899
|
+
spawner: processSpawner,
|
|
900
|
+
});
|
|
901
|
+
if (!result.ok) {
|
|
902
|
+
return c.json({ error: result.error }, result.statusHint as 400 | 404 | 500 | 504);
|
|
903
|
+
}
|
|
904
|
+
return c.json(result.body);
|
|
905
|
+
});
|
|
906
|
+
|
|
853
907
|
// POST /api/flows/:id/reset — the "Restart demo" workflow (US-008). Order:
|
|
854
908
|
// 1. Stop every live play-script + every long-running status-script for
|
|
855
909
|
// this demo in parallel — both must complete before any reset script
|
package/src/cli.ts
CHANGED
|
@@ -350,6 +350,7 @@ async function runStart() {
|
|
|
350
350
|
async function seedExamples(registry: Registry) {
|
|
351
351
|
await seedExample(registry, 'order-pipeline');
|
|
352
352
|
await seedExample(registry, 'ecommerce-platform');
|
|
353
|
+
await seedExample(registry, 'component-showcase');
|
|
353
354
|
}
|
|
354
355
|
|
|
355
356
|
async function seedExample(registry: Registry, exampleName: string) {
|
|
@@ -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
|
@@ -67,6 +67,7 @@ const NODE_STYLE_KEYS = new Set([
|
|
|
67
67
|
'fontSize',
|
|
68
68
|
'textColor',
|
|
69
69
|
'cornerRadius',
|
|
70
|
+
'shadow',
|
|
70
71
|
'borderWidth',
|
|
71
72
|
'color',
|
|
72
73
|
'strokeWidth',
|
|
@@ -133,6 +134,10 @@ export function splitFlow(resolved: {
|
|
|
133
134
|
const flowData: Record<string, unknown> = {};
|
|
134
135
|
for (const [k, v] of Object.entries(data)) {
|
|
135
136
|
if (v === undefined) continue;
|
|
137
|
+
// Component nodes externalize `spec` to <project>/nodes/<id>/spec.json
|
|
138
|
+
// (the sidecar). Drop it from flow.json so the strict on-disk schema
|
|
139
|
+
// doesn't reject it and the spec stays single-sourced on disk.
|
|
140
|
+
if (node.type === 'component' && k === 'spec') continue;
|
|
136
141
|
if (NODE_DATA_FLOW_KEYS.has(k)) {
|
|
137
142
|
flowData[k] = v;
|
|
138
143
|
} 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,
|
|
@@ -103,6 +105,7 @@ export const NodePatchBodySchema = z
|
|
|
103
105
|
fontSize: z.number().positive().optional(),
|
|
104
106
|
textColor: ColorTokenSchema.optional(),
|
|
105
107
|
cornerRadius: z.number().min(0).optional(),
|
|
108
|
+
shadow: z.number().int().min(0).max(5).optional(),
|
|
106
109
|
width: z.number().positive().optional(),
|
|
107
110
|
height: z.number().positive().optional(),
|
|
108
111
|
// type:'html'-only: when true, the renderer measures content and React Flow
|
|
@@ -141,6 +144,12 @@ export const NodePatchBodySchema = z
|
|
|
141
144
|
playAction: PlayActionSchema.optional(),
|
|
142
145
|
statusAction: StatusActionSchema.optional(),
|
|
143
146
|
stateSource: StateSourceSchema.optional(),
|
|
147
|
+
// type:'component'-only: json-render spec describing the reactive UI.
|
|
148
|
+
// Externalized to `<project>/nodes/<id>/spec.json` by patchNodeImpl; the
|
|
149
|
+
// in-memory ResolvedFlow keeps `data.spec` populated for the post-merge
|
|
150
|
+
// reparse + SSE broadcast, but splitFlow strips it from flow.json so the
|
|
151
|
+
// sidecar is the source of truth on disk.
|
|
152
|
+
spec: ComponentSpecSchema.optional(),
|
|
144
153
|
})
|
|
145
154
|
.strict();
|
|
146
155
|
export type NodePatchBody = z.infer<typeof NodePatchBodySchema>;
|
|
@@ -159,6 +168,7 @@ const NODE_DATA_PATCH_KEYS = [
|
|
|
159
168
|
'fontSize',
|
|
160
169
|
'textColor',
|
|
161
170
|
'cornerRadius',
|
|
171
|
+
'shadow',
|
|
162
172
|
'width',
|
|
163
173
|
'height',
|
|
164
174
|
'autoSize',
|
|
@@ -172,6 +182,7 @@ const NODE_DATA_PATCH_KEYS = [
|
|
|
172
182
|
'playAction',
|
|
173
183
|
'statusAction',
|
|
174
184
|
'stateSource',
|
|
185
|
+
'spec',
|
|
175
186
|
] as const satisfies ReadonlyArray<keyof NodePatchBody>;
|
|
176
187
|
|
|
177
188
|
const EXTERNALIZED_FIELD_NAMES = new Set<string>(EXTERNALIZED_NODE_FIELDS.map((e) => e.field));
|
|
@@ -238,6 +249,10 @@ const SEMANTIC_KEYS_BY_TYPE: Record<z.infer<typeof NodeTypeSchema>, ReadonlySet<
|
|
|
238
249
|
'statusAction',
|
|
239
250
|
'alt',
|
|
240
251
|
]),
|
|
252
|
+
// Component nodes externalize `spec` to <project>/nodes/<id>/spec.json; the
|
|
253
|
+
// semantic-key set covers only the universal capability fields so retype
|
|
254
|
+
// never drags `spec` through `data`. (US-007 wires the sidecar writer.)
|
|
255
|
+
component: GEOMETRIC_SEMANTIC_KEYS,
|
|
241
256
|
};
|
|
242
257
|
|
|
243
258
|
// Visual data keys — routed to style.json on write by splitFlow. Kept here
|
|
@@ -253,6 +268,7 @@ const NODE_VISUAL_KEYS = new Set([
|
|
|
253
268
|
'fontSize',
|
|
254
269
|
'textColor',
|
|
255
270
|
'cornerRadius',
|
|
271
|
+
'shadow',
|
|
256
272
|
'borderWidth',
|
|
257
273
|
'color',
|
|
258
274
|
'strokeWidth',
|
|
@@ -321,12 +337,13 @@ export const mergeNodeUpdates = (node: Record<string, unknown>, updates: NodePat
|
|
|
321
337
|
}
|
|
322
338
|
}
|
|
323
339
|
|
|
324
|
-
// type:'html'
|
|
340
|
+
// type:'html' + type:'component' invariant enforcement:
|
|
325
341
|
// autoSize === true ⊻ (width and height set).
|
|
326
342
|
// autoSize: true is the dominant signal — it strips width/height even if
|
|
327
343
|
// the same patch tried to write them. Writing width/height implicitly
|
|
328
|
-
// flips autoSize to false.
|
|
329
|
-
|
|
344
|
+
// flips autoSize to false. Both node types default to autoSize:true in the
|
|
345
|
+
// renderer and share the same shrink-wrap-to-content mechanism.
|
|
346
|
+
if (node.type === 'html' || node.type === 'component') {
|
|
330
347
|
// The autoSize invariant requires `width`/`height` to be ABSENT from the
|
|
331
348
|
// serialized JSON when autoSize is true — not present with value
|
|
332
349
|
// `undefined` (which would serialize as a stray `"width": null` or get
|
|
@@ -757,7 +774,16 @@ export async function mutateMergedFlow<E extends { kind: string }>(
|
|
|
757
774
|
const styleParse = StyleSchema.safeParse(read.rawStyle);
|
|
758
775
|
if (!styleParse.success) return { kind: 'badSchema', issues: styleParse.error.issues };
|
|
759
776
|
|
|
760
|
-
|
|
777
|
+
// Inline component spec sidecars (<project>/nodes/<id>/spec.json) before the
|
|
778
|
+
// mutator runs so the post-mutation ResolvedFlowSchema parse sees `data.spec`
|
|
779
|
+
// on every existing component node. splitFlow strips `spec` back out before
|
|
780
|
+
// we write flow.json, keeping the sidecar as the on-disk source of truth.
|
|
781
|
+
const projectRoot = dirname(flowPath);
|
|
782
|
+
const { flow: inlinedFlow } = inlineComponentSpecs(
|
|
783
|
+
mergeFlowAndStyle(flowParse.data, styleParse.data),
|
|
784
|
+
projectRoot,
|
|
785
|
+
);
|
|
786
|
+
const merged = inlinedFlow as unknown as {
|
|
761
787
|
version: number;
|
|
762
788
|
name: string;
|
|
763
789
|
resetAction?: unknown;
|
|
@@ -1532,6 +1558,22 @@ export async function patchNodeImpl(
|
|
|
1532
1558
|
}
|
|
1533
1559
|
node.data = data;
|
|
1534
1560
|
}
|
|
1561
|
+
// Component spec sidecar — write the pretty-printed JSON to
|
|
1562
|
+
// `<project>/nodes/<id>/spec.json` so the on-disk source of truth stays
|
|
1563
|
+
// in sync. mergeNodeUpdates already put data.spec on the merged tree for
|
|
1564
|
+
// the post-mutation ResolvedFlowSchema parse; splitFlow strips it from
|
|
1565
|
+
// flow.json so we don't double-store the spec.
|
|
1566
|
+
if (node.type === 'component' && updates.spec !== undefined) {
|
|
1567
|
+
const specAbs = nodeFileAbsPath(entry.repoPath, nodeId, 'spec.json');
|
|
1568
|
+
try {
|
|
1569
|
+
writeNodeFile(specAbs, `${JSON.stringify(updates.spec, null, 2)}\n`);
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
return {
|
|
1572
|
+
kind: 'writeFailed',
|
|
1573
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1535
1577
|
return { kind: 'ok' };
|
|
1536
1578
|
});
|
|
1537
1579
|
}
|