framefields 2.5.5 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/audio/index.mjs +10 -10
- package/dist/{audio-C9euVW4K.mjs → audio-QBA2VogL.mjs} +3 -3
- package/dist/{audio-C9euVW4K.mjs.map → audio-QBA2VogL.mjs.map} +1 -1
- package/dist/cli.mjs +866 -16
- package/dist/cli.mjs.map +1 -1
- package/dist/components/index.d.mts +2 -2
- package/dist/components/index.mjs +43 -43
- package/dist/{dist-BuirkW-f.mjs → dist-CDwWQYg3.mjs} +2 -2
- package/dist/dist-CDwWQYg3.mjs.map +1 -0
- package/dist/{dist-DBdxM6xv.mjs → dist-nHCqVk-e.mjs} +3 -3
- package/dist/dist-nHCqVk-e.mjs.map +1 -0
- package/dist/effects/index.mjs +34 -34
- package/dist/{effects-B263lgUi.mjs → effects-C-6I4iYW.mjs} +2 -2
- package/dist/{effects-B263lgUi.mjs.map → effects-C-6I4iYW.mjs.map} +1 -1
- package/dist/fonts/index.d.mts +1 -1
- package/dist/fonts/index.mjs +2 -2
- package/dist/{fonts-DNx6ps2p.mjs → fonts-BieW3VWV.mjs} +7 -3
- package/dist/fonts-BieW3VWV.mjs.map +1 -0
- package/dist/{gpu-BRBIoxMC.mjs → gpu-Bb4WmYYH.mjs} +2 -2
- package/dist/{gpu-BRBIoxMC.mjs.map → gpu-Bb4WmYYH.mjs.map} +1 -1
- package/dist/index-D-Gnyffj.d.mts.map +1 -1
- package/dist/{index-N1tMc5Z9.d.mts → index-E1IrPAm4.d.mts} +629 -203
- package/dist/index-E1IrPAm4.d.mts.map +1 -0
- package/dist/{index-CalU-mpZ.d.mts → index-j5LGVVbm.d.mts} +3 -1
- package/dist/index-j5LGVVbm.d.mts.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +50 -50
- package/dist/preview/index.mjs +10 -10
- package/dist/{preview-ChR8Z8Fm.mjs → preview-D6jS38Di.mjs} +2 -2
- package/dist/{preview-ChR8Z8Fm.mjs.map → preview-D6jS38Di.mjs.map} +1 -1
- package/dist/preview-engine/{chunk-ZJLNXN7W.mjs → chunk-AYTP6OGO.mjs} +6 -6
- package/dist/preview-engine/chunk-MLAIEIUF.mjs +1974 -0
- package/dist/preview-engine/chunk-W3I7LKDV.mjs +5 -0
- package/dist/preview-engine/chunk-ZMMNN77R.mjs +3 -0
- package/dist/preview-engine/components.mjs +1 -1
- package/dist/preview-engine/fonts.mjs +1 -1
- package/dist/preview-engine/framefields.mjs +1 -1
- package/dist/preview-engine/player.mjs +1 -1
- package/dist/preview-engine/{renderers-WZEV464O.mjs → renderers-4G3UOPA7.mjs} +1 -1
- package/dist/{program-O2bWX0cs.mjs → program-Bf4yHbHC.mjs} +19 -3
- package/dist/program-Bf4yHbHC.mjs.map +1 -0
- package/dist/registry/annotate.json +1 -1
- package/dist/registry/app-frame.json +1 -1
- package/dist/registry/backdrop.json +3 -3
- package/dist/registry/before-after.json +1 -1
- package/dist/registry/callout.json +1 -1
- package/dist/registry/captions.json +7 -1
- package/dist/registry/card-stack.json +3 -3
- package/dist/registry/chat-thread.json +7 -1
- package/dist/registry/code-block.json +8 -2
- package/dist/registry/confetti.json +1 -1
- package/dist/registry/device-frame.json +2 -1
- package/dist/registry/focus-zoom.json +1 -1
- package/dist/registry/logo-reveal.json +1 -1
- package/dist/registry/lower-third.json +1 -1
- package/dist/registry/meter.json +1 -1
- package/dist/registry/mouse-pointer.json +1 -1
- package/dist/registry/selection.json +3 -3
- package/dist/registry/spotlight.json +1 -1
- package/dist/registry/terminal.json +7 -1
- package/dist/registry/text-reveal.json +2 -1
- package/dist/registry/title-card.json +1 -1
- package/dist/registry/toasts.json +1 -1
- package/dist/registry/touch-point.json +4 -4
- package/dist/registry/waveform.json +3 -3
- package/dist/registry/word-cycle.json +2 -2
- package/dist/renderer/index.mjs +2 -2
- package/dist/renderer-4FQmduPw.mjs +5 -0
- package/dist/{renderer-DW21AyEv.mjs → renderer-4v6XYAiX.mjs} +3 -3
- package/dist/{renderer-DW21AyEv.mjs.map → renderer-4v6XYAiX.mjs.map} +1 -1
- package/dist/{renderer-YFHESw3v.mjs → renderer-79mO0b9Y.mjs} +3 -3
- package/dist/{renderer-YFHESw3v.mjs.map → renderer-79mO0b9Y.mjs.map} +1 -1
- package/dist/{renderer-DtqiWq1m.mjs → renderer-AOZWHGT3.mjs} +3 -3
- package/dist/{renderer-DtqiWq1m.mjs.map → renderer-AOZWHGT3.mjs.map} +1 -1
- package/dist/{renderer-Dw3K8KT9.mjs → renderer-B1Ox12RF.mjs} +2 -2
- package/dist/{renderer-Dw3K8KT9.mjs.map → renderer-B1Ox12RF.mjs.map} +1 -1
- package/dist/{renderer-DkM6Ikdf.mjs → renderer-B6vr-B0t.mjs} +3 -3
- package/dist/{renderer-DkM6Ikdf.mjs.map → renderer-B6vr-B0t.mjs.map} +1 -1
- package/dist/renderer-BBCpRuYU.mjs +5 -0
- package/dist/renderer-BBTo_jLO.mjs +5 -0
- package/dist/{renderer-CpDaUeCN.mjs → renderer-BDCS7k5X.mjs} +3 -3
- package/dist/{renderer-CpDaUeCN.mjs.map → renderer-BDCS7k5X.mjs.map} +1 -1
- package/dist/{renderer-DO1TeUqV.mjs → renderer-BFdgbqgW.mjs} +3 -3
- package/dist/{renderer-DO1TeUqV.mjs.map → renderer-BFdgbqgW.mjs.map} +1 -1
- package/dist/{renderer-CEAmByVW.mjs → renderer-BHgYRSu8.mjs} +2 -2
- package/dist/{renderer-CEAmByVW.mjs.map → renderer-BHgYRSu8.mjs.map} +1 -1
- package/dist/{renderer-CA_CwisV.mjs → renderer-BKssznKn.mjs} +2 -2
- package/dist/{renderer-CA_CwisV.mjs.map → renderer-BKssznKn.mjs.map} +1 -1
- package/dist/{renderer-DWG6EXGj.mjs → renderer-BL9vkn6m.mjs} +3 -3
- package/dist/{renderer-DWG6EXGj.mjs.map → renderer-BL9vkn6m.mjs.map} +1 -1
- package/dist/renderer-BLg66ZgG.mjs +5 -0
- package/dist/{renderer-RbMM6Njv.mjs → renderer-BQFxoNzi.mjs} +3 -3
- package/dist/{renderer-RbMM6Njv.mjs.map → renderer-BQFxoNzi.mjs.map} +1 -1
- package/dist/{renderer-x1xlZaH_.mjs → renderer-BQdn_qGp.mjs} +3 -3
- package/dist/{renderer-x1xlZaH_.mjs.map → renderer-BQdn_qGp.mjs.map} +1 -1
- package/dist/{renderer-DkOxEQsg.mjs → renderer-BR35Xp9n.mjs} +3 -3
- package/dist/{renderer-DkOxEQsg.mjs.map → renderer-BR35Xp9n.mjs.map} +1 -1
- package/dist/renderer-BScS8AzG.mjs +5 -0
- package/dist/{renderer-C2MaJKfY.mjs → renderer-BTjveb1Y.mjs} +3 -3
- package/dist/{renderer-C2MaJKfY.mjs.map → renderer-BTjveb1Y.mjs.map} +1 -1
- package/dist/renderer-Bb1cS5mJ.mjs +5 -0
- package/dist/{renderer-Dwh0jaor.mjs → renderer-Bbkf5EIi.mjs} +3 -3
- package/dist/{renderer-Dwh0jaor.mjs.map → renderer-Bbkf5EIi.mjs.map} +1 -1
- package/dist/{renderer-DsnV8tpW.mjs → renderer-BmgyvIC3.mjs} +3 -3
- package/dist/{renderer-DsnV8tpW.mjs.map → renderer-BmgyvIC3.mjs.map} +1 -1
- package/dist/{renderer-BfQdc3RM.mjs → renderer-Bpve7M6S.mjs} +69 -69
- package/dist/{renderer-BfQdc3RM.mjs.map → renderer-Bpve7M6S.mjs.map} +1 -1
- package/dist/renderer-Bpwd9zEY.mjs +5 -0
- package/dist/{renderer-C5QYI1Wu.mjs → renderer-BsnhAgrB.mjs} +2 -2
- package/dist/{renderer-C5QYI1Wu.mjs.map → renderer-BsnhAgrB.mjs.map} +1 -1
- package/dist/{renderer-BlU3Sh_0.mjs → renderer-By5xTRqB.mjs} +3 -3
- package/dist/{renderer-BlU3Sh_0.mjs.map → renderer-By5xTRqB.mjs.map} +1 -1
- package/dist/{renderer-D8m3fuRp.mjs → renderer-BzEQXAdl.mjs} +2 -2
- package/dist/{renderer-D8m3fuRp.mjs.map → renderer-BzEQXAdl.mjs.map} +1 -1
- package/dist/renderer-Bzyd9xi5.mjs +5 -0
- package/dist/{renderer-1NoJLLDa.mjs → renderer-C64g87yU.mjs} +3 -3
- package/dist/{renderer-1NoJLLDa.mjs.map → renderer-C64g87yU.mjs.map} +1 -1
- package/dist/{renderer-Daqvhwjp.mjs → renderer-C6Wb1TZT.mjs} +3 -3
- package/dist/{renderer-Daqvhwjp.mjs.map → renderer-C6Wb1TZT.mjs.map} +1 -1
- package/dist/{renderer-BWNQQZ12.mjs → renderer-CHPLHbAs.mjs} +3 -3
- package/dist/{renderer-BWNQQZ12.mjs.map → renderer-CHPLHbAs.mjs.map} +1 -1
- package/dist/{renderer-B5ZdkcMi.mjs → renderer-CJRu7lLH.mjs} +2 -2
- package/dist/{renderer-B5ZdkcMi.mjs.map → renderer-CJRu7lLH.mjs.map} +1 -1
- package/dist/{renderer-Bvizxddu.mjs → renderer-CMWUtfwn.mjs} +2 -2
- package/dist/{renderer-Bvizxddu.mjs.map → renderer-CMWUtfwn.mjs.map} +1 -1
- package/dist/{renderer-C3tdjg8B.mjs → renderer-CRkgyk08.mjs} +3 -3
- package/dist/{renderer-C3tdjg8B.mjs.map → renderer-CRkgyk08.mjs.map} +1 -1
- package/dist/renderer-CXVssx1k.mjs +5 -0
- package/dist/{renderer-DcApNxER.mjs → renderer-CY0BT6ok.mjs} +2 -2
- package/dist/{renderer-DcApNxER.mjs.map → renderer-CY0BT6ok.mjs.map} +1 -1
- package/dist/renderer-CYFiGLls.mjs +5 -0
- package/dist/{renderer-PhjHSS97.mjs → renderer-CYLP8sD6.mjs} +2 -2
- package/dist/{renderer-PhjHSS97.mjs.map → renderer-CYLP8sD6.mjs.map} +1 -1
- package/dist/{renderer-C6n8y6Pw.mjs → renderer-CY_1u6zi.mjs} +3 -3
- package/dist/{renderer-C6n8y6Pw.mjs.map → renderer-CY_1u6zi.mjs.map} +1 -1
- package/dist/renderer-C__BvvtP.mjs +5 -0
- package/dist/renderer-Ca3YZsoT.mjs +5 -0
- package/dist/{renderer-DADTT3ym.mjs → renderer-CcSBjtRa.mjs} +2 -2
- package/dist/{renderer-CnclEQdA.mjs → renderer-CgdplRri.mjs} +2 -2
- package/dist/{renderer-CnclEQdA.mjs.map → renderer-CgdplRri.mjs.map} +1 -1
- package/dist/{renderer-oR1LvHeO.mjs → renderer-Cn9tU_s5.mjs} +2 -2
- package/dist/{renderer-oR1LvHeO.mjs.map → renderer-Cn9tU_s5.mjs.map} +1 -1
- package/dist/renderer-CnM5iFj9.mjs +5 -0
- package/dist/{renderer-DHdFVpdH.mjs → renderer-Cqp_TUl_.mjs} +2 -2
- package/dist/{renderer-DHdFVpdH.mjs.map → renderer-Cqp_TUl_.mjs.map} +1 -1
- package/dist/{renderer-C8pJFxcL.mjs → renderer-CsOLHKFx.mjs} +2 -2
- package/dist/{renderer-C8pJFxcL.mjs.map → renderer-CsOLHKFx.mjs.map} +1 -1
- package/dist/{renderer-R5Lg65F8.mjs → renderer-D01RT3Ov.mjs} +2 -2
- package/dist/renderer-D1nZrvQx.mjs +5 -0
- package/dist/renderer-D6XjVQME.mjs +5 -0
- package/dist/{renderer-QQDU1b0Z.mjs → renderer-DAONCf_3.mjs} +3 -3
- package/dist/{renderer-QQDU1b0Z.mjs.map → renderer-DAONCf_3.mjs.map} +1 -1
- package/dist/{renderer-DTPmvL1i.mjs → renderer-DAfWLGfy.mjs} +3 -3
- package/dist/{renderer-DTPmvL1i.mjs.map → renderer-DAfWLGfy.mjs.map} +1 -1
- package/dist/{renderer-BvA4sQz5.mjs → renderer-DEeKkljI.mjs} +3 -3
- package/dist/{renderer-BvA4sQz5.mjs.map → renderer-DEeKkljI.mjs.map} +1 -1
- package/dist/{renderer-YBwN9uyj.mjs → renderer-DFYXjAIT.mjs} +5 -5
- package/dist/{renderer-YBwN9uyj.mjs.map → renderer-DFYXjAIT.mjs.map} +1 -1
- package/dist/renderer-DIuMnMF0.mjs +5 -0
- package/dist/{renderer-Dz4muMyE.mjs → renderer-DJHuPNJx.mjs} +2 -2
- package/dist/{renderer-Dz4muMyE.mjs.map → renderer-DJHuPNJx.mjs.map} +1 -1
- package/dist/{renderer-H4-8sBjl.mjs → renderer-DJTXhVNh.mjs} +2 -2
- package/dist/{renderer-H4-8sBjl.mjs.map → renderer-DJTXhVNh.mjs.map} +1 -1
- package/dist/{renderer-BMiZIGkZ.mjs → renderer-DNEFYrQ5.mjs} +2 -2
- package/dist/renderer-DUvvM8EF.mjs +5 -0
- package/dist/renderer-DWogyuMO.mjs +5 -0
- package/dist/renderer-DWsj_LJR.mjs +5 -0
- package/dist/{renderer-DGOY-LBR.mjs → renderer-DYJHFHF4.mjs} +2 -2
- package/dist/{renderer-DGOY-LBR.mjs.map → renderer-DYJHFHF4.mjs.map} +1 -1
- package/dist/{renderer-CVbNiH03.mjs → renderer-DYR4gFEM.mjs} +3 -3
- package/dist/{renderer-CVbNiH03.mjs.map → renderer-DYR4gFEM.mjs.map} +1 -1
- package/dist/renderer-DYun0DJl.mjs +5 -0
- package/dist/renderer-D_6YqD-Z.mjs +5 -0
- package/dist/{renderer-B0IJirDs.mjs → renderer-D_UPFbei.mjs} +3 -3
- package/dist/{renderer-B0IJirDs.mjs.map → renderer-D_UPFbei.mjs.map} +1 -1
- package/dist/renderer-Db9LP86x.mjs +5 -0
- package/dist/{renderer-CFRTJ7cT.mjs → renderer-DgRJG_l-.mjs} +2 -2
- package/dist/{renderer-CFRTJ7cT.mjs.map → renderer-DgRJG_l-.mjs.map} +1 -1
- package/dist/renderer-DhHiC87m.mjs +5 -0
- package/dist/{renderer-BZ5uOfoX.mjs → renderer-DiD3r9tx.mjs} +2 -2
- package/dist/{renderer-BZ5uOfoX.mjs.map → renderer-DiD3r9tx.mjs.map} +1 -1
- package/dist/{renderer-B2no_9zi.mjs → renderer-Dj-y-BtT.mjs} +3 -3
- package/dist/{renderer-B2no_9zi.mjs.map → renderer-Dj-y-BtT.mjs.map} +1 -1
- package/dist/renderer-Dj5jGAkR.mjs +5 -0
- package/dist/{renderer-Bs6BaMLf.mjs → renderer-DlA7kzIs.mjs} +3 -3
- package/dist/{renderer-Bs6BaMLf.mjs.map → renderer-DlA7kzIs.mjs.map} +1 -1
- package/dist/renderer-DlPUQ7RF.mjs +5 -0
- package/dist/renderer-DpIPdgn8.mjs +5 -0
- package/dist/renderer-Dv0C9CEt.mjs +6 -0
- package/dist/{renderer-CFCR_7bq.mjs → renderer-EwSSuJXj.mjs} +3 -3
- package/dist/{renderer-CFCR_7bq.mjs.map → renderer-EwSSuJXj.mjs.map} +1 -1
- package/dist/renderer-Iw8fDL-p.mjs +5 -0
- package/dist/{renderer-tcS585Iy.mjs → renderer-JJibsJfb.mjs} +2 -2
- package/dist/{renderer-tcS585Iy.mjs.map → renderer-JJibsJfb.mjs.map} +1 -1
- package/dist/{renderer-CTy0bSsl.mjs → renderer-KppAWfmJ.mjs} +3 -3
- package/dist/{renderer-CTy0bSsl.mjs.map → renderer-KppAWfmJ.mjs.map} +1 -1
- package/dist/{renderer-CPSg6eEQ.mjs → renderer-LW8MfGjd.mjs} +2 -2
- package/dist/{renderer-CPSg6eEQ.mjs.map → renderer-LW8MfGjd.mjs.map} +1 -1
- package/dist/{renderer-Cg2xS8h8.mjs → renderer-PMjV8x65.mjs} +2 -2
- package/dist/{renderer-Cg2xS8h8.mjs.map → renderer-PMjV8x65.mjs.map} +1 -1
- package/dist/renderer-SLI69Mju.mjs +5 -0
- package/dist/{renderer-DdkPY4hO.mjs → renderer-UArFa4y9.mjs} +2 -2
- package/dist/{renderer-DdkPY4hO.mjs.map → renderer-UArFa4y9.mjs.map} +1 -1
- package/dist/{renderer-B7vXnBUo.mjs → renderer-VwKmjuc6.mjs} +3 -3
- package/dist/{renderer-B7vXnBUo.mjs.map → renderer-VwKmjuc6.mjs.map} +1 -1
- package/dist/renderer-ZVtbb3-P.mjs +5 -0
- package/dist/{renderer-Dk0okoS3.mjs → renderer-bb3puodg.mjs} +2 -2
- package/dist/{renderer-Dk0okoS3.mjs.map → renderer-bb3puodg.mjs.map} +1 -1
- package/dist/{renderer-DVGVcDvB.mjs → renderer-eyvY4Lq4.mjs} +3 -3
- package/dist/{renderer-DVGVcDvB.mjs.map → renderer-eyvY4Lq4.mjs.map} +1 -1
- package/dist/renderer-gHNCu7H5.mjs +5 -0
- package/dist/renderer-gxDNmbtU.mjs +5 -0
- package/dist/renderer-hkK4DNRX.mjs +5 -0
- package/dist/{renderer-CrKYlipx.mjs → renderer-ij448rvt.mjs} +2 -2
- package/dist/{renderer-CrKYlipx.mjs.map → renderer-ij448rvt.mjs.map} +1 -1
- package/dist/{renderer-BrKgtYq4.mjs → renderer-j7aUfyeR.mjs} +3 -3
- package/dist/{renderer-BrKgtYq4.mjs.map → renderer-j7aUfyeR.mjs.map} +1 -1
- package/dist/{renderer-8_d6YNnE.mjs → renderer-kOlfYLtu.mjs} +2 -2
- package/dist/{renderer-8_d6YNnE.mjs.map → renderer-kOlfYLtu.mjs.map} +1 -1
- package/dist/{renderer-D3FJcRA3.mjs → renderer-l1qJE4z-.mjs} +2 -2
- package/dist/{renderer-D3FJcRA3.mjs.map → renderer-l1qJE4z-.mjs.map} +1 -1
- package/dist/{renderer-BKJ8cywK.mjs → renderer-ml2LHmRb.mjs} +2 -2
- package/dist/{renderer-BKJ8cywK.mjs.map → renderer-ml2LHmRb.mjs.map} +1 -1
- package/dist/{renderer-DmTxEeMh.mjs → renderer-mu2mYo5w.mjs} +3 -3
- package/dist/{renderer-DmTxEeMh.mjs.map → renderer-mu2mYo5w.mjs.map} +1 -1
- package/dist/{renderer-Bwxm-kWL.mjs → renderer-ovkctV5a.mjs} +2 -2
- package/dist/renderer-oy6SGDRz.mjs +5 -0
- package/dist/{renderer-CqJou_2L.mjs → renderer-qQmoO5Hk.mjs} +2 -2
- package/dist/{renderer-CqJou_2L.mjs.map → renderer-qQmoO5Hk.mjs.map} +1 -1
- package/dist/renderer-rq7UIcuH.mjs +5 -0
- package/dist/{renderer-BStETpqC.mjs → renderer-rv-AKqCl.mjs} +2 -2
- package/dist/{renderer-BStETpqC.mjs.map → renderer-rv-AKqCl.mjs.map} +1 -1
- package/dist/renderer-xkEJahws.mjs +5 -0
- package/dist/{renderer-BNs31kME.mjs → renderer-yF40Pb-n.mjs} +3 -3
- package/dist/{renderer-BNs31kME.mjs.map → renderer-yF40Pb-n.mjs.map} +1 -1
- package/dist/{renderer-Bag1LFos.mjs → renderer-yFHP9Zj2.mjs} +3 -3
- package/dist/{renderer-Bag1LFos.mjs.map → renderer-yFHP9Zj2.mjs.map} +1 -1
- package/dist/{renderer-CbxfSmYE.mjs → renderer-zOK5DYYn.mjs} +3 -3
- package/dist/{renderer-CbxfSmYE.mjs.map → renderer-zOK5DYYn.mjs.map} +1 -1
- package/dist/{renderers-CP8CFWDs-EZZPEbhW.mjs → renderers-CP8CFWDs-DPLX5EJs.mjs} +2 -2
- package/dist/{renderers-CP8CFWDs-EZZPEbhW.mjs.map → renderers-CP8CFWDs-DPLX5EJs.mjs.map} +1 -1
- package/dist/signals/index.mjs +4 -4
- package/dist/{src-r7Suenhp.mjs → src-BnkrNj2W.mjs} +612 -117
- package/dist/src-BnkrNj2W.mjs.map +1 -0
- package/package.json +59 -58
- package/dist/dist-BuirkW-f.mjs.map +0 -1
- package/dist/dist-DBdxM6xv.mjs.map +0 -1
- package/dist/fonts-DNx6ps2p.mjs.map +0 -1
- package/dist/index-CalU-mpZ.d.mts.map +0 -1
- package/dist/index-N1tMc5Z9.d.mts.map +0 -1
- package/dist/preview-engine/chunk-2TCJSHFA.mjs +0 -1974
- package/dist/preview-engine/chunk-Q7XC3FI7.mjs +0 -3
- package/dist/preview-engine/chunk-ZZAZF6VC.mjs +0 -5
- package/dist/program-O2bWX0cs.mjs.map +0 -1
- package/dist/renderer-0441BN0U.mjs +0 -5
- package/dist/renderer-3qDgFc-W.mjs +0 -5
- package/dist/renderer-6AIqTN5Z.mjs +0 -5
- package/dist/renderer-B7MZ5R7u.mjs +0 -5
- package/dist/renderer-BE2rIA4B.mjs +0 -5
- package/dist/renderer-BIQ81ln4.mjs +0 -5
- package/dist/renderer-BKm3_Ob3.mjs +0 -5
- package/dist/renderer-BNdT1Wwf.mjs +0 -5
- package/dist/renderer-BQycs1q_.mjs +0 -5
- package/dist/renderer-Bj7MxXfk.mjs +0 -5
- package/dist/renderer-BjRAGYYC.mjs +0 -5
- package/dist/renderer-BqE2BQ_X.mjs +0 -5
- package/dist/renderer-Bz5BMkOE.mjs +0 -5
- package/dist/renderer-CDOSfB9H.mjs +0 -5
- package/dist/renderer-CT26yQT-.mjs +0 -5
- package/dist/renderer-CZeAbz48.mjs +0 -5
- package/dist/renderer-D50b7kip.mjs +0 -5
- package/dist/renderer-D5LmrO96.mjs +0 -5
- package/dist/renderer-DCvqZ5o_.mjs +0 -5
- package/dist/renderer-DJGJeGEn.mjs +0 -5
- package/dist/renderer-DONvNNXi.mjs +0 -5
- package/dist/renderer-DPaSo--Y.mjs +0 -6
- package/dist/renderer-DfYYokhr.mjs +0 -5
- package/dist/renderer-DlIJhdEM.mjs +0 -5
- package/dist/renderer-DqD3nMtv.mjs +0 -5
- package/dist/renderer-DrIl-uGt.mjs +0 -5
- package/dist/renderer-DyCB3Q1d.mjs +0 -5
- package/dist/renderer-DyJ-iSv6.mjs +0 -5
- package/dist/renderer-Dzs0jV0G.mjs +0 -5
- package/dist/renderer-HIKXw5-Y.mjs +0 -5
- package/dist/renderer-Jgt2g8Wp.mjs +0 -5
- package/dist/renderer-KnZWmT-M.mjs +0 -5
- package/dist/renderer-OMSXddG9.mjs +0 -5
- package/dist/renderer-P78xh6OV.mjs +0 -5
- package/dist/renderer-gaFP1Kak.mjs +0 -5
- package/dist/renderer-lQ8gH7Zd.mjs +0 -5
- package/dist/src-r7Suenhp.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
5
7
|
import { parseArgs } from "node:util";
|
|
6
8
|
|
|
7
|
-
//#region src/cli/
|
|
9
|
+
//#region src/cli/config.ts
|
|
8
10
|
/**
|
|
9
|
-
* `framefields
|
|
10
|
-
*
|
|
11
|
-
* imports only `framefields` and `framefields/components` (the toolkit),
|
|
12
|
-
* plus any component it builds on, which is installed alongside it.
|
|
13
|
-
*
|
|
14
|
-
* Where they go is remembered in `framefields.json` at the project root.
|
|
11
|
+
* `framefields.json` at the project root: where installed components live,
|
|
12
|
+
* and which fonts `framefields add` / `framefields fonts add` installed.
|
|
15
13
|
*/
|
|
16
14
|
const CONFIG_FILE = "framefields.json";
|
|
17
15
|
/** The project's config, or the default it would get: `src/components` when there is a `src`. */
|
|
@@ -20,6 +18,7 @@ function readConfig(cwd) {
|
|
|
20
18
|
if (existsSync(file)) {
|
|
21
19
|
const config = JSON.parse(readFileSync(file, "utf8"));
|
|
22
20
|
if (typeof config.components !== "string") throw new Error(`${CONFIG_FILE}: "components" must be a directory path`);
|
|
21
|
+
if (config.fonts !== void 0 && (typeof config.fonts !== "object" || Object.values(config.fonts).some((v) => typeof v !== "string"))) throw new Error(`${CONFIG_FILE}: "fonts" must map roles to font file paths, e.g. { "emoji": "assets/fonts/NotoColorEmoji.ttf" }`);
|
|
23
22
|
return {
|
|
24
23
|
config,
|
|
25
24
|
saved: true
|
|
@@ -30,6 +29,236 @@ function readConfig(cwd) {
|
|
|
30
29
|
saved: false
|
|
31
30
|
};
|
|
32
31
|
}
|
|
32
|
+
function writeConfig(cwd, config) {
|
|
33
|
+
writeFileSync(join(cwd, CONFIG_FILE), `${JSON.stringify(config, null, " ")}\n`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/cli/fonts.ts
|
|
38
|
+
/**
|
|
39
|
+
* Fonts the CLI installs into a project: a catalog of pinned files (URL,
|
|
40
|
+
* size, SHA-256, license), the installer `framefields add` and
|
|
41
|
+
* `framefields fonts add` share, and the generated `fonts.ts` module that
|
|
42
|
+
* registers whatever is installed.
|
|
43
|
+
*
|
|
44
|
+
* framefields ships no fonts in its package (a color emoji font alone is
|
|
45
|
+
* ~10 MB); they're fetched when a project needs one, verified byte for byte,
|
|
46
|
+
* and saved with their license next to them.
|
|
47
|
+
*/
|
|
48
|
+
const NOTO_EMOJI = "https://raw.githubusercontent.com/googlefonts/noto-emoji/v2.051";
|
|
49
|
+
/** google/fonts at one commit: Inter and JetBrains Mono. */
|
|
50
|
+
const GOOGLE_FONTS = "https://raw.githubusercontent.com/google/fonts/e7db6ca603fd4862010383ff17ffd6cc4ffac1c0";
|
|
51
|
+
/** Installable fonts by role. */
|
|
52
|
+
const FONTS = {
|
|
53
|
+
emoji: {
|
|
54
|
+
label: "Noto Color Emoji 2.051: color emoji in any text (🎉 ❤️ 👍)",
|
|
55
|
+
file: "NotoColorEmoji.ttf",
|
|
56
|
+
font: {
|
|
57
|
+
url: `${NOTO_EMOJI}/fonts/NotoColorEmoji.ttf`,
|
|
58
|
+
sha256: "72a635cb3d2f3524c51620cdde406b217204e8a6a06c6a096ff8ed4b5fd6e27b",
|
|
59
|
+
bytes: 10673480
|
|
60
|
+
},
|
|
61
|
+
license: {
|
|
62
|
+
id: "OFL-1.1",
|
|
63
|
+
file: "NotoColorEmoji-OFL.txt",
|
|
64
|
+
url: `${NOTO_EMOJI}/fonts/LICENSE`,
|
|
65
|
+
sha256: "6a73f9541c2de74158c0e7cf6b0a58ef774f5a780bf191f2d7ec9cc53efe2bf2",
|
|
66
|
+
bytes: 4301
|
|
67
|
+
},
|
|
68
|
+
register: { emoji: true }
|
|
69
|
+
},
|
|
70
|
+
sans: googleFont({
|
|
71
|
+
label: "Inter 4: the built-in themes' display and body face",
|
|
72
|
+
file: "Inter.ttf",
|
|
73
|
+
family: "Inter",
|
|
74
|
+
path: "ofl/inter/Inter%5Bopsz,wght%5D.ttf",
|
|
75
|
+
sha256: "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031",
|
|
76
|
+
bytes: 876576,
|
|
77
|
+
license: {
|
|
78
|
+
path: "ofl/inter/OFL.txt",
|
|
79
|
+
sha256: "5b9321a4298cfeb6b34354164a1c3afc3db114569984c502b9b35d988fd58c57",
|
|
80
|
+
bytes: 4377
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
83
|
+
mono: googleFont({
|
|
84
|
+
label: "JetBrains Mono 2: the built-in themes' code and terminal face",
|
|
85
|
+
file: "JetBrainsMono.ttf",
|
|
86
|
+
family: "JetBrains Mono",
|
|
87
|
+
path: "ofl/jetbrainsmono/JetBrainsMono%5Bwght%5D.ttf",
|
|
88
|
+
sha256: "48715a42ec242c21e9f02692891e147d022299a52e48d5e413e1a942193ffeda",
|
|
89
|
+
bytes: 187208,
|
|
90
|
+
license: {
|
|
91
|
+
path: "ofl/jetbrainsmono/OFL.txt",
|
|
92
|
+
sha256: "b2fe5e8987594e9ffd1d2ca52a2f5d73eb8335243893c5d6254b5ad69269591d",
|
|
93
|
+
bytes: 4399
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
/** google/fonts, pinned to one commit. */
|
|
98
|
+
function googleFont(o) {
|
|
99
|
+
const at = (path$1) => `${GOOGLE_FONTS}/${path$1}`;
|
|
100
|
+
return {
|
|
101
|
+
label: o.label,
|
|
102
|
+
file: o.file,
|
|
103
|
+
font: {
|
|
104
|
+
url: at(o.path),
|
|
105
|
+
sha256: o.sha256,
|
|
106
|
+
bytes: o.bytes
|
|
107
|
+
},
|
|
108
|
+
license: {
|
|
109
|
+
url: at(o.license.path),
|
|
110
|
+
sha256: o.license.sha256,
|
|
111
|
+
bytes: o.license.bytes,
|
|
112
|
+
id: "OFL-1.1",
|
|
113
|
+
file: o.file.replace(/\.ttf$/, "-OFL.txt")
|
|
114
|
+
},
|
|
115
|
+
register: { family: o.family }
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Where fonts go when `framefields.json` names no place: the setup skill's layout. */
|
|
119
|
+
const DEFAULT_FONTS_DIR = "assets/fonts";
|
|
120
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
121
|
+
/** A project-relative path with forward slashes, as `framefields.json` keeps it. */
|
|
122
|
+
const projectPath = (cwd, path$1) => relative(cwd, path$1).split(sep).join(posix.sep);
|
|
123
|
+
/** Install the font for `role` into the project and record it in `framefields.json`. */
|
|
124
|
+
async function installFont(role, options) {
|
|
125
|
+
const catalog = options.catalog ?? FONTS;
|
|
126
|
+
const spec = catalog[role];
|
|
127
|
+
if (!spec) throw new Error(`Unknown font "${role}". Fonts framefields can install: ${Object.keys(catalog).join(", ")}.`);
|
|
128
|
+
const { cwd } = options;
|
|
129
|
+
const { config } = readConfig(cwd);
|
|
130
|
+
const configured = config.fonts?.[role];
|
|
131
|
+
if (configured && existsSync(resolve(cwd, configured))) return {
|
|
132
|
+
role,
|
|
133
|
+
path: configured,
|
|
134
|
+
status: "configured"
|
|
135
|
+
};
|
|
136
|
+
const target = resolve(cwd, options.dir ?? DEFAULT_FONTS_DIR, spec.file);
|
|
137
|
+
const path$1 = projectPath(cwd, target);
|
|
138
|
+
let status = "present";
|
|
139
|
+
const current = existsSync(target) ? readFileSync(target) : null;
|
|
140
|
+
if (!current || sha256(current) !== spec.font.sha256) {
|
|
141
|
+
if (current && !options.overwrite) throw new Error(`${path$1} exists but isn't the pinned ${spec.file}. Pass --overwrite to replace it, or point "fonts.${role}" in framefields.json at it to keep using yours.`);
|
|
142
|
+
options.onDownload?.(spec);
|
|
143
|
+
const get = options.fetch ?? fetch;
|
|
144
|
+
const [font, license] = await Promise.all([download(spec.font, get), download(spec.license, get)]);
|
|
145
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
146
|
+
writeFileSync(target, font);
|
|
147
|
+
writeFileSync(join(dirname(target), spec.license.file), license);
|
|
148
|
+
status = "installed";
|
|
149
|
+
}
|
|
150
|
+
writeConfig(cwd, {
|
|
151
|
+
...config,
|
|
152
|
+
fonts: {
|
|
153
|
+
...config.fonts,
|
|
154
|
+
[role]: path$1
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
role,
|
|
159
|
+
path: path$1,
|
|
160
|
+
status
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async function download(file, get) {
|
|
164
|
+
let res;
|
|
165
|
+
try {
|
|
166
|
+
res = await get(file.url);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new Error(`Couldn't download ${file.url} (${error instanceof Error ? error.message : String(error)}). Check the connection, or download it yourself and point framefields.json "fonts" at it.`);
|
|
169
|
+
}
|
|
170
|
+
if (!res.ok) throw new Error(`${file.url}: ${res.status} ${res.statusText}`);
|
|
171
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
172
|
+
const hash = sha256(bytes);
|
|
173
|
+
if (bytes.length !== file.bytes || hash !== file.sha256) throw new Error(`${file.url} isn't the pinned file (got ${bytes.length} bytes, sha256 ${hash}; expected ${file.bytes}, ${file.sha256}). Nothing was written.`);
|
|
174
|
+
return bytes;
|
|
175
|
+
}
|
|
176
|
+
/** First line of the generated module: files without it are the user's and left alone. */
|
|
177
|
+
const FONTS_MODULE_MARKER = "// Generated by framefields from framefields.json.";
|
|
178
|
+
/**
|
|
179
|
+
* Write `<components>/fonts.ts`, which registers every font in
|
|
180
|
+
* `framefields.json`. Call it once before building or rendering:
|
|
181
|
+
* `await registerFonts()`.
|
|
182
|
+
*/
|
|
183
|
+
function writeFontsModule(cwd, componentsDir, catalog = FONTS) {
|
|
184
|
+
const { config } = readConfig(cwd);
|
|
185
|
+
const target = join(componentsDir, "fonts.ts");
|
|
186
|
+
const path$1 = projectPath(cwd, target);
|
|
187
|
+
const fonts$1 = Object.entries(config.fonts ?? {}).filter(([role]) => catalog[role]).sort(([a], [b]) => a.localeCompare(b)).map(([role, file]) => {
|
|
188
|
+
const from = relative(componentsDir, resolve(cwd, file)).split(sep).join(posix.sep);
|
|
189
|
+
const url = from.startsWith(".") ? from : `./${from}`;
|
|
190
|
+
return {
|
|
191
|
+
register: catalog[role].register,
|
|
192
|
+
url
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
const call = (r, source) => "family" in r ? `register({ family: ${JSON.stringify(r.family)}, source: ${source} })` : `registerEmojiFont(${source})`;
|
|
196
|
+
const calls = fonts$1.map((f) => `await FontManager.${call(f.register, `asset("${f.url}")`)};`);
|
|
197
|
+
const content = `${FONTS_MODULE_MARKER}
|
|
198
|
+
// \`framefields add\` and \`framefields fonts add\` rewrite it when fonts
|
|
199
|
+
// change; register any other fonts in your own code.
|
|
200
|
+
import { FontManager } from "framefields";
|
|
201
|
+
|
|
202
|
+
/** A path next to this file as a URL: works in Node and in the browser preview. */
|
|
203
|
+
const asset = (path: string) => new URL(path, import.meta.url).href;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Register the fonts this project installed with framefields. Call it
|
|
207
|
+
* once, before building any text or rendering:
|
|
208
|
+
*
|
|
209
|
+
* \`\`\`ts
|
|
210
|
+
* await registerFonts();
|
|
211
|
+
* \`\`\`
|
|
212
|
+
*/
|
|
213
|
+
export async function registerFonts(): Promise<void> {
|
|
214
|
+
${fonts$1.map((f) => "family" in f.register ? `\tawait FontManager.register({\n\t\tfamily: ${JSON.stringify(f.register.family)},\n\t\tsource: asset("${f.url}"),\n\t});` : `\tawait FontManager.registerEmojiFont(\n\t\tasset("${f.url}"),\n\t);`).join("\n")}
|
|
215
|
+
}
|
|
216
|
+
`;
|
|
217
|
+
if (existsSync(target)) {
|
|
218
|
+
const current = readFileSync(target, "utf8");
|
|
219
|
+
if (current === content) return {
|
|
220
|
+
path: path$1,
|
|
221
|
+
status: "unchanged",
|
|
222
|
+
calls
|
|
223
|
+
};
|
|
224
|
+
if (!current.startsWith(FONTS_MODULE_MARKER)) return {
|
|
225
|
+
path: path$1,
|
|
226
|
+
status: "skipped",
|
|
227
|
+
calls
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
mkdirSync(componentsDir, { recursive: true });
|
|
231
|
+
writeFileSync(target, content);
|
|
232
|
+
return {
|
|
233
|
+
path: path$1,
|
|
234
|
+
status: "written",
|
|
235
|
+
calls
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* A hint when fonts land in a Git repo whose `.gitattributes` doesn't send
|
|
240
|
+
* them to LFS (a 10 MB binary bloats every clone), or null.
|
|
241
|
+
*/
|
|
242
|
+
function lfsHint(cwd, fontPath) {
|
|
243
|
+
if (!existsSync(join(cwd, ".git"))) return null;
|
|
244
|
+
const attrs = join(cwd, ".gitattributes");
|
|
245
|
+
const text = existsSync(attrs) ? readFileSync(attrs, "utf8") : "";
|
|
246
|
+
if (/\.ttf\b[^\n]*filter=lfs/.test(text)) return null;
|
|
247
|
+
return `Fonts are large binaries: track them with Git LFS, e.g. \`git lfs track "${posix.dirname(fontPath)}/**/*.ttf"\`.`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/cli/add.ts
|
|
252
|
+
/**
|
|
253
|
+
* `framefields add`: copy components' source into a project, the way a
|
|
254
|
+
* shadcn-style registry does. The code becomes the project's own; it
|
|
255
|
+
* imports only `framefields` and `framefields/components` (the toolkit),
|
|
256
|
+
* plus any component it builds on, which is installed alongside it.
|
|
257
|
+
*
|
|
258
|
+
* Where they go is remembered in `framefields.json` at the project root.
|
|
259
|
+
* Components that draw with fonts beyond the theme's (emoji) get them
|
|
260
|
+
* installed too, plus a generated `fonts.ts` that registers them.
|
|
261
|
+
*/
|
|
33
262
|
/** Resolve `names` (and what they import) and write their files. */
|
|
34
263
|
async function add(names, options) {
|
|
35
264
|
const { cwd, registry } = options;
|
|
@@ -54,7 +283,9 @@ async function add(names, options) {
|
|
|
54
283
|
written: [],
|
|
55
284
|
unchanged: [],
|
|
56
285
|
skipped: [],
|
|
57
|
-
configWritten: false
|
|
286
|
+
configWritten: false,
|
|
287
|
+
fonts: [],
|
|
288
|
+
fontsSkipped: []
|
|
58
289
|
};
|
|
59
290
|
for (const item of order) for (const file of item.files) {
|
|
60
291
|
const target = join(dir, file.path);
|
|
@@ -74,11 +305,499 @@ async function add(names, options) {
|
|
|
74
305
|
report.written.push(shown);
|
|
75
306
|
}
|
|
76
307
|
if (!saved) {
|
|
77
|
-
|
|
308
|
+
writeConfig(cwd, { components: relative(cwd, dir) || "." });
|
|
78
309
|
report.configWritten = true;
|
|
79
310
|
}
|
|
311
|
+
const needs = /* @__PURE__ */ new Map();
|
|
312
|
+
for (const item of order) for (const f of item.fonts ?? []) if (!needs.has(f.font)) needs.set(f.font, {
|
|
313
|
+
why: f.why,
|
|
314
|
+
by: item.name
|
|
315
|
+
});
|
|
316
|
+
for (const [role, need] of needs) {
|
|
317
|
+
if (options.fonts === false) {
|
|
318
|
+
report.fontsSkipped.push({
|
|
319
|
+
role,
|
|
320
|
+
...need
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const done = await installFont(role, {
|
|
325
|
+
cwd,
|
|
326
|
+
dir: options.fontsDir,
|
|
327
|
+
overwrite: options.overwrite,
|
|
328
|
+
fetch: options.fetch,
|
|
329
|
+
catalog: options.fontCatalog,
|
|
330
|
+
onDownload: options.onDownload
|
|
331
|
+
});
|
|
332
|
+
report.fonts.push({
|
|
333
|
+
...done,
|
|
334
|
+
...need
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (report.fonts.length > 0 || readConfig(cwd).config.fonts) report.fontsModule = writeFontsModule(cwd, dir, options.fontCatalog);
|
|
338
|
+
return report;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/cli/templates.ts
|
|
343
|
+
/**
|
|
344
|
+
* The files `framefields init` writes: the project layout the framefields
|
|
345
|
+
* setup skill describes. `film.ts` only builds; `render.ts` renders frames,
|
|
346
|
+
* a grid, the live preview or the video; `theme.ts` holds the numbers.
|
|
347
|
+
*/
|
|
348
|
+
/**
|
|
349
|
+
* Packages with install scripts. framefields needs exactly one (skia-canvas
|
|
350
|
+
* downloads its native binary); the others work without theirs.
|
|
351
|
+
*/
|
|
352
|
+
const ALLOWED_SCRIPTS = ["skia-canvas"];
|
|
353
|
+
const DENIED_SCRIPTS = [
|
|
354
|
+
"sharp",
|
|
355
|
+
"webgpu",
|
|
356
|
+
"onnxruntime-node",
|
|
357
|
+
"node-av",
|
|
358
|
+
"esbuild",
|
|
359
|
+
"protobufjs"
|
|
360
|
+
];
|
|
361
|
+
/** Dev tools the scripts use, at versions the scaffold is tested with. */
|
|
362
|
+
const DEV_DEPENDENCIES = {
|
|
363
|
+
"@types/node": "^24.1.0",
|
|
364
|
+
tsx: "^4.20.3",
|
|
365
|
+
typescript: "^5.8.3"
|
|
366
|
+
};
|
|
367
|
+
const SCRIPTS = {
|
|
368
|
+
typecheck: "tsc --noEmit -p .",
|
|
369
|
+
frames: "tsx src/render.ts frames",
|
|
370
|
+
grid: "tsx src/render.ts grid",
|
|
371
|
+
preview: "tsx src/render.ts preview",
|
|
372
|
+
render: "tsx src/render.ts"
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* The `package.json` fields a framefields project needs. npm reads
|
|
376
|
+
* `allowScripts`, pnpm its `pnpm` block, bun `trustedDependencies`.
|
|
377
|
+
*/
|
|
378
|
+
function packageFields(o) {
|
|
379
|
+
return {
|
|
380
|
+
type: "module",
|
|
381
|
+
...o.packageManager && { packageManager: o.packageManager },
|
|
382
|
+
scripts: SCRIPTS,
|
|
383
|
+
dependencies: { framefields: `^${o.framefieldsVersion}` },
|
|
384
|
+
devDependencies: DEV_DEPENDENCIES,
|
|
385
|
+
allowScripts: Object.fromEntries([...ALLOWED_SCRIPTS.map((p) => [p, true]), ...DENIED_SCRIPTS.map((p) => [p, false])]),
|
|
386
|
+
pnpm: {
|
|
387
|
+
onlyBuiltDependencies: ALLOWED_SCRIPTS,
|
|
388
|
+
ignoredBuiltDependencies: DENIED_SCRIPTS
|
|
389
|
+
},
|
|
390
|
+
trustedDependencies: ALLOWED_SCRIPTS
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const TSCONFIG = `${JSON.stringify({
|
|
394
|
+
compilerOptions: {
|
|
395
|
+
module: "NodeNext",
|
|
396
|
+
moduleResolution: "NodeNext",
|
|
397
|
+
target: "ES2022",
|
|
398
|
+
strict: true,
|
|
399
|
+
skipLibCheck: true,
|
|
400
|
+
noEmit: true,
|
|
401
|
+
types: ["node"]
|
|
402
|
+
},
|
|
403
|
+
include: ["src/**/*"]
|
|
404
|
+
}, null, " ")}\n`;
|
|
405
|
+
/** Lines `init` makes sure `.gitignore` has. */
|
|
406
|
+
const GITIGNORE = [
|
|
407
|
+
"node_modules/",
|
|
408
|
+
"output/",
|
|
409
|
+
"scratch/",
|
|
410
|
+
".DS_Store",
|
|
411
|
+
".env"
|
|
412
|
+
];
|
|
413
|
+
/** Where the AI gateway's keys come from. */
|
|
414
|
+
const AI_DASHBOARD = "https://framefields.dev/dashboard";
|
|
415
|
+
/** Optional settings; `render.ts` loads `.env` when there is one. Commit this, not `.env`. */
|
|
416
|
+
const ENV_EXAMPLE = `# Copy to .env (git-ignored). Everything here is optional.
|
|
417
|
+
|
|
418
|
+
# framefields AI gateway: generate images, video, music, voice and 3D from code.
|
|
419
|
+
# Create a key at ${AI_DASHBOARD}
|
|
420
|
+
FRAMEFIELDS_API_KEY=
|
|
421
|
+
`;
|
|
422
|
+
/** Media, LUTs and fonts go to Git LFS; renders stay out of Git. */
|
|
423
|
+
const GITATTRIBUTES = [
|
|
424
|
+
"assets/**/*.mp4 filter=lfs diff=lfs merge=lfs -text",
|
|
425
|
+
"assets/**/*.mov filter=lfs diff=lfs merge=lfs -text",
|
|
426
|
+
"assets/**/*.webm filter=lfs diff=lfs merge=lfs -text",
|
|
427
|
+
"assets/**/*.mp3 filter=lfs diff=lfs merge=lfs -text",
|
|
428
|
+
"assets/**/*.wav filter=lfs diff=lfs merge=lfs -text",
|
|
429
|
+
"assets/**/*.cube filter=lfs diff=lfs merge=lfs -text",
|
|
430
|
+
"assets/**/*.lottie filter=lfs diff=lfs merge=lfs -text",
|
|
431
|
+
"assets/fonts/**/*.ttf filter=lfs diff=lfs merge=lfs -text",
|
|
432
|
+
"assets/fonts/**/*.otf filter=lfs diff=lfs merge=lfs -text"
|
|
433
|
+
];
|
|
434
|
+
const THEME_TS = `import { registerFonts as registerInstalledFonts } from "./components/fonts.js";
|
|
435
|
+
|
|
436
|
+
export const W = 1920;
|
|
437
|
+
export const H = 1080;
|
|
438
|
+
export const FPS = 30;
|
|
439
|
+
export const INK = "#0E0D0C";
|
|
440
|
+
export const PAPER = "#EEE9E0";
|
|
441
|
+
/** Installed by \`framefields init\` (assets/fonts), registered below. */
|
|
442
|
+
export const SANS = "Inter";
|
|
443
|
+
export const MONO = "JetBrains Mono";
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Register the film's fonts. Fonts framefields installed
|
|
447
|
+
* (\`npx framefields fonts add <role>\`) come from the generated
|
|
448
|
+
* components/fonts.ts; register any others here with \`FontManager.register\`.
|
|
449
|
+
*/
|
|
450
|
+
export async function registerFonts(): Promise<void> {
|
|
451
|
+
await registerInstalledFonts();
|
|
452
|
+
}
|
|
453
|
+
`;
|
|
454
|
+
const FILM_TS = `import { Composition, Layer, LayerAnimation } from "framefields";
|
|
455
|
+
import { FPS, H, INK, PAPER, registerFonts, SANS, W } from "./theme.js";
|
|
456
|
+
|
|
457
|
+
const DURATION = 3 * FPS; // frames
|
|
458
|
+
|
|
459
|
+
/** Builds the film. Only builds: no rendering or writes, so tests, the preview and renders share it. */
|
|
460
|
+
export async function buildFilm(): Promise<Composition> {
|
|
461
|
+
// Fonts must be registered before any text layer is built.
|
|
462
|
+
await registerFonts();
|
|
463
|
+
|
|
464
|
+
const film = new Composition({
|
|
465
|
+
width: W,
|
|
466
|
+
height: H,
|
|
467
|
+
fps: FPS,
|
|
468
|
+
durationMs: (DURATION / FPS) * 1000,
|
|
469
|
+
backgroundColor: INK,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
film.add(
|
|
473
|
+
Layer.text("Hello, framefields", {
|
|
474
|
+
id: "title",
|
|
475
|
+
position: "absolute",
|
|
476
|
+
x: 0,
|
|
477
|
+
y: H / 2 - 60,
|
|
478
|
+
width: W,
|
|
479
|
+
height: 120,
|
|
480
|
+
fontFamily: SANS,
|
|
481
|
+
fontSize: 96,
|
|
482
|
+
fontWeight: 700,
|
|
483
|
+
fill: PAPER,
|
|
484
|
+
align: "center",
|
|
485
|
+
}).animate(
|
|
486
|
+
LayerAnimation.create()
|
|
487
|
+
.fadeIn(0, 20, "power2.out")
|
|
488
|
+
.fromTo("y", H / 2 - 20, H / 2 - 60, {
|
|
489
|
+
start: 0,
|
|
490
|
+
end: 30,
|
|
491
|
+
ease: "expo.out",
|
|
492
|
+
}),
|
|
493
|
+
),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
return film;
|
|
497
|
+
}
|
|
498
|
+
`;
|
|
499
|
+
const RENDER_TS = `import { existsSync } from "node:fs";
|
|
500
|
+
import fs from "node:fs/promises";
|
|
501
|
+
import path from "node:path";
|
|
502
|
+
import { HeadlessMediaRenderer, startPreview } from "framefields";
|
|
503
|
+
import { buildFilm } from "./film.js";
|
|
504
|
+
|
|
505
|
+
// Optional settings such as FRAMEFIELDS_API_KEY (see .env.example).
|
|
506
|
+
const ENV = path.resolve(import.meta.dirname, "../.env");
|
|
507
|
+
if (existsSync(ENV)) process.loadEnvFile(ENV);
|
|
508
|
+
|
|
509
|
+
const OUT = path.resolve(import.meta.dirname, "../output");
|
|
510
|
+
// \`npm run frames -- 0 30\` needs the \`--\`; pnpm passes it through: drop it.
|
|
511
|
+
const [mode = "video", ...args] = process.argv
|
|
512
|
+
.slice(2)
|
|
513
|
+
.filter((a) => a !== "--");
|
|
514
|
+
|
|
515
|
+
if (mode === "preview") {
|
|
516
|
+
// The page runs film.ts itself and renders it with WebGPU in the browser.
|
|
517
|
+
const session = await startPreview({
|
|
518
|
+
entry: new URL("./film.ts", import.meta.url),
|
|
519
|
+
export: "buildFilm",
|
|
520
|
+
});
|
|
521
|
+
console.log(\`Preview at \${session.url}\`);
|
|
522
|
+
await session.closed; // until its tab closes or the next preview replaces it
|
|
523
|
+
process.exit(0);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const film = await buildFilm();
|
|
527
|
+
await fs.mkdir(OUT, { recursive: true });
|
|
528
|
+
|
|
529
|
+
if (mode === "frames") {
|
|
530
|
+
const renderer = new HeadlessMediaRenderer();
|
|
531
|
+
for (const frame of (args.length ? args : ["0"]).map(Number)) {
|
|
532
|
+
const png = await film.renderFrame({ frame, renderer });
|
|
533
|
+
const file = path.join(OUT, \`f\${String(frame).padStart(4, "0")}.png\`);
|
|
534
|
+
await fs.writeFile(file, png);
|
|
535
|
+
console.log(\`Wrote \${path.relative(process.cwd(), file)}\`);
|
|
536
|
+
}
|
|
537
|
+
} else if (mode === "grid") {
|
|
538
|
+
const png = await film.renderFrameGrid({
|
|
539
|
+
frames: args.length ? args.map(Number) : [0, 10, 20, 30, 45, 60, 75, 89],
|
|
540
|
+
columns: 4,
|
|
541
|
+
cellWidth: 480,
|
|
542
|
+
showLabels: true,
|
|
543
|
+
});
|
|
544
|
+
await fs.writeFile(path.join(OUT, "grid.png"), png);
|
|
545
|
+
console.log("Wrote output/grid.png");
|
|
546
|
+
} else {
|
|
547
|
+
const result = await film.renderVideo({
|
|
548
|
+
outputPath: path.join(OUT, "output.mp4"),
|
|
549
|
+
quality: "high",
|
|
550
|
+
});
|
|
551
|
+
await result.cleanup?.();
|
|
552
|
+
console.log(\`Exported \${result.filePath}\`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Native GPU and encoder threads keep Node alive; always exit explicitly.
|
|
556
|
+
process.exit(0);
|
|
557
|
+
`;
|
|
558
|
+
function readme(name, run) {
|
|
559
|
+
return `# ${name}
|
|
560
|
+
|
|
561
|
+
A film built with [framefields](https://www.npmjs.com/package/framefields).
|
|
562
|
+
|
|
563
|
+
\`\`\`bash
|
|
564
|
+
${run("typecheck")}
|
|
565
|
+
${run("frames")} -- 0 30 60 # PNGs in output/
|
|
566
|
+
${run("grid")} # a contact sheet of the motion
|
|
567
|
+
${run("preview")} # play it in the browser
|
|
568
|
+
${run("render")} # output/output.mp4
|
|
569
|
+
\`\`\`
|
|
570
|
+
|
|
571
|
+
- \`src/film.ts\` builds the composition, \`src/theme.ts\` holds sizes, colors and fonts,
|
|
572
|
+
\`src/render.ts\` renders it.
|
|
573
|
+
- \`npx framefields list\` shows ready-made components; \`npx framefields add <name>\` copies
|
|
574
|
+
one into \`src/components/\`.
|
|
575
|
+
- Optional: the AI gateway generates images, video, music, voice and 3D from code. Copy
|
|
576
|
+
\`.env.example\` to \`.env\` and set \`FRAMEFIELDS_API_KEY\` (keys at ${AI_DASHBOARD}).
|
|
577
|
+
- Footage, audio and fonts live in \`assets/\` and are tracked with Git LFS
|
|
578
|
+
(\`git lfs install\` once per machine, \`git lfs pull\` after cloning).
|
|
579
|
+
`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/cli/init.ts
|
|
584
|
+
/**
|
|
585
|
+
* `framefields init [dir]`: a new framefields project, or framefields added
|
|
586
|
+
* to an existing one. Writes the layout the setup skill describes
|
|
587
|
+
* (`package.json` with the install-script approvals framefields needs,
|
|
588
|
+
* `tsconfig.json`, `src/theme.ts` / `film.ts` / `render.ts`, `assets/`,
|
|
589
|
+
* LFS `.gitattributes`, `.gitignore`), installs the themes' fonts and keeps
|
|
590
|
+
* them registered in `src/components/fonts.ts`.
|
|
591
|
+
*
|
|
592
|
+
* It never overwrites: files that exist are left alone, and `package.json`,
|
|
593
|
+
* `.gitignore` and `.gitattributes` gain only what they're missing.
|
|
594
|
+
*/
|
|
595
|
+
const PACKAGE_MANAGERS = [
|
|
596
|
+
"npm",
|
|
597
|
+
"pnpm",
|
|
598
|
+
"yarn",
|
|
599
|
+
"bun"
|
|
600
|
+
];
|
|
601
|
+
/** The package manager in `npm_config_user_agent` (`pnpm/10.13.1 npm/? node/v22 …`). */
|
|
602
|
+
function detectPackageManager(userAgent) {
|
|
603
|
+
const m = userAgent?.match(/^(npm|pnpm|yarn|bun)\/(\d[\w.+-]*)/);
|
|
604
|
+
if (!m) return null;
|
|
605
|
+
return {
|
|
606
|
+
name: m[1],
|
|
607
|
+
version: m[2]
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
const runScriptFor = (pm) => (script) => pm === "npm" || pm === "bun" ? `${pm} run ${script}` : `${pm} ${script}`;
|
|
611
|
+
/** `My Film!` → `my-film`. */
|
|
612
|
+
function packageName(folder) {
|
|
613
|
+
return folder.toLowerCase().replace(/[^a-z0-9._~-]+/g, "-").replace(/^[-._]+|[-._]+$/g, "") || "framefields-film";
|
|
614
|
+
}
|
|
615
|
+
async function init(options) {
|
|
616
|
+
const root = resolve(options.cwd, options.dir ?? ".");
|
|
617
|
+
mkdirSync(root, { recursive: true });
|
|
618
|
+
const detected = detectPackageManager(options.userAgent);
|
|
619
|
+
const pm = options.packageManager ?? detected?.name ?? "npm";
|
|
620
|
+
const pmVersion = detected?.name === pm ? detected.version : void 0;
|
|
621
|
+
const run = options.run ?? spawnRunner;
|
|
622
|
+
const report = {
|
|
623
|
+
root,
|
|
624
|
+
name: "",
|
|
625
|
+
packageManager: pm,
|
|
626
|
+
written: [],
|
|
627
|
+
merged: [],
|
|
628
|
+
unchanged: [],
|
|
629
|
+
fonts: [],
|
|
630
|
+
fontsModule: {
|
|
631
|
+
path: "",
|
|
632
|
+
status: "unchanged",
|
|
633
|
+
calls: []
|
|
634
|
+
},
|
|
635
|
+
git: "skipped",
|
|
636
|
+
lfs: null,
|
|
637
|
+
installed: false,
|
|
638
|
+
runScript: runScriptFor(pm)
|
|
639
|
+
};
|
|
640
|
+
const fields = packageFields({
|
|
641
|
+
framefieldsVersion: options.framefieldsVersion ?? ownVersion(),
|
|
642
|
+
...pmVersion && { packageManager: `${pm}@${pmVersion}` }
|
|
643
|
+
});
|
|
644
|
+
const pkgPath = join(root, "package.json");
|
|
645
|
+
if (existsSync(pkgPath)) {
|
|
646
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
647
|
+
if (pkg.type === "commonjs") throw new Error("package.json has \"type\": \"commonjs\". framefields projects are ES modules: set \"type\": \"module\" (and convert any require() files) first.");
|
|
648
|
+
report.name = typeof pkg.name === "string" ? pkg.name : packageName(basename(root));
|
|
649
|
+
record(report, "package.json", writeIfChanged(pkgPath, mergePackage(pkg, fields), pkg));
|
|
650
|
+
} else {
|
|
651
|
+
report.name = options.name ?? packageName(basename(root));
|
|
652
|
+
writeJson(pkgPath, {
|
|
653
|
+
name: report.name,
|
|
654
|
+
version: "0.0.0",
|
|
655
|
+
private: true,
|
|
656
|
+
...fields
|
|
657
|
+
});
|
|
658
|
+
report.written.push("package.json");
|
|
659
|
+
}
|
|
660
|
+
const files = [
|
|
661
|
+
["tsconfig.json", TSCONFIG],
|
|
662
|
+
["src/theme.ts", THEME_TS],
|
|
663
|
+
["src/film.ts", FILM_TS],
|
|
664
|
+
["src/render.ts", RENDER_TS],
|
|
665
|
+
["assets/footage/.gitkeep", ""],
|
|
666
|
+
[".env.example", ENV_EXAMPLE],
|
|
667
|
+
["README.md", readme(report.name, report.runScript)]
|
|
668
|
+
];
|
|
669
|
+
for (const [path$1, content] of files) {
|
|
670
|
+
const target = join(root, path$1);
|
|
671
|
+
if (existsSync(target)) report.unchanged.push(path$1);
|
|
672
|
+
else {
|
|
673
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
674
|
+
writeFileSync(target, content);
|
|
675
|
+
report.written.push(path$1);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
for (const [path$1, lines] of [[".gitignore", GITIGNORE], [".gitattributes", GITATTRIBUTES]]) record(report, path$1, appendLines(join(root, path$1), lines));
|
|
679
|
+
const { config, saved } = readConfig(root);
|
|
680
|
+
if (!saved) {
|
|
681
|
+
writeConfig(root, config);
|
|
682
|
+
report.written.push("framefields.json");
|
|
683
|
+
}
|
|
684
|
+
const roles = [...options.fonts === false ? [] : ["sans", "mono"], ...options.emoji ? ["emoji"] : []];
|
|
685
|
+
for (const role of roles) report.fonts.push(await installFont(role, {
|
|
686
|
+
cwd: root,
|
|
687
|
+
fetch: options.fetch,
|
|
688
|
+
catalog: options.fontCatalog,
|
|
689
|
+
onDownload: options.onDownload
|
|
690
|
+
}));
|
|
691
|
+
report.fontsModule = writeFontsModule(root, resolve(root, readConfig(root).config.components), options.fontCatalog);
|
|
692
|
+
if (options.git !== false) {
|
|
693
|
+
if (await run("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
694
|
+
cwd: root,
|
|
695
|
+
quiet: true
|
|
696
|
+
}) === 0) report.git = "existing";
|
|
697
|
+
else if (await run("git", ["init", "-q"], {
|
|
698
|
+
cwd: root,
|
|
699
|
+
quiet: true
|
|
700
|
+
}) === 0) report.git = "initialized";
|
|
701
|
+
else report.git = "unavailable";
|
|
702
|
+
if (report.git !== "unavailable") report.lfs = await run("git", ["lfs", "version"], {
|
|
703
|
+
cwd: root,
|
|
704
|
+
quiet: true
|
|
705
|
+
}) === 0;
|
|
706
|
+
}
|
|
707
|
+
if (options.install) {
|
|
708
|
+
const code = await run(pm, ["install"], { cwd: root });
|
|
709
|
+
if (code !== 0) throw new Error(`\`${pm} install\` failed (exit ${code}). The project files are written; fix the error and run it again.`);
|
|
710
|
+
report.installed = true;
|
|
711
|
+
}
|
|
80
712
|
return report;
|
|
81
713
|
}
|
|
714
|
+
/** Add every field of `fields` the package lacks; existing values win. */
|
|
715
|
+
function mergePackage(pkg, fields) {
|
|
716
|
+
const out = { ...pkg };
|
|
717
|
+
const obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
718
|
+
const union = (a, b) => [...new Set([...Array.isArray(a) ? a : [], ...b])];
|
|
719
|
+
out.type ??= fields.type;
|
|
720
|
+
if (fields.packageManager) out.packageManager ??= fields.packageManager;
|
|
721
|
+
for (const key of [
|
|
722
|
+
"scripts",
|
|
723
|
+
"dependencies",
|
|
724
|
+
"devDependencies",
|
|
725
|
+
"allowScripts"
|
|
726
|
+
]) {
|
|
727
|
+
const theirs = obj(pkg[key]);
|
|
728
|
+
const present = key === "dependencies" || key === "devDependencies" ? {
|
|
729
|
+
...obj(pkg.dependencies),
|
|
730
|
+
...obj(pkg.devDependencies)
|
|
731
|
+
} : theirs;
|
|
732
|
+
const missing = Object.entries(fields[key]).filter(([k]) => !(k in present));
|
|
733
|
+
if (missing.length > 0) out[key] = {
|
|
734
|
+
...theirs,
|
|
735
|
+
...Object.fromEntries(missing)
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
const pnpm = obj(pkg.pnpm);
|
|
739
|
+
out.pnpm = {
|
|
740
|
+
...pnpm,
|
|
741
|
+
onlyBuiltDependencies: union(pnpm.onlyBuiltDependencies, fields.pnpm.onlyBuiltDependencies),
|
|
742
|
+
ignoredBuiltDependencies: union(pnpm.ignoredBuiltDependencies, fields.pnpm.ignoredBuiltDependencies)
|
|
743
|
+
};
|
|
744
|
+
out.trustedDependencies = union(pkg.trustedDependencies, fields.trustedDependencies);
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
function writeJson(path$1, value) {
|
|
748
|
+
writeFileSync(path$1, `${JSON.stringify(value, null, " ")}\n`);
|
|
749
|
+
}
|
|
750
|
+
function writeIfChanged(path$1, next, prev) {
|
|
751
|
+
if (JSON.stringify(next) === JSON.stringify(prev)) return false;
|
|
752
|
+
writeJson(path$1, next);
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
/** Create `path` with `lines` ("written"), or append the ones it lacks (true if any). */
|
|
756
|
+
function appendLines(path$1, lines) {
|
|
757
|
+
if (!existsSync(path$1)) {
|
|
758
|
+
writeFileSync(path$1, `${lines.join("\n")}\n`);
|
|
759
|
+
return "written";
|
|
760
|
+
}
|
|
761
|
+
const text = readFileSync(path$1, "utf8");
|
|
762
|
+
const have = new Set(text.split(/\r?\n/).map((l) => l.trim()));
|
|
763
|
+
const missing = lines.filter((l) => !have.has(l));
|
|
764
|
+
if (missing.length === 0) return false;
|
|
765
|
+
writeFileSync(path$1, `${text}${text.endsWith("\n") || text === "" ? "" : "\n"}${missing.join("\n")}\n`);
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
/** File it under written (new), merged (changed) or unchanged. */
|
|
769
|
+
function record(report, path$1, result) {
|
|
770
|
+
if (result === "written") report.written.push(path$1);
|
|
771
|
+
else if (result) report.merged.push(path$1);
|
|
772
|
+
else report.unchanged.push(path$1);
|
|
773
|
+
}
|
|
774
|
+
/** This package's version, from the nearest `package.json` named framefields. */
|
|
775
|
+
function ownVersion() {
|
|
776
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
777
|
+
for (;;) {
|
|
778
|
+
const file = join(dir, "package.json");
|
|
779
|
+
if (existsSync(file)) {
|
|
780
|
+
const pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
781
|
+
if (pkg.name === "framefields" && pkg.version) return pkg.version;
|
|
782
|
+
}
|
|
783
|
+
const up = dirname(dir);
|
|
784
|
+
if (up === dir) throw new Error("Can't find framefields' own package.json");
|
|
785
|
+
dir = up;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const spawnRunner = (command, args, { cwd, quiet }) => new Promise((done) => {
|
|
789
|
+
const child = spawn(command, args, {
|
|
790
|
+
cwd,
|
|
791
|
+
stdio: quiet ? "ignore" : "inherit",
|
|
792
|
+
shell: process.platform === "win32"
|
|
793
|
+
});
|
|
794
|
+
child.on("error", () => done(127));
|
|
795
|
+
child.on("close", (code) => done(code ?? 1));
|
|
796
|
+
});
|
|
797
|
+
/** Where the project is, as the user would `cd` to it. */
|
|
798
|
+
function shownPath(cwd, root) {
|
|
799
|
+
return relative(cwd, root) || ".";
|
|
800
|
+
}
|
|
82
801
|
|
|
83
802
|
//#endregion
|
|
84
803
|
//#region src/cli/registry.ts
|
|
@@ -125,29 +844,55 @@ function openRegistry(source, cwd = process.cwd()) {
|
|
|
125
844
|
/**
|
|
126
845
|
* The `framefields` command.
|
|
127
846
|
*
|
|
128
|
-
* npx framefields
|
|
129
|
-
* npx framefields
|
|
847
|
+
* npx framefields init [dir] start a project (or add framefields to one)
|
|
848
|
+
* npx framefields add <name...> copy components into this project
|
|
849
|
+
* npx framefields list the components you can add
|
|
850
|
+
* npx framefields fonts add <role...> install fonts (emoji)
|
|
851
|
+
* npx framefields fonts list the fonts you can install
|
|
130
852
|
*/
|
|
131
853
|
const HELP = `Usage: framefields <command> [options]
|
|
132
854
|
|
|
133
855
|
Commands:
|
|
856
|
+
init [dir] Start a framefields project in dir (default: here), or add
|
|
857
|
+
framefields to the project there: package.json, tsconfig,
|
|
858
|
+
src/theme.ts, film.ts, render.ts, assets/, .gitignore,
|
|
859
|
+
LFS .gitattributes, and the themes' fonts. Never
|
|
860
|
+
overwrites; existing files only gain what they lack.
|
|
134
861
|
add <name...> Copy components' source into this project (and the
|
|
135
862
|
components they build on). The code is yours to edit.
|
|
136
863
|
list List the components you can add.
|
|
864
|
+
fonts add <role...>
|
|
865
|
+
Install fonts (pinned, hash-checked, with their license)
|
|
866
|
+
and register them in the generated <components>/fonts.ts.
|
|
867
|
+
fonts list List the fonts you can install.
|
|
868
|
+
|
|
869
|
+
Options for init:
|
|
870
|
+
--name <name> Package name (default: the folder's name).
|
|
871
|
+
--pm <manager> npm, pnpm, yarn or bun (default: the one running this).
|
|
872
|
+
--install Install dependencies afterwards.
|
|
873
|
+
--emoji Also install the color emoji font.
|
|
874
|
+
--no-fonts Skip Inter and JetBrains Mono.
|
|
875
|
+
--no-git Don't run \`git init\`.
|
|
137
876
|
|
|
138
877
|
Options for add:
|
|
139
878
|
--all Add every component.
|
|
140
879
|
--dir <path> Components directory (default: from framefields.json,
|
|
141
880
|
else src/components or components). Saved the first time.
|
|
142
881
|
--overwrite Replace files you have changed.
|
|
882
|
+
--no-fonts Don't install the fonts components ask for (emoji).
|
|
883
|
+
|
|
884
|
+
Options for add and fonts add:
|
|
885
|
+
--fonts-dir <path> Where downloaded fonts go (default assets/fonts).
|
|
143
886
|
|
|
144
|
-
Options for
|
|
887
|
+
Options for add and list:
|
|
145
888
|
--registry <src> A registry folder or http(s) URL instead of the one
|
|
146
889
|
shipped with this version of framefields.
|
|
147
890
|
-h, --help Show this help.
|
|
148
891
|
|
|
149
|
-
|
|
892
|
+
Examples:
|
|
893
|
+
npx framefields init my-film --install
|
|
150
894
|
npx framefields add mouse-pointer focus-zoom
|
|
895
|
+
npx framefields fonts add emoji
|
|
151
896
|
`;
|
|
152
897
|
async function main(argv) {
|
|
153
898
|
const { values, positionals } = parseArgs({
|
|
@@ -157,6 +902,13 @@ async function main(argv) {
|
|
|
157
902
|
all: { type: "boolean" },
|
|
158
903
|
dir: { type: "string" },
|
|
159
904
|
overwrite: { type: "boolean" },
|
|
905
|
+
"no-fonts": { type: "boolean" },
|
|
906
|
+
"fonts-dir": { type: "string" },
|
|
907
|
+
name: { type: "string" },
|
|
908
|
+
pm: { type: "string" },
|
|
909
|
+
install: { type: "boolean" },
|
|
910
|
+
emoji: { type: "boolean" },
|
|
911
|
+
"no-git": { type: "boolean" },
|
|
160
912
|
registry: { type: "string" },
|
|
161
913
|
help: {
|
|
162
914
|
type: "boolean",
|
|
@@ -170,6 +922,31 @@ async function main(argv) {
|
|
|
170
922
|
return command || values.help ? 0 : 1;
|
|
171
923
|
}
|
|
172
924
|
const cwd = process.cwd();
|
|
925
|
+
const log = (line) => process.stdout.write(`${line}\n`);
|
|
926
|
+
if (command === "fonts") return fonts(names, cwd, values, log);
|
|
927
|
+
if (command === "init") {
|
|
928
|
+
if (names.length > 1) {
|
|
929
|
+
process.stderr.write("init takes one folder: `framefields init my-film`.\n");
|
|
930
|
+
return 1;
|
|
931
|
+
}
|
|
932
|
+
if (values.pm && !PACKAGE_MANAGERS.includes(values.pm)) {
|
|
933
|
+
process.stderr.write(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}.\n`);
|
|
934
|
+
return 1;
|
|
935
|
+
}
|
|
936
|
+
logInit(await init({
|
|
937
|
+
cwd,
|
|
938
|
+
dir: names[0],
|
|
939
|
+
name: values.name,
|
|
940
|
+
packageManager: values.pm,
|
|
941
|
+
userAgent: process.env.npm_config_user_agent,
|
|
942
|
+
fonts: !values["no-fonts"],
|
|
943
|
+
emoji: values.emoji,
|
|
944
|
+
git: !values["no-git"],
|
|
945
|
+
install: values.install,
|
|
946
|
+
onDownload
|
|
947
|
+
}), cwd, log);
|
|
948
|
+
return 0;
|
|
949
|
+
}
|
|
173
950
|
const registry = openRegistry(values.registry, cwd);
|
|
174
951
|
if (command === "list") {
|
|
175
952
|
const index = await registry.index();
|
|
@@ -190,13 +967,16 @@ async function main(argv) {
|
|
|
190
967
|
cwd,
|
|
191
968
|
registry,
|
|
192
969
|
dir: values.dir,
|
|
193
|
-
overwrite: values.overwrite
|
|
970
|
+
overwrite: values.overwrite,
|
|
971
|
+
fonts: !values["no-fonts"],
|
|
972
|
+
fontsDir: values["fonts-dir"],
|
|
973
|
+
onDownload
|
|
194
974
|
});
|
|
195
|
-
const log = (line) => process.stdout.write(`${line}\n`);
|
|
196
975
|
for (const f of report.written) log(` wrote ${f}`);
|
|
197
976
|
for (const f of report.unchanged) log(` unchanged ${f}`);
|
|
198
977
|
for (const f of report.skipped) log(` skipped ${f} (yours differs; --overwrite replaces it)`);
|
|
199
978
|
if (report.configWritten) log(" wrote framefields.json");
|
|
979
|
+
logFonts(report, cwd, log);
|
|
200
980
|
const extra = report.components.filter((c) => !wanted.includes(c.name));
|
|
201
981
|
if (extra.length > 0) log(`\nWith what they build on: ${extra.map((c) => c.name).join(", ")}.`);
|
|
202
982
|
warnIfMissingFramefields(cwd, log);
|
|
@@ -204,6 +984,76 @@ async function main(argv) {
|
|
|
204
984
|
if (first) log(`\nimport { ${first.title} } from "./${first.name}/index.js"; // from ${relative(cwd, report.dir) || "."}`);
|
|
205
985
|
return 0;
|
|
206
986
|
}
|
|
987
|
+
const onDownload = (spec) => process.stdout.write(` fetching ${spec.file} (${(spec.font.bytes / 1e6).toFixed(1)} MB, ${spec.license.id})\n`);
|
|
988
|
+
/** What `add` did about fonts, and how to use them. */
|
|
989
|
+
function logFonts(report, cwd, log) {
|
|
990
|
+
for (const f of report.fonts) log(` ${f.status === "installed" ? "font " : "font ok "} ${f.path} (${f.role}, for ${f.by}: ${f.why})`);
|
|
991
|
+
for (const f of report.fontsSkipped) log(` no font ${f.role} for ${f.by} (--no-fonts): ${f.why} Install later with \`framefields fonts add ${f.role}\`.`);
|
|
992
|
+
if (report.fontsModule) logFontsModule(report.fontsModule, log);
|
|
993
|
+
const installed = report.fonts.find((f) => f.status === "installed");
|
|
994
|
+
const hint = installed && lfsHint(cwd, installed.path);
|
|
995
|
+
if (hint) log(`\n${hint}`);
|
|
996
|
+
if (report.fonts.some((f) => f.status !== "configured") && report.fontsModule) log(`\nRegister them once before rendering: \`await registerFonts()\` from ${report.fontsModule.path.replace(/\.ts$/, ".js")}.`);
|
|
997
|
+
}
|
|
998
|
+
function logFontsModule(m, log) {
|
|
999
|
+
if (m.status === "written") log(` wrote ${m.path}`);
|
|
1000
|
+
else if (m.status === "unchanged") log(` unchanged ${m.path}`);
|
|
1001
|
+
else log(` skipped ${m.path} (not generated by framefields). Register the fonts yourself:\n${m.calls.map((c) => ` ${c}`).join("\n")}`);
|
|
1002
|
+
}
|
|
1003
|
+
/** `framefields fonts add <role...>` and `framefields fonts list`. */
|
|
1004
|
+
async function fonts([sub, ...roles], cwd, values, log) {
|
|
1005
|
+
if (sub === "list") {
|
|
1006
|
+
const { config: config$1 } = readConfig(cwd);
|
|
1007
|
+
for (const [role, spec] of Object.entries(FONTS)) log(`${role.padEnd(6)} ${spec.label}${config$1.fonts?.[role] ? ` (installed: ${config$1.fonts[role]})` : ""}`);
|
|
1008
|
+
return 0;
|
|
1009
|
+
}
|
|
1010
|
+
if (sub !== "add" || roles.length === 0) {
|
|
1011
|
+
process.stderr.write(`Usage: framefields fonts add <role...> (roles: ${Object.keys(FONTS).join(", ")})\n framefields fonts list\n`);
|
|
1012
|
+
return 1;
|
|
1013
|
+
}
|
|
1014
|
+
const unknown = roles.filter((r) => !FONTS[r]);
|
|
1015
|
+
if (unknown.length > 0) {
|
|
1016
|
+
process.stderr.write(`Unknown font ${unknown.map((r) => `"${r}"`).join(", ")}. Fonts framefields can install: ${Object.keys(FONTS).join(", ")}.\n`);
|
|
1017
|
+
return 1;
|
|
1018
|
+
}
|
|
1019
|
+
const { config, saved } = readConfig(cwd);
|
|
1020
|
+
const installs = [];
|
|
1021
|
+
for (const role of roles) installs.push(await installFont(role, {
|
|
1022
|
+
cwd,
|
|
1023
|
+
dir: values["fonts-dir"],
|
|
1024
|
+
overwrite: values.overwrite,
|
|
1025
|
+
onDownload
|
|
1026
|
+
}));
|
|
1027
|
+
for (const f of installs) log(` ${f.status === "installed" ? "font " : "font ok "} ${f.path} (${f.role})`);
|
|
1028
|
+
if (!saved) log(" wrote framefields.json");
|
|
1029
|
+
const module = writeFontsModule(cwd, resolve(cwd, values.dir ?? config.components));
|
|
1030
|
+
logFontsModule(module, log);
|
|
1031
|
+
const installed = installs.find((f) => f.status === "installed");
|
|
1032
|
+
const hint = installed && lfsHint(cwd, installed.path);
|
|
1033
|
+
if (hint) log(`\n${hint}`);
|
|
1034
|
+
log(`\nRegister them once before rendering: \`await registerFonts()\` from ${module.path.replace(/\.ts$/, ".js")}.`);
|
|
1035
|
+
return 0;
|
|
1036
|
+
}
|
|
1037
|
+
function logInit(report, cwd, log) {
|
|
1038
|
+
const at = shownPath(cwd, report.root);
|
|
1039
|
+
for (const f of report.written) log(` wrote ${f}`);
|
|
1040
|
+
for (const f of report.merged) log(` merged ${f} (added what it lacked)`);
|
|
1041
|
+
for (const f of report.unchanged) log(` unchanged ${f}`);
|
|
1042
|
+
for (const f of report.fonts) log(` ${f.status === "installed" ? "font " : "font ok "} ${f.path} (${f.role})`);
|
|
1043
|
+
logFontsModule(report.fontsModule, log);
|
|
1044
|
+
if (report.git === "initialized") log(" git initialized a repository");
|
|
1045
|
+
if (report.git === "unavailable") log(" git not found: skipped `git init`");
|
|
1046
|
+
if (report.lfs === false) log("\nGit LFS isn't installed. Footage and fonts are tracked with it: install it (https://git-lfs.com), then run `git lfs install` once.");
|
|
1047
|
+
else if (report.lfs) log("\nRun `git lfs install` once on this machine if you haven't yet.");
|
|
1048
|
+
const steps = [
|
|
1049
|
+
...at === "." ? [] : [`cd ${/\s/.test(at) ? JSON.stringify(at) : at}`],
|
|
1050
|
+
...report.installed ? [] : [`${report.packageManager} install`],
|
|
1051
|
+
`${report.runScript("frames")} -- 0 30 60`
|
|
1052
|
+
];
|
|
1053
|
+
log(`\n${report.name} is ready. Next:\n${steps.map((s) => ` ${s}`).join("\n")}`);
|
|
1054
|
+
log("\nThen edit src/film.ts, and run `npx framefields list` for ready-made components.");
|
|
1055
|
+
log(`\nOptional: to generate images, video, music, voice and 3D with the framefields AI gateway, get a key at ${AI_DASHBOARD} and put it in .env as FRAMEFIELDS_API_KEY (see .env.example).`);
|
|
1056
|
+
}
|
|
207
1057
|
/** Installed components import `framefields`; say so if the project lacks it. */
|
|
208
1058
|
function warnIfMissingFramefields(cwd, log) {
|
|
209
1059
|
try {
|