diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f2869d..3f731df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,8 +16,8 @@ jobs: - name: Setup BEAM uses: erlef/setup-beam@v1 with: - otp-version: '26.0.2' - gleam-version: '1.4.1' + otp-version: '27.0.0' + gleam-version: '1.6.2' rebar3-version: '3' # elixir-version: "1.15.4" - name: Download gleam dependencies @@ -50,8 +50,8 @@ jobs: - name: Setup BEAM uses: erlef/setup-beam@v1 with: - otp-version: '26.0.2' - gleam-version: '1.4.1' + otp-version: '27.0.0' + gleam-version: '1.6.2' rebar3-version: '3' - name: Download gleam dependencies run: gleam deps download diff --git a/.mise.toml b/.mise.toml index a9ab64a..8e48dfe 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,6 +1,6 @@ # Runtimes used by the stack. [tools] -gleam = "1.4.1" +gleam = "1.6.2" erlang = "27" node = "22" deno = "latest" diff --git a/apps/backend/gleam.toml b/apps/backend/gleam.toml index 3a3c8df..5b4d239 100644 --- a/apps/backend/gleam.toml +++ b/apps/backend/gleam.toml @@ -2,30 +2,33 @@ name = "backend" version = "1.0.0" [dependencies] -aws4_request = ">= 0.1.1 and < 1.0.0" +aws4_request = ">= 1.2.0 and < 2.0.0" birl = "~> 1.6" chomp = ">= 0.1.0 and < 1.0.0" -cors_builder = ">= 2.0.0 and < 3.0.0" +cors_builder = ">= 2.0.2 and < 3.0.0" decipher = ">= 1.2.0 and < 2.0.0" +envoy = ">= 1.0.2 and < 2.0.0" gleam_erlang = "~> 0.25" gleam_hexpm = "~> 1.0" gleam_http = "~> 3.6" gleam_httpc = ">= 2.2.0 and < 3.0.0" -gleam_json = "~> 1.0" +gleam_json = ">= 2.1.0 and < 3.0.0" gleam_otp = "~> 0.10" gleam_package_interface = ">= 1.0.0 and < 2.0.0" -gleam_pgo = {path = "../../packages/pgo"} -gleam_stdlib = "~> 0.34 or ~> 1.0" +gleam_regexp = ">= 1.0.0 and < 2.0.0" +gleam_stdlib = ">= 0.44.0 and < 1.0.0" +gleam_yielder = ">= 1.1.0 and < 2.0.0" glexer = ">= 1.0.1 and < 2.0.0" -mist = ">= 1.2.0 and < 2.0.0" -pgo = "~> 0.14" +interfaces = {path = "../../packages/interfaces"} +mist = ">= 3.0.0 and < 4.0.0" +pog = ">= 1.0.1 and < 2.0.0" prng = ">= 3.0.3 and < 4.0.0" radiate = ">= 0.4.0 and < 1.0.0" ranger = ">= 1.2.0 and < 2.0.0" simplifile = ">= 2.1.0 and < 3.0.0" -tom = { path ="../../packages/tom" } +tom = ">= 1.1.0 and < 2.0.0" verl = ">= 1.1.1 and < 2.0.0" -wisp = ">= 1.1.0 and < 2.0.0" +wisp = ">= 1.3.0 and < 2.0.0" [dev-dependencies] gleeunit = "~> 1.0" diff --git a/apps/backend/manifest.toml b/apps/backend/manifest.toml index fe50b78..618c275 100644 --- a/apps/backend/manifest.toml +++ b/apps/backend/manifest.toml @@ -2,80 +2,85 @@ # You typically do not need to edit this file packages = [ - { name = "aws4_request", version = "0.1.1", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_http", "gleam_stdlib"], otp_app = "aws4_request", source = "hex", outer_checksum = "90B1DB6E2A7F0396CD4713850B14B3A910331B5BA76D051E411D1499AAA2EA9A" }, + { name = "aws4_request", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_http", "gleam_stdlib"], otp_app = "aws4_request", source = "hex", outer_checksum = "A73515D60E2D164B8B546778F9471ADE6089D33B2DC5D362C1B2CAF12FB6471B" }, { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "chomp", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "chomp", source = "hex", outer_checksum = "C87304897B4D4DEA69420DB2FF88B087673AAE9EC09CA8A0FBF4675F605767C2" }, - { name = "cors_builder", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "mist", "wisp"], otp_app = "cors_builder", source = "hex", outer_checksum = "E4634D1CAD89BC26B4FE986B45D55268BA5369E9224C2FD7872C0DB5AC028E47" }, - { name = "decipher", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_json", "gleam_stdlib", "stoiridh_version"], otp_app = "decipher", source = "hex", outer_checksum = "9F1B5C6FF0D798046E4E0EF87D09DD729324CB72BD7F0D4152B797324D51223E" }, + { name = "chomp", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "chomp", source = "hex", outer_checksum = "6DDC9EE76C5364E80F43A90A238351C25F627E59135AEF2506629112A7C4FD4F" }, + { name = "cors_builder", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "mist", "wisp"], otp_app = "cors_builder", source = "hex", outer_checksum = "8FB02BDB2F5E20B13C5A85B7B4F2A52E8C50A23F2AE798FA18A9CF2E2197CE7F" }, + { name = "decipher", version = "1.2.1", build_tools = ["gleam"], requirements = ["birl", "gleam_json", "gleam_stdlib", "stoiridh_version"], otp_app = "decipher", source = "hex", outer_checksum = "4F82516A5FF09BD7DF352DE38F1691C2254508066152F5DEA8665B216A9C9909" }, { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, - { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, + { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, { name = "filespy", version = "0.5.0", build_tools = ["gleam"], requirements = ["fs", "gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "filespy", source = "hex", outer_checksum = "F8E7A9C9CA86D68CCC25491125BFF36BEF7483892D7BEC24AA30D6B540504F06" }, { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, { name = "glam", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "66EC3BCD632E51EED029678F8DF419659C1E57B1A93D874C5131FE220DFAD2B2" }, { name = "gleam_bitwise", version = "1.3.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "B36E1D3188D7F594C7FD4F43D0D2CE17561DE896202017548578B16FE1FE9EFC" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, + { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, + { name = "gleam_erlang", version = "0.32.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "B18643083A0117AC5CFD0C1AEEBE5469071895ECFA426DCC26517A07F6AD9948" }, { name = "gleam_hexpm", version = "1.1.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], otp_app = "gleam_hexpm", source = "hex", outer_checksum = "D32439FD6AD683FE1094922737904EC2091E2D7B1F236AD23815935694A5221A" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, + { name = "gleam_http", version = "3.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "A9EE0722106FCCAB8AD3BF9D0A3EFF92BFE8561D59B83BAE96EB0BE1938D4E0F" }, + { name = "gleam_httpc", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF6CDD88830CC9853F7638ECC0BE7D7CD9522640DA5FAB4C08CFAC8DEBD08028" }, + { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, + { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, { name = "gleam_package_interface", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "CF3BFC5D0997750D9550D8D73A90F4B8D71C6C081B20ED4E70FFBE1E99AFC3C2" }, - { name = "gleam_pgo", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "pgo"], source = "local", path = "../../packages/pgo" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, + { name = "gleam_regexp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "A3655FDD288571E90EE9C4009B719FEF59FA16AFCDF3952A76A125AF23CF1592" }, + { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, { name = "glexer", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "BD477AD657C2B637FEF75F2405FAEFFA533F277A74EF1A5E17B55B1178C228FB" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, + { name = "glisten", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "912132751031473CB38F454120124FFC96AF6B0EA33D92C9C90DB16327A2A972" }, { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "interfaces", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_json", "gleam_stdlib"], source = "local", path = "../../packages/interfaces" }, { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "opentelemetry_api", version = "1.3.0", build_tools = ["rebar3", "mix"], requirements = ["opentelemetry_semantic_conventions"], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "B9E5FF775FD064FA098DBA3C398490B77649A352B40B0B730A6B7DC0BDD68858" }, - { name = "opentelemetry_semantic_conventions", version = "0.2.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_semantic_conventions", source = "hex", outer_checksum = "D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895" }, + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, + { name = "mist", version = "3.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "CDA1A74E768419235E16886463EC4722EFF4AB3F8D820A76EAD45D7C167D7282" }, + { name = "opentelemetry_api", version = "1.4.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "3DFBBFAA2C2ED3121C5C483162836C4F9027DEF469C41578AF5EF32589FCFC58" }, { name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" }, { name = "pgo", version = "0.14.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9" }, { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, - { name = "pprint", version = "1.0.3", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "76BBB92E23D12D954BD452686543F29EDE8EBEBB7FC0ACCBCA66EEF276EC3A06" }, + { name = "pog", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "pgo"], otp_app = "pog", source = "hex", outer_checksum = "48D6570C1E98F77A3C29E7ED4F1D81205DD0D870DCEC55F536EF3A57A28DF627" }, + { name = "pprint", version = "1.0.4", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "C310A98BDC0995644847C3C8702DE19656D6BCD638B2A8A358B97824379ECAA1" }, { name = "prng", version = "3.0.3", build_tools = ["gleam"], requirements = ["gleam_bitwise", "gleam_stdlib"], otp_app = "prng", source = "hex", outer_checksum = "53006736FE23A0F61828C95B505193E10905D8DB76E128F1642D3E571E08F589" }, { name = "radiate", version = "0.4.0", build_tools = ["gleam"], requirements = ["filespy", "gleam_otp", "gleam_stdlib", "shellout"], otp_app = "radiate", source = "hex", outer_checksum = "93A76A66EE4741DBFD3E79E27CBD11FE58EC3CB1C58F017FC165944E339D6293" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "ranger", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "B8F3AFF23A3A5B5D9526B8D18E7C43A7DFD3902B151B97EC65397FE29192B695" }, { name = "shellout", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "E2FCD18957F0E9F67E1F497FC9FF57393392F8A9BAEAEA4779541DE7A68DD7E0" }, - { name = "simplifile", version = "2.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "BDD04F5D31D6D34E2EDFAEF0B68A6297AEC939888C3BFCE61133DE13857F6DA2" }, + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, { name = "stoiridh_version", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "stoiridh_version", source = "hex", outer_checksum = "EEF8ADAB9755BD33EB202F169376F1A7797AEF90823FDCA671D8590D04FBF56B" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../../packages/tom" }, + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "tom", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "228E667239504B57AD05EC3C332C930391592F6C974D0EFECF32FFD0F3629A27" }, { name = "verl", version = "1.1.1", build_tools = ["rebar3"], requirements = [], otp_app = "verl", source = "hex", outer_checksum = "0925E51CD92A0A8BE271765B02430B2E2CFF8AC30EF24D123BD0D58511E8FB18" }, - { name = "wisp", version = "1.1.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5B25E37E08A94039668D2ACF7B2D2A665BEAF4B48EFA8613010B4E3164137664" }, + { name = "wisp", version = "1.3.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "D12384EE63ADEE833B40A6D26EF8014A6E4BFC10EC2CDC8B57D325DD4B52740E" }, ] [requirements] -aws4_request = { version = ">= 0.1.1 and < 1.0.0" } +aws4_request = { version = ">= 1.2.0 and < 2.0.0" } birl = { version = "~> 1.6" } chomp = { version = ">= 0.1.0 and < 1.0.0" } -cors_builder = { version = ">= 2.0.0 and < 3.0.0" } +cors_builder = { version = ">= 2.0.2 and < 3.0.0" } decipher = { version = ">= 1.2.0 and < 2.0.0" } +envoy = { version = ">= 1.0.2 and < 2.0.0" } gleam_erlang = { version = "~> 0.25" } gleam_hexpm = { version = "~> 1.0" } gleam_http = { version = "~> 3.6" } gleam_httpc = { version = ">= 2.2.0 and < 3.0.0" } -gleam_json = { version = "~> 1.0" } +gleam_json = { version = ">= 2.1.0 and < 3.0.0" } gleam_otp = { version = "~> 0.10" } gleam_package_interface = { version = ">= 1.0.0 and < 2.0.0" } -gleam_pgo = { path = "../../packages/pgo" } -gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } +gleam_regexp = { version = ">= 1.0.0 and < 2.0.0" } +gleam_stdlib = { version = ">= 0.44.0 and < 1.0.0" } +gleam_yielder = { version = ">= 1.1.0 and < 2.0.0" } gleeunit = { version = "~> 1.0" } glexer = { version = ">= 1.0.1 and < 2.0.0" } -mist = { version = ">= 1.2.0 and < 2.0.0" } -pgo = { version = "~> 0.14" } +interfaces = { path = "../../packages/interfaces" } +mist = { version = ">= 3.0.0 and < 4.0.0" } +pog = { version = ">= 1.0.1 and < 2.0.0" } pprint = { version = ">= 1.0.3 and < 2.0.0" } prng = { version = ">= 3.0.3 and < 4.0.0" } radiate = { version = ">= 0.4.0 and < 1.0.0" } ranger = { version = ">= 1.2.0 and < 2.0.0" } simplifile = { version = ">= 2.1.0 and < 3.0.0" } -tom = { path = "../../packages/tom" } +tom = { version = ">= 1.1.0 and < 2.0.0" } verl = { version = ">= 1.1.1 and < 2.0.0" } -wisp = { version = ">= 1.1.0 and < 2.0.0" } +wisp = { version = ">= 1.3.0 and < 2.0.0" } diff --git a/apps/backend/package.json b/apps/backend/package.json index 3f3dbac..5af73d0 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -9,6 +9,6 @@ "backend:db:reset": "yarn backend:db:drop && yarn backend:db:init" }, "dependencies": { - "@chouqueth/gleam": "^1.3.2" + "@chouqueth/gleam": "^1.6.2" } } diff --git a/apps/backend/src/api/github.gleam b/apps/backend/src/api/github.gleam index a00f0d8..6f01afa 100644 --- a/apps/backend/src/api/github.gleam +++ b/apps/backend/src/api/github.gleam @@ -8,7 +8,7 @@ import gleam/httpc import gleam/json import gleam/list import gleam/option.{type Option, Some} -import gleam/regex +import gleam/regexp import gleam/result fn query( @@ -17,12 +17,7 @@ fn query( variables: Option(json.Json), decoder: dynamic.Decoder(a), ) { - let body = - json.object([ - #("query", json.string(query)), - #("variables", json.nullable(variables, function.identity)), - ]) - use response <- result.try( + use response <- result.try({ request.new() |> request.set_header("authorization", "Bearer " <> token) |> request.set_header("user-agent", "gloogle / 1.0.0") @@ -30,19 +25,27 @@ fn query( |> request.set_scheme(http.Https) |> request.set_host("api.github.com") |> request.set_path("/graphql") - |> request.set_body(json.to_string(body)) + |> request.set_body(encode_body(query, variables)) |> httpc.send() - |> result.map_error(error.FetchError), - ) + |> result.map_error(error.FetchError) + }) response.body |> json.decode(using: decoder) |> result.map_error(error.JsonError) } +fn encode_body(query: String, variables: Option(json.Json)) -> String { + json.object([ + #("query", json.string(query)), + #("variables", json.nullable(variables, function.identity)), + ]) + |> json.to_string +} + fn match_repository_name(repo_url: String) { - let assert Ok(owner_name) = regex.from_string("https://github.com/(.+)/(.+)") - regex.scan(with: owner_name, content: repo_url) + let assert Ok(owner_name) = regexp.from_string("https://github.com/(.+)/(.+)") + regexp.scan(with: owner_name, content: repo_url) |> list.first() |> result.replace_error(error.UnknownError( "No repository match for " <> repo_url, diff --git a/apps/backend/src/api/hex_repo.gleam b/apps/backend/src/api/hex_repo.gleam index eea473f..25982b7 100644 --- a/apps/backend/src/api/hex_repo.gleam +++ b/apps/backend/src/api/hex_repo.gleam @@ -1,3 +1,4 @@ +import aws/s3 import backend/error import gleam/bit_array import gleam/erlang/process @@ -7,15 +8,14 @@ import gleam/httpc import gleam/json import gleam/package_interface import gleam/result -import s3 import simplifile import tom import wisp -@external(erlang, "gloogle_hex_ffi", "get_home") +@external(erlang, "backend_ffi", "get_home") pub fn get_home() -> Result(String, Nil) -@external(erlang, "gloogle_hex_ffi", "extract_tar") +@external(erlang, "backend_ffi", "extract_tar") fn extract_tar( tarbin: BitArray, base_name: String, @@ -23,7 +23,7 @@ fn extract_tar( slug: String, ) -> Result(#(String, String, String), Nil) -@external(erlang, "gloogle_hex_ffi", "remove_tar") +@external(erlang, "backend_ffi", "remove_tar") fn remove_tar(slug: String) -> Nil fn package_slug(name: String, version: String) { diff --git a/apps/backend/src/s3.gleam b/apps/backend/src/aws/s3.gleam similarity index 67% rename from apps/backend/src/s3.gleam rename to apps/backend/src/aws/s3.gleam index c8725a2..2d2405b 100644 --- a/apps/backend/src/s3.gleam +++ b/apps/backend/src/aws/s3.gleam @@ -1,6 +1,5 @@ import aws4_request -import backend/config -import birl +import backend/context import gleam/http import gleam/http/request import gleam/httpc @@ -8,9 +7,8 @@ import gleam/option.{type Option, None, Some} import gleam/result fn request(url: String, method: http.Method, body: Option(BitArray)) { - let date = birl.to_erlang_universal_datetime(birl.now()) - use bucket_uri <- result.try(config.bucket_uri()) - use #(access_key, secret_key) <- result.try(config.scaleway_keys()) + use bucket_uri <- result.try(context.bucket_uri()) + use #(access_key, secret_key) <- result.try(context.scaleway_keys()) request.new() |> request.set_method(method) |> request.set_path(url) @@ -18,9 +16,19 @@ fn request(url: String, method: http.Method, body: Option(BitArray)) { |> request.set_host(bucket_uri) |> request.set_scheme(http.Https) |> request.set_header("content-type", "application/octet-stream") - |> aws4_request.sign(date, access_key, secret_key, "fr-par", "s3") + |> sign(access_key, secret_key) |> httpc.send_bits() - |> result.nil_error() + |> result.replace_error(Nil) +} + +fn sign(request, access_key_id, secret_access_key) { + aws4_request.signer( + access_key_id:, + secret_access_key:, + region: "fr-par", + service: "s3", + ) + |> aws4_request.sign_bits(request) } pub fn get(name: String) { diff --git a/apps/backend/src/backend.gleam b/apps/backend/src/backend.gleam index a9f76dc..03f82a4 100644 --- a/apps/backend/src/backend.gleam +++ b/apps/backend/src/backend.gleam @@ -1,73 +1,57 @@ -import backend/config +import backend/context.{type Context, Context} import backend/gleam/type_search/state as type_search -import backend/postgres/postgres import backend/router +import backend/setup +import backend/workers +import envoy import gleam/erlang/process import gleam/function +import gleam/int +import gleam/option.{Some} import gleam/otp/supervisor +import gleam/result import mist -import periodic -import setup -import tasks/hex -import tasks/popularity -import tasks/ranking -import tasks/timeseries import wisp import wisp/logger import wisp/wisp_mist pub fn main() { - wisp.configure_logger() - - let secret_key_base = config.get_secret_key_base() - let cnf = config.read_config() - let ctx = postgres.connect(cnf) + configure_logger() + let assert Ok(ctx) = context.init() + let assert Ok(ctx) = start_type_search_worker(ctx) + let assert Ok(_) = start_http_server(ctx) + let assert Ok(_) = start_periodic_workers(ctx) + process.sleep_forever() +} - logger.set_level(cnf.level) +fn configure_logger() { + let level = logger.read_level() + wisp.configure_logger() + logger.set_level(level) setup.radiate() +} - let assert Ok(subject) = type_search.init(ctx.db) - - let ctx = ctx |> config.add_type_search_subject(subject) - - let assert Ok(_) = - router.handle_request(_, ctx) - |> wisp_mist.handler(secret_key_base) - |> mist.new() - |> mist.port(cnf.port) - |> mist.start_http() - - let assert Ok(_) = { - use periodic_children <- supervisor.start() - use periodic_children <- function.tap(periodic_children) - let assert Ok(_) = { - use children <- supervisor.start() - // Every 10 seconds - add_periodic_worker(periodic_children, waiting: 10 * 1000, do: fn() { - hex.sync_new_gleam_releases(ctx, children) - }) - // Every day - add_periodic_worker(periodic_children, waiting: 86_400_000, do: fn() { - ranking.compute_ranking(ctx) - }) - // Every day - add_periodic_worker(periodic_children, waiting: 86_400_000, do: fn() { - popularity.compute_popularity(ctx) - }) - // Every hour - add_periodic_worker(periodic_children, waiting: 3600 * 1000, do: fn() { - timeseries.store_timeseries(ctx) - }) - } - } +fn start_type_search_worker(ctx: Context) { + use subject <- result.map(type_search.init(ctx.db)) + Context(..ctx, type_search_subject: Some(subject)) +} - process.sleep_forever() +fn start_http_server(ctx) { + use port <- result.try(envoy.get("PORT")) + use port <- result.map(int.parse(port)) + let secret_key_base = context.get_secret_key_base() + router.handle_request(_, ctx) + |> wisp_mist.handler(secret_key_base) + |> mist.new() + |> mist.port(port) + |> mist.start_http() } -fn add_periodic_worker(children, waiting delay, do work) { - use _ <- function.tap(children) - supervisor.add(children, { - use _ <- supervisor.worker() - periodic.periodically(do: work, waiting: delay) - }) +fn start_periodic_workers(ctx) { + use children <- supervisor.start() + use children <- function.tap(children) + workers.sync_new_gleam_releases_ten_secondly(ctx, children) + workers.compute_ranking_daily(ctx, children) + workers.compute_popularity_daily(ctx, children) + workers.store_timeseries_hourly(ctx, children) } diff --git a/apps/backend/src/backend/config.gleam b/apps/backend/src/backend/config.gleam deleted file mode 100644 index 2a6cef2..0000000 --- a/apps/backend/src/backend/config.gleam +++ /dev/null @@ -1,76 +0,0 @@ -import backend/gleam/type_search/msg as type_search -import gleam/erlang/os -import gleam/erlang/process.{type Subject} -import gleam/int -import gleam/option.{type Option} -import gleam/pgo -import gleam/result -import gleam/string -import wisp -import wisp/logger - -pub type Environment { - Development - Production -} - -pub type Context { - Context( - db: pgo.Connection, - hex_api_key: String, - github_token: String, - env: Environment, - type_search_subject: Option(Subject(type_search.Msg)), - ) -} - -pub type Config { - Config( - database_url: String, - hex_api_key: String, - port: Int, - level: logger.Level, - github_token: String, - env: Environment, - ) -} - -pub fn read_config() { - let assert Ok(database_url) = os.get_env("DATABASE_URL") - let assert Ok(hex_api_key) = os.get_env("HEX_API_KEY") - let assert Ok(github_token) = os.get_env("GITHUB_TOKEN") - let env = case result.unwrap(os.get_env("GLEAM_ENV"), "") { - "development" -> Development - _ -> Production - } - let assert Ok(port) = - os.get_env("PORT") - |> result.try(int.parse) - let level = - os.get_env("LOG_LEVEL") - |> result.try(logger.parse) - |> result.unwrap(logger.Info) - Config(database_url, hex_api_key, port, level, github_token, env) -} - -pub fn get_secret_key_base() { - wisp.random_string(64) -} - -pub fn is_dev() { - os.get_env("GLEAM_ENV") == Ok("development") -} - -pub fn bucket_uri() { - os.get_env("BUCKET_URI") -} - -pub fn scaleway_keys() { - use access_key <- result.try(os.get_env("SCALEWAY_ACCESS_KEY")) - use secret_key <- result.map(os.get_env("SCALEWAY_SECRET_KEY")) - #(access_key, secret_key) -} - -pub fn add_type_search_subject(context, subject) { - Context(..context, type_search_subject: option.Some(subject)) -} diff --git a/apps/backend/src/backend/context.gleam b/apps/backend/src/backend/context.gleam new file mode 100644 index 0000000..43836f2 --- /dev/null +++ b/apps/backend/src/backend/context.gleam @@ -0,0 +1,57 @@ +import backend/gleam/type_search/msg as type_search +import backend/postgres/postgres +import envoy +import gleam/erlang/process.{type Subject} +import gleam/option.{type Option, None} +import gleam/result +import pog +import wisp + +pub type Environment { + Development + Production +} + +pub type Context { + Context( + db: pog.Connection, + hex_api_key: String, + github_token: String, + env: Environment, + type_search_subject: Option(Subject(type_search.Msg)), + ) +} + +pub fn init() { + let env = read_environment() + use database_url <- result.try(envoy.get("DATABASE_URL")) + use hex_api_key <- result.try(envoy.get("HEX_API_KEY")) + use github_token <- result.map(envoy.get("GITHUB_TOKEN")) + let db = postgres.connect(database_url) + Context(db:, hex_api_key:, github_token:, env:, type_search_subject: None) +} + +pub fn read_environment() { + case envoy.get("GLEAM_ENV") { + Ok("development") -> Development + _ -> Production + } +} + +pub fn get_secret_key_base() { + wisp.random_string(64) +} + +pub fn is_dev() { + envoy.get("GLEAM_ENV") == Ok("development") +} + +pub fn bucket_uri() { + envoy.get("BUCKET_URI") +} + +pub fn scaleway_keys() { + use access_key <- result.try(envoy.get("SCALEWAY_ACCESS_KEY")) + use secret_key <- result.map(envoy.get("SCALEWAY_SECRET_KEY")) + #(access_key, secret_key) +} diff --git a/apps/backend/src/backend/data/hex_read.gleam b/apps/backend/src/backend/data/hex_read.gleam index d4caa20..8f5c6dc 100644 --- a/apps/backend/src/backend/data/hex_read.gleam +++ b/apps/backend/src/backend/data/hex_read.gleam @@ -9,7 +9,7 @@ pub type HexRead { pub fn decode(data) { dynamic.decode2( HexRead, - dynamic.element(0, dynamic.int), - dynamic.element(1, helpers.decode_time), + dynamic.field("id", dynamic.int), + dynamic.field("last_check", helpers.decode_time), )(data) } diff --git a/apps/backend/src/backend/data/hex_user.gleam b/apps/backend/src/backend/data/hex_user.gleam index 04c5a2d..abed1a6 100644 --- a/apps/backend/src/backend/data/hex_user.gleam +++ b/apps/backend/src/backend/data/hex_user.gleam @@ -17,11 +17,11 @@ pub type HexUser { pub fn decode(data) { dynamic.decode6( HexUser, - dynamic.element(0, dynamic.int), - dynamic.element(1, dynamic.string), - dynamic.element(2, dynamic.optional(dynamic.string)), - dynamic.element(3, dynamic.string), - dynamic.element(4, helpers.decode_time), - dynamic.element(5, helpers.decode_time), + dynamic.field("id", dynamic.int), + dynamic.field("username", dynamic.string), + dynamic.field("email", dynamic.optional(dynamic.string)), + dynamic.field("url", dynamic.string), + dynamic.field("created_at", helpers.decode_time), + dynamic.field("updated_at", helpers.decode_time), )(data) } diff --git a/apps/backend/src/backend/error.gleam b/apps/backend/src/backend/error.gleam index 2f436af..bb85d12 100644 --- a/apps/backend/src/backend/error.gleam +++ b/apps/backend/src/backend/error.gleam @@ -2,15 +2,15 @@ import gleam/dynamic import gleam/int import gleam/json import gleam/list -import gleam/pgo import gleam/result import gleam/string +import pog import simplifile import tom import wisp pub type Error { - DatabaseError(pgo.QueryError) + DatabaseError(pog.QueryError) FetchError(dynamic.Dynamic) JsonError(json.DecodeError) SimplifileError(simplifile.FileError, String) @@ -20,6 +20,18 @@ pub type Error { EmptyError } +pub fn empty() { + Error(EmptyError) +} + +pub fn new(message: String) { + Error(UnknownError(message)) +} + +pub fn replace_nil(res, message: String) { + result.replace_error(res, UnknownError(message)) +} + pub fn log_dynamic_error(error: dynamic.DecodeError) { wisp.log_warning("Dynamic Decode Error") wisp.log_warning(" expected: " <> error.expected) @@ -30,15 +42,13 @@ pub fn log_dynamic_error(error: dynamic.DecodeError) { pub fn log_decode_error(error: json.DecodeError) { case error { json.UnexpectedEndOfInput -> wisp.log_warning("Unexpected end of input") - json.UnexpectedByte(byte, position) -> { + json.UnexpectedByte(byte) -> { wisp.log_warning("Unexpected byte") wisp.log_warning(" byte: " <> byte) - wisp.log_warning(" position: " <> int.to_string(position)) } - json.UnexpectedSequence(byte, position) -> { + json.UnexpectedSequence(byte) -> { wisp.log_warning("Unexpected sequence") wisp.log_warning(" byte: " <> byte) - wisp.log_warning(" position: " <> int.to_string(position)) } json.UnexpectedFormat(errors) -> { wisp.log_warning("Unexpected format") @@ -54,7 +64,7 @@ pub fn log_error(error: Error) { FetchError(_dyn) -> wisp.log_warning("Fetch error") DatabaseError(error) -> { wisp.log_warning("Query error") - log_pgo_error(error) + log_pog_error(error) } JsonError(error) -> { wisp.log_warning("JSON error") @@ -168,36 +178,36 @@ pub fn log_simplifile(error: simplifile.FileError) { } } -pub fn log_pgo_error(error: pgo.QueryError) { +pub fn log_pog_error(error: pog.QueryError) { case error { - pgo.ConstraintViolated(message, constraint, details) -> { + pog.ConstraintViolated(message, constraint, details) -> { wisp.log_warning("Constraint violated") wisp.log_warning(" message: " <> message) wisp.log_warning(" constraint: " <> constraint) wisp.log_warning(" details: " <> details) } - pgo.PostgresqlError(code, name, message) -> { - let code = result.unwrap(pgo.error_code_name(code), code) + pog.PostgresqlError(code, name, message) -> { + let code = result.unwrap(pog.error_code_name(code), code) wisp.log_warning("PostgreSQL error") wisp.log_warning(" error: " <> code) wisp.log_warning(" name: " <> name) wisp.log_warning(" message: " <> message) } - pgo.UnexpectedArgumentCount(expected, got) -> { + pog.UnexpectedArgumentCount(expected, got) -> { wisp.log_warning("Unexpected argument count") wisp.log_warning(" expected: " <> int.to_string(expected)) wisp.log_warning(" got: " <> int.to_string(got)) } - pgo.UnexpectedArgumentType(expected, got) -> { + pog.UnexpectedArgumentType(expected, got) -> { wisp.log_warning("Unexpected argument type") wisp.log_warning(" expected: " <> expected) wisp.log_warning(" got: " <> got) } - pgo.UnexpectedResultType(error) -> { + pog.UnexpectedResultType(error) -> { wisp.log_warning("Unexpected result type") list.map(error, log_dynamic_error) Nil } - pgo.ConnectionUnavailable -> wisp.log_warning("Connection unavailable") + pog.ConnectionUnavailable -> wisp.log_warning("Connection unavailable") } } diff --git a/apps/backend/src/backend/gleam/context.gleam b/apps/backend/src/backend/gleam/context.gleam index 0171d39..2ef8586 100644 --- a/apps/backend/src/backend/gleam/context.gleam +++ b/apps/backend/src/backend/gleam/context.gleam @@ -3,12 +3,12 @@ import gleam/dict.{type Dict} import gleam/erlang/process.{type Subject} import gleam/option.{type Option} import gleam/package_interface -import gleam/pgo +import pog import tom pub type Context { Context( - db: pgo.Connection, + db: pog.Connection, package_interface: package_interface.Package, gleam_toml: Dict(String, tom.Toml), /// Allow to bypass parameters relations if activated. diff --git a/apps/backend/src/backend/gleam/generate/types.gleam b/apps/backend/src/backend/gleam/generate/types.gleam index 35c4ff9..6967abd 100644 --- a/apps/backend/src/backend/gleam/generate/types.gleam +++ b/apps/backend/src/backend/gleam/generate/types.gleam @@ -7,17 +7,17 @@ import gleam/dict import gleam/dynamic import gleam/json.{type Json} import gleam/list -import gleam/option +import gleam/option.{type Option, None, Some} import gleam/order import gleam/package_interface.{ type Constant, type Function, type Implementations, type Parameter, type Type, type TypeAlias, type TypeConstructor, type TypeDefinition, } import gleam/pair -import gleam/pgo import gleam/result import gleam/set.{type Set} import gleam/verl +import pog fn reduce_components( components: List(a), @@ -105,8 +105,8 @@ fn type_to_json(ctx: Context, type_: Type) { let res = extract_parameters_relation(ctx, name, package, module) use ref <- result.map(res) let new_ids = case ref { - option.None -> gen.1 - option.Some(ref) -> set.insert(gen.1, ref.1) + None -> gen.1 + Some(ref) -> set.insert(gen.1, ref.1) } json.object([ #("kind", json.string("named")), @@ -122,38 +122,51 @@ fn type_to_json(ctx: Context, type_: Type) { } fn find_package_release(ctx: Context, package: String, requirement: String) { - let decoder = dynamic.tuple2(dynamic.int, dynamic.string) - use response <- result.try({ - "SELECT package_release.id, package_release.version + "SELECT package_release.id id, package_release.version version FROM package JOIN package_release ON package.id = package_release.package_id WHERE package.name = $1" - |> pgo.execute(ctx.db, [pgo.text(package)], decoder) - |> result.map_error(error.DatabaseError) - }) - response.rows - |> keep_matching_releases(requirement) + |> pog.query + |> pog.parameter(pog.text(package)) + |> pog.returning(dynamic.decode2( + pair.new, + dynamic.field("id", dynamic.int), + dynamic.field("version", dynamic.string), + )) + |> pog.execute(ctx.db) + |> result.map_error(error.DatabaseError) + |> result.map(fn(response) { response.rows }) + |> result.map(keep_matching_releases(_, requirement)) } fn keep_matching_releases(rows: List(#(Int, String)), requirement: String) { - let requirement = bit_array.from_string(requirement) rows - |> list.filter(fn(r) { - let version = bit_array.from_string(r.1) - let is_matching = verl.is_match(version: version, requirement: requirement) - result.unwrap(is_matching, False) - }) - |> list.sort(fn(a, b) { - let a = bit_array.from_string(a.1) - let b = bit_array.from_string(b.1) - case verl.gte(version: a, with: b) { - True -> order.Lt - False -> order.Gt - } - }) - |> list.map(fn(a) { a.0 }) - |> Ok() + |> list.filter(keep_matching_requirement(_, requirement)) + |> list.sort(by_decreasing_version) + |> list.map(pair.first) +} + +fn keep_matching_requirement(release: #(Int, String), requirement: String) { + let #(_release_id, release_version) = release + let requirement = bit_array.from_string(requirement) + let version = bit_array.from_string(release_version) + let is_matching = verl.is_match(version:, requirement:) + result.unwrap(is_matching, False) +} + +fn by_decreasing_version( + release_1: #(Int, String), + release_2: #(Int, String), +) -> order.Order { + let #(_, release_1_version) = release_1 + let #(_, release_2_version) = release_2 + let release_1_version = bit_array.from_string(release_1_version) + let release_2_version = bit_array.from_string(release_2_version) + case verl.gte(version: release_1_version, with: release_2_version) { + True -> order.Lt + False -> order.Gt + } } fn find_signature_from_release( @@ -162,11 +175,9 @@ fn find_signature_from_release( module: String, releases: List(Int), ) { - use acc, release <- list.fold(releases, Error(Nil)) + use acc, release <- list.fold(releases, error.empty()) use <- bool.guard(when: result.is_ok(acc), return: acc) - let args = [pgo.text(name), pgo.text(module), pgo.int(release)] - use t <- result.try({ - "SELECT release.version, signature.id + "SELECT release.version version, signature.id id FROM package_release release JOIN package_module module ON module.package_release_id = release.id @@ -175,11 +186,21 @@ fn find_signature_from_release( WHERE signature.name = $1 AND module.name = $2 AND module.package_release_id = $3" - |> pgo.execute(ctx.db, args, dynamic.tuple2(dynamic.string, dynamic.int)) - |> result.nil_error() + |> pog.query + |> pog.parameter(pog.text(name)) + |> pog.parameter(pog.text(module)) + |> pog.parameter(pog.int(release)) + |> pog.returning(dynamic.decode2( + pair.new, + dynamic.field("version", dynamic.string), + dynamic.field("id", dynamic.int), + )) + |> pog.execute(ctx.db) + |> result.map_error(error.DatabaseError) + |> result.try(fn(response) { + list.first(response.rows) + |> error.replace_nil("[find_signature_from_release] No row") }) - list.first(t.rows) - |> result.nil_error() } fn find_type_signature( @@ -188,9 +209,9 @@ fn find_type_signature( package: String, module: String, releases: List(Int), -) -> Result(option.Option(#(String, Int)), error.Error) { +) -> Result(Option(#(String, Int)), error.Error) { case find_signature_from_release(ctx, name, module, releases) { - Ok(value) -> Ok(option.Some(value)) + Ok(value) -> Ok(Some(value)) Error(_) -> { let slug = package <> "/" <> module let package_name = ctx.package_interface.name @@ -205,7 +226,7 @@ fn find_type_signature( True -> case dict.get(ctx.package_interface.modules, module) { // Module is hidden, everything is correct, type is hidden. - Error(_) -> Ok(option.None) + Error(_) -> Ok(None) // Module is not hidden, checking if type is hidden by itself. Ok(mod) -> { let slug = slug <> "." <> name @@ -220,7 +241,7 @@ fn find_type_signature( Error(_) -> case dict.get(mod.types, name) { // Type is hidden, returning None because it can't be extracted. - Error(_) -> Ok(option.None) + Error(_) -> Ok(None) // Type is not hidden, returning an error to restart the extraction. Ok(_) -> { let id = package_name <> ", looking for " <> slug @@ -241,17 +262,15 @@ fn extract_parameters_relation( name: String, package: String, module: String, -) -> Result(option.Option(#(String, Int)), error.Error) { - use <- bool.guard(when: is_prelude(package, module), return: Ok(option.None)) +) -> Result(Option(#(String, Int)), error.Error) { + use <- bool.guard(when: is_prelude(package, module), return: Ok(None)) use requirement <- result.try(toml.find_package_requirement(ctx, package)) use releases <- result.try(find_package_release(ctx, package, requirement)) - use error <- result.try_recover({ - find_type_signature(ctx, name, package, module, releases) + find_type_signature(ctx, name, package, module, releases) + |> result.try_recover(fn(error) { + use <- bool.guard(when: ctx.ignore_parameters_errors, return: Ok(None)) + Error(error) }) - case ctx.ignore_parameters_errors { - False -> Error(error) - True -> Ok(option.None) - } } fn is_prelude(package: String, module: String) { diff --git a/apps/backend/src/backend/gleam/type_search.gleam b/apps/backend/src/backend/gleam/type_search.gleam index 2373e44..e4ddec2 100644 --- a/apps/backend/src/backend/gleam/type_search.gleam +++ b/apps/backend/src/backend/gleam/type_search.gleam @@ -5,8 +5,8 @@ import gleam/int import gleam/list import gleam/option.{type Option} import gleam/pair -import gleam/pgo import gleam/result +import pog pub type TypeSearch { TypeSearch(keys: Keys, rows: List(Int)) @@ -48,25 +48,25 @@ fn update_keys( parse.DiscardName -> panic as "No Discard name in add" parse.Index(_value, index) -> { let value = int.to_string(index) - dict.update(keys.keys, value, fn(k) { + dict.upsert(keys.keys, value, fn(k) { let k = option.unwrap(k, Keys(keys: dict.new(), next: next)) update_keys(k, rest, updater) }) } parse.Custom(value, kinds) -> - dict.update(keys.keys, value, fn(k) { + dict.upsert(keys.keys, value, fn(k) { let k = option.unwrap(k, Keys(keys: dict.new(), next: next)) update_keys(k, list.append(kinds, rest), updater) }) parse.Function(kinds, return) -> { let kinds = postpend(kinds, return) - dict.update(keys.keys, "fn", fn(k) { + dict.upsert(keys.keys, "fn", fn(k) { let k = option.unwrap(k, Keys(keys: dict.new(), next: next)) update_keys(k, list.append(kinds, rest), updater) }) } parse.Tuple(kinds) -> { - dict.update(keys.keys, "#()", fn(k) { + dict.upsert(keys.keys, "#()", fn(k) { let k = option.unwrap(k, Keys(keys: dict.new(), next: next)) update_keys(k, list.append(kinds, rest), updater) }) @@ -112,7 +112,7 @@ fn get_next_tree( keys: Keys, kind: Kind, env: Dict(Int, String), - db: pgo.Connection, + db: pog.Connection, ) -> List(#(Keys, Dict(Int, String))) { case kind { parse.DiscardName -> { @@ -182,7 +182,7 @@ fn find_next_tree( kind: Kind, kinds: List(Kind), env: Dict(Int, String), - db: pgo.Connection, + db: pog.Connection, ) -> List(Int) { case kind { parse.DiscardName -> { @@ -252,7 +252,7 @@ fn do_find( searches: TypeSearch, kinds: List(Kind), env: Dict(Int, String), - db: pgo.Connection, + db: pog.Connection, ) { case kinds { [] -> searches.rows @@ -260,7 +260,7 @@ fn do_find( } } -pub fn find(searches: TypeSearch, kind: Kind, db: pgo.Connection) { +pub fn find(searches: TypeSearch, kind: Kind, db: pog.Connection) { case kind { Function(kinds, return_value) -> kinds diff --git a/apps/backend/src/backend/gleam/type_search/state.gleam b/apps/backend/src/backend/gleam/type_search/state.gleam index 9930818..bf65f47 100644 --- a/apps/backend/src/backend/gleam/type_search/state.gleam +++ b/apps/backend/src/backend/gleam/type_search/state.gleam @@ -8,27 +8,25 @@ import gleam/function import gleam/list import gleam/option import gleam/otp/actor -import gleam/pgo +import gleam/pair import gleam/result +import pog pub type State { - State(db: pgo.Connection, search: TypeSearch) + State(db: pog.Connection, search: TypeSearch) } -pub fn init(db: pgo.Connection) { +pub fn init(db: pog.Connection) { let init = fn() { - let search = - compute_rows(0, db, #(0, type_search.empty()), { - fn(search: #(Int, TypeSearch), row: #(String, Int)) { - let #(signature, id) = row - signature - |> parse.parse_function - |> result.map(fn(kind) { - #(search.0 + 1, type_search.add(search.1, kind, id)) - }) - |> result.unwrap(search) - } - }) + let search = { + use search, row <- compute_rows(0, db, #(0, type_search.empty())) + let idx = search.0 + 1 + let #(signature, id) = row + signature + |> parse.parse_function + |> result.map(fn(kind) { #(idx, type_search.add(search.1, kind, id)) }) + |> result.unwrap(search) + } process.new_selector() |> process.selecting(process.new_subject(), function.identity) |> actor.Ready(State(db, search.1), _) @@ -41,7 +39,7 @@ fn loop(msg: msg.Msg, state: State) -> actor.Next(msg.Msg, State) { msg.Find(subject, signature) -> { signature |> parse.parse_function - |> result.nil_error + |> result.replace_error(Nil) |> result.then(permutation_search(state, _)) |> option.from_result |> function.tap(fn(res) { process.send(subject, res) }) @@ -56,7 +54,6 @@ fn loop(msg: msg.Msg, state: State) -> actor.Next(msg.Msg, State) { |> actor.continue } } - actor.continue(state) } fn is_permutable(list: List(a), len: Int) { @@ -88,11 +85,10 @@ fn permutation_search(state: State, kind: parse.Kind) { fn compute_rows( offset: Int, - db: pgo.Connection, + db: pog.Connection, default: a, next: fn(a, #(String, Int)) -> a, ) { - let decoder = dynamic.tuple2(dynamic.string, dynamic.int) let rows = "SELECT signature_, id FROM package_type_fun_signature @@ -100,7 +96,14 @@ fn compute_rows( ORDER BY id ASC LIMIT 1000 OFFSET $1" - |> pgo.execute(db, [pgo.int(offset)], decoder) + |> pog.query + |> pog.parameter(pog.int(offset)) + |> pog.returning(dynamic.decode2( + pair.new, + dynamic.field("signature_", dynamic.string), + dynamic.field("id", dynamic.int), + )) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.unwrap([]) use <- bool.guard(when: list.is_empty(rows), return: default) diff --git a/apps/backend/src/backend/postgres/postgres.gleam b/apps/backend/src/backend/postgres/postgres.gleam index 65a31b5..8ccc051 100644 --- a/apps/backend/src/backend/postgres/postgres.gleam +++ b/apps/backend/src/backend/postgres/postgres.gleam @@ -1,26 +1,17 @@ -import backend/config.{type Config, type Context, Context} import backend/error import gleam/bool import gleam/list import gleam/option.{type Option, Some} -import gleam/pgo.{Config} import gleam/result import gleam/string import gleam/uri +import pog.{Config} -pub fn connect(cnf: Config) { - let assert Ok(config) = parse_database_url(cnf.database_url) +pub fn connect(database_url: String) { + let assert Ok(config) = parse_database_url(database_url) config - |> pgo.connect() - |> fn(db) { - Context( - db: db, - hex_api_key: cnf.hex_api_key, - github_token: cnf.github_token, - env: cnf.env, - type_search_subject: option.None, - ) - } + |> pog.rows_as_map(True) + |> pog.connect } fn parse_database_url(database_url: String) { @@ -33,7 +24,7 @@ fn parse_database_url(database_url: String) { return: Error(error.UnknownError("No postgres protocol")), ) - pgo.default_config() + pog.default_config() |> fn(cnf) { Config(..cnf, database: string.replace(db_uri.path, "/", "")) } |> update_config(db_uri.userinfo, add_user_info) |> update_config(db_uri.host, fn(cnf, u) { Config(..cnf, host: u) }) @@ -51,15 +42,15 @@ fn parse_database_url(database_url: String) { } fn update_config( - cnf: pgo.Config, + cnf: pog.Config, field: Option(a), - mapper: fn(pgo.Config, a) -> pgo.Config, + mapper: fn(pog.Config, a) -> pog.Config, ) { option.map(field, fn(u) { mapper(cnf, u) }) |> option.unwrap(cnf) } -fn add_user_info(c: pgo.Config, u: String) { +fn add_user_info(c: pog.Config, u: String) { case string.split(u, ":") { [user, password] -> Config(..c, user: user, password: Some(password)) [user] -> Config(..c, user: user) diff --git a/apps/backend/src/backend/postgres/queries.gleam b/apps/backend/src/backend/postgres/queries.gleam index 1411fdb..77a4c5c 100644 --- a/apps/backend/src/backend/postgres/queries.gleam +++ b/apps/backend/src/backend/postgres/queries.gleam @@ -3,6 +3,9 @@ import backend/data/hex_user.{type HexUser} import backend/error import backend/gleam/context import birl.{type Time} +import data/analytics +import data/package +import data/type_search import gleam/bool import gleam/dict.{type Dict} import gleam/dynamic @@ -13,10 +16,13 @@ import gleam/list import gleam/option.{type Option, None, Some} import gleam/package_interface import gleam/pair -import gleam/pgo import gleam/result import gleam/string import helpers +import pog + +@external(erlang, "backend_ffi", "coerce") +fn coerce(a: a) -> b pub type SignatureKind { TypeAlias @@ -25,9 +31,11 @@ pub type SignatureKind { Function } -pub fn get_last_hex_date(db: pgo.Connection) { +pub fn get_last_hex_date(db: pog.Connection) { "SELECT id, last_check FROM hex_read ORDER BY last_check DESC LIMIT 1" - |> pgo.execute(db, [], hex_read.decode) + |> pog.query + |> pog.returning(hex_read.decode) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(response) { response.rows @@ -37,15 +45,17 @@ pub fn get_last_hex_date(db: pgo.Connection) { }) } -pub fn upsert_most_recent_hex_timestamp(db: pgo.Connection, latest: Time) { - let timestamp = helpers.convert_time(latest) +pub fn upsert_most_recent_hex_timestamp(db: pog.Connection, latest: Time) { "INSERT INTO hex_read OVERRIDING SYSTEM VALUE VALUES (1, $1) ON CONFLICT (id) DO UPDATE SET last_check = $1 - RETURNING *" - |> pgo.execute(db, [timestamp], hex_read.decode) + RETURNING id, last_check" + |> pog.query + |> pog.parameter(helpers.convert_time(latest)) + |> pog.returning(hex_read.decode) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.try(fn(response) { let err = "Upsert most recent hex timestamp failed" @@ -55,13 +65,16 @@ pub fn upsert_most_recent_hex_timestamp(db: pgo.Connection, latest: Time) { }) } -pub fn upsert_search_analytics(db: pgo.Connection, query: String) { +pub fn upsert_search_analytics(db: pog.Connection, query: String) { "INSERT INTO search_analytics (query) VALUES ($1) ON CONFLICT (query) DO UPDATE SET occurences = search_analytics.occurences + 1 RETURNING *" - |> pgo.execute(db, [pgo.text(query)], dynamic.dynamic) + |> pog.query + |> pog.parameter(pog.text(query)) + |> pog.returning(dynamic.dynamic) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.try(fn(response) { let err = "Upsert search analytics failed" @@ -71,74 +84,74 @@ pub fn upsert_search_analytics(db: pgo.Connection, query: String) { }) } -pub fn select_more_popular_packages(db: pgo.Connection) { - let decoder = - dynamic.tuple4( - dynamic.string, - dynamic.string, - dynamic.int, - dynamic.optional(dynamic.int), - ) +pub fn select_more_popular_packages(db: pog.Connection) { use ranked <- result.try({ - "SELECT name, repository, rank, (popularity -> 'github')::int + "SELECT name, repository, rank, (popularity -> 'github')::int AS popularity FROM package ORDER BY rank DESC LIMIT 22" - |> pgo.execute(db, [], decoder) + |> pog.query + |> pog.returning(analytics.decode_package) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) }) use popular <- result.try({ - "SELECT name, repository, rank, (popularity -> 'github')::int + "SELECT name, repository, rank, (popularity -> 'github')::int AS popularity FROM package WHERE popularity -> 'github' IS NOT NULL AND name != 'funtil' AND name != 'dew' ORDER BY popularity -> 'github' DESC LIMIT 23" - |> pgo.execute(db, [], decoder) + |> pog.query + |> pog.returning(analytics.decode_package) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) }) Ok(#(ranked, popular)) } -pub fn select_last_day_search_analytics(db: pgo.Connection) { +pub fn select_last_day_search_analytics(db: pog.Connection) { let #(date, _) = birl.to_erlang_universal_datetime(birl.now()) let now = birl.from_erlang_universal_datetime(#(date, #(0, 0, 0))) "SELECT query, occurences FROM search_analytics WHERE updated_at >= $1" - |> pgo.execute( - db, - [helpers.convert_time(now)], - dynamic.tuple2(dynamic.string, dynamic.int), - ) + |> pog.query + |> pog.parameter(helpers.convert_time(now)) + |> pog.returning(dynamic.decode2( + pair.new, + dynamic.field("query", dynamic.string), + dynamic.field("occurences", dynamic.int), + )) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) } pub fn upsert_search_analytics_timeseries( - db: pgo.Connection, + db: pog.Connection, analytic: #(String, Int), ) { let #(date, _) = birl.to_erlang_universal_datetime(birl.now()) let now = birl.from_erlang_universal_datetime(#(date, #(0, 0, 0))) - let timestamp = helpers.convert_time(now) let #(query, occurences) = analytic "INSERT INTO analytics_timeseries (query, occurences, date) VALUES ($1, $2, $3) ON CONFLICT (query, date) DO UPDATE SET occurences = $2" - |> pgo.execute( - db, - [pgo.text(query), pgo.int(occurences), timestamp], - dynamic.dynamic, - ) + |> pog.query + |> pog.parameter(pog.text(query)) + |> pog.parameter(pog.int(occurences)) + |> pog.parameter(helpers.convert_time(now)) + |> pog.returning(dynamic.dynamic) + |> pog.execute(db) |> result.map_error(error.DatabaseError) } -pub fn get_timeseries_count(db: pgo.Connection) { +pub fn get_timeseries_count(db: pog.Connection) { "SELECT SUM(at.occurences - COALESCE( (SELECT att.occurences @@ -154,65 +167,81 @@ pub fn get_timeseries_count(db: pgo.Connection) { WHERE at.date >= now() - INTERVAL '30 day' GROUP BY at.date ORDER BY date DESC" - |> pgo.execute(db, [], dynamic.tuple2(dynamic.int, helpers.decode_time)) + |> pog.query + |> pog.returning(dynamic.decode2( + pair.new, + dynamic.field("searches", dynamic.int), + dynamic.field("date", helpers.decode_time), + )) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -pub fn upsert_hex_user(db: pgo.Connection, owner: hexpm.PackageOwner) { - let username = pgo.text(owner.username) - let email = pgo.nullable(pgo.text, owner.email) - let url = pgo.text(owner.url) +pub fn upsert_hex_user(db: pog.Connection, owner: hexpm.PackageOwner) { "INSERT INTO hex_user (username, email, url) VALUES ($1, $2, $3) ON CONFLICT (username) DO UPDATE SET email = $2, url = $3 RETURNING id, username, email, url, created_at, updated_at" - |> pgo.execute(db, [username, email, url], hex_user.decode) + |> pog.query + |> pog.parameter(pog.text(owner.username)) + |> pog.parameter(pog.nullable(pog.text, owner.email)) + |> pog.parameter(pog.text(owner.url)) + |> pog.returning(hex_user.decode) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) } -fn upsert_package_owners(db: pgo.Connection, owners: List(hexpm.PackageOwner)) { +fn upsert_package_owners(db: pog.Connection, owners: List(hexpm.PackageOwner)) { owners |> list.map(upsert_hex_user(db, _)) |> result.all() |> result.map(list.flatten) } -fn get_current_package_owners(db: pgo.Connection, package_id: Int) { - let pid = pgo.int(package_id) - "SELECT package_owner.hex_user_id +fn get_current_package_owners(db: pog.Connection, package_id: Int) { + "SELECT package_owner.hex_user_id AS user_id FROM package_owner WHERE package_owner.package_id = $1" - |> pgo.execute(db, [pid], dynamic.element(0, dynamic.int)) + |> pog.query + |> pog.parameter(pog.int(package_id)) + |> pog.returning(dynamic.field("user_id", dynamic.int)) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) } -pub fn get_total_searches(db: pgo.Connection) { - "SELECT SUM(occurences) FROM search_analytics" - |> pgo.execute(db, [], dynamic.element(0, dynamic.int)) +pub fn get_total_searches(db: pog.Connection) { + "SELECT SUM(occurences) occurences FROM search_analytics" + |> pog.query + |> pog.returning(dynamic.field("occurences", dynamic.int)) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) } -pub fn get_total_signatures(db: pgo.Connection) { - "SELECT COUNT(*) FROM package_type_fun_signature" - |> pgo.execute(db, [], dynamic.element(0, dynamic.int)) +pub fn get_total_signatures(db: pog.Connection) { + "SELECT COUNT(*) c FROM package_type_fun_signature" + |> pog.query + |> pog.returning(dynamic.field("c", dynamic.int)) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) } -pub fn get_total_packages(db: pgo.Connection) { - "SELECT COUNT(*) FROM package" - |> pgo.execute(db, [], dynamic.element(0, dynamic.int)) +pub fn get_total_packages(db: pog.Connection) { + "SELECT COUNT(*) c FROM package" + |> pog.query + |> pog.returning(dynamic.field("c", dynamic.int)) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) } fn add_new_package_owners( - db: pgo.Connection, + db: pog.Connection, owners: List(HexUser), current_owners: List(Int), package_id: Int, @@ -220,18 +249,20 @@ fn add_new_package_owners( owners |> list.filter(fn(o) { bool.negate(list.contains(current_owners, o.id)) }) |> list.map(fn(u) { - let hex_user_id = pgo.int(u.id) - let pid = pgo.int(package_id) "INSERT INTO package_owner (hex_user_id, package_id) VALUES ($1, $2)" - |> pgo.execute(db, [hex_user_id, pid], dynamic.dynamic) + |> pog.query + |> pog.parameter(pog.int(u.id)) + |> pog.parameter(pog.int(package_id)) + |> pog.returning(dynamic.dynamic) + |> pog.execute(db) }) |> result.all() |> result.map_error(error.DatabaseError) } fn remove_old_package_owners( - db: pgo.Connection, + db: pog.Connection, owners: List(HexUser), current_owners: List(Int), package_id: Int, @@ -240,19 +271,21 @@ fn remove_old_package_owners( current_owners |> list.filter(fn(id) { list.contains(curr, id) }) |> list.map(fn(u) { - let hex_user_id = pgo.int(u) - let pid = pgo.int(package_id) "DELETE FROM package_owner WHERE package_owner.hex_user_id = $1 AND package_owner.package_id = $2" - |> pgo.execute(db, [hex_user_id, pid], dynamic.dynamic) + |> pog.query + |> pog.parameter(pog.int(u)) + |> pog.parameter(pog.int(package_id)) + |> pog.returning(dynamic.dynamic) + |> pog.execute(db) }) |> result.all() |> result.map_error(error.DatabaseError) } pub fn sync_package_owners( - db: pgo.Connection, + db: pog.Connection, package_id: Int, owners: List(hexpm.PackageOwner), ) { @@ -263,24 +296,7 @@ pub fn sync_package_owners( Ok(Nil) } -pub fn upsert_package(db: pgo.Connection, package: hexpm.Package) { - let name = pgo.text(package.name) - let hex_url = pgo.nullable(pgo.text, package.html_url) - let docs = pgo.nullable(pgo.text, package.docs_html_url) - let repo = - package.meta.links - |> dict.get("Repository") - |> option.from_result() - |> pgo.nullable(pgo.text, _) - let links = - package.meta.links - |> helpers.json_dict() - |> pgo.text() - let licenses = - package.meta.licenses - |> helpers.json_list() - |> pgo.text() - let description = pgo.nullable(pgo.text, package.meta.description) +pub fn upsert_package(db: pog.Connection, package: hexpm.Package) { "INSERT INTO package (name, repository, documentation, hex_url, links, licenses, description) VALUES ($1, $2, $3, $4, $5, $6, $7) @@ -293,11 +309,21 @@ pub fn upsert_package(db: pgo.Connection, package: hexpm.Package) { licenses = $6, description = $7 RETURNING id" - |> pgo.execute( - db, - [name, repo, docs, hex_url, links, licenses, description], - dynamic.element(0, dynamic.int), - ) + |> pog.query + |> pog.parameter(pog.text(package.name)) + |> pog.parameter(pog.nullable(pog.text, package.html_url)) + |> pog.parameter(pog.nullable(pog.text, package.docs_html_url)) + |> pog.parameter({ + package.meta.links + |> dict.get("Repository") + |> option.from_result() + |> pog.nullable(pog.text, _) + }) + |> pog.parameter(package.meta.links |> helpers.json_dict() |> pog.text()) + |> pog.parameter(package.meta.licenses |> helpers.json_list() |> pog.text()) + |> pog.parameter(pog.nullable(pog.text, package.meta.description)) + |> pog.returning(dynamic.field("id", dynamic.int)) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.try(fn(response) { response.rows @@ -307,30 +333,12 @@ pub fn upsert_package(db: pgo.Connection, package: hexpm.Package) { } pub fn upsert_release( - db: pgo.Connection, + db: pog.Connection, package_id: Int, release: hexpm.Release, package_interface: Option(String), gleam_toml: Option(String), ) { - let package_id = pgo.int(package_id) - let version = pgo.text(release.version) - let url = pgo.text(release.url) - let package_interface = pgo.nullable(pgo.text, package_interface) - let gleam_toml = pgo.nullable(pgo.text, gleam_toml) - let inserted_at = - release.inserted_at - |> birl.to_erlang_universal_datetime - |> dynamic.from - |> dynamic.unsafe_coerce - let args = [ - package_id, - version, - url, - package_interface, - gleam_toml, - inserted_at, - ] "INSERT INTO package_release ( package_id, version, @@ -346,38 +354,45 @@ pub fn upsert_release( gleam_toml = $5, inserted_at = $6 RETURNING id, package_interface, gleam_toml" - |> pgo.execute( - db, - args, - dynamic.tuple3( - dynamic.int, - dynamic.optional(dynamic.string), - dynamic.optional(dynamic.string), - ), - ) + |> pog.query + |> pog.parameter(pog.int(package_id)) + |> pog.parameter(pog.text(release.version)) + |> pog.parameter(pog.text(release.url)) + |> pog.parameter(pog.nullable(pog.text, package_interface)) + |> pog.parameter(pog.nullable(pog.text, gleam_toml)) + |> pog.parameter({ + release.inserted_at + |> birl.to_erlang_universal_datetime + |> coerce + }) + |> pog.returning(dynamic.decode3( + fn(a, b, c) { #(a, b, c) }, + dynamic.field("id", dynamic.int), + dynamic.field("package_interface", dynamic.optional(dynamic.string)), + dynamic.field("gleam_toml", dynamic.optional(dynamic.string)), + )) + |> pog.execute(db) |> result.map_error(error.DatabaseError) } pub fn lookup_release( - db: pgo.Connection, + db: pog.Connection, package_id: Int, release: hexpm.Release, ) { - let package_id = pgo.int(package_id) - let version = pgo.text(release.version) - let args = [package_id, version] "SELECT id, package_interface, gleam_toml - FROM package_release - WHERE package_id = $1 AND version = $2" - |> pgo.execute( - db, - args, - dynamic.tuple3( - dynamic.int, - dynamic.optional(dynamic.string), - dynamic.optional(dynamic.string), - ), - ) + FROM package_release + WHERE package_id = $1 AND version = $2" + |> pog.query + |> pog.parameter(pog.int(package_id)) + |> pog.parameter(pog.text(release.version)) + |> pog.returning(dynamic.decode3( + fn(a, b, c) { #(a, b, c) }, + dynamic.field("id", dynamic.int), + dynamic.field("package_interface", dynamic.optional(dynamic.string)), + dynamic.field("gleam_toml", dynamic.optional(dynamic.string)), + )) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.try(fn(res) { res.rows @@ -387,84 +402,96 @@ pub fn lookup_release( } pub fn add_package_gleam_constraint( - db: pgo.Connection, + db: pog.Connection, constraint: String, release_id: Int, ) { - let constraint = pgo.text(constraint) - let release_id = pgo.int(release_id) "UPDATE package_release SET gleam_constraint = $1 WHERE id = $2" - |> pgo.execute(db, [constraint, release_id], dynamic.dynamic) + |> pog.query + |> pog.parameter(pog.text(constraint)) + |> pog.parameter(pog.int(release_id)) + |> pog.execute(db) |> result.replace(Nil) |> result.map_error(error.DatabaseError) } -pub fn add_package_gleam_retirement( - db: pgo.Connection, +pub fn add_package_retirement( + db: pog.Connection, retirement: hexpm.ReleaseRetirement, release_id: Int, ) { - let retirement = - json.object([ - #("message", json.nullable(retirement.message, json.string)), - #("reason", { - json.string(case retirement.reason { - hexpm.OtherReason -> "other-reason" - hexpm.Invalid -> "invalid" - hexpm.Security -> "security" - hexpm.Deprecated -> "deprecated" - hexpm.Renamed -> "renamed" - }) - }), - ]) - |> json.to_string() - |> pgo.text() - let release_id = pgo.int(release_id) "UPDATE package_release SET retirement = $1 WHERE id = $2" - |> pgo.execute(db, [retirement, release_id], dynamic.dynamic) + |> pog.query + |> pog.parameter(pog.text(encode_retirement(retirement))) + |> pog.parameter(pog.int(release_id)) + |> pog.execute(db) |> result.replace(Nil) |> result.map_error(error.DatabaseError) } +fn encode_retirement(retirement: hexpm.ReleaseRetirement) { + json.object([ + #("message", json.nullable(retirement.message, json.string)), + #("reason", { + json.string(case retirement.reason { + hexpm.OtherReason -> "other-reason" + hexpm.Invalid -> "invalid" + hexpm.Security -> "security" + hexpm.Deprecated -> "deprecated" + hexpm.Renamed -> "renamed" + }) + }), + ]) + |> json.to_string() +} + pub fn get_package_release_ids( - db: pgo.Connection, + db: pog.Connection, package: package_interface.Package, ) { - use response <- result.try({ - let args = [pgo.text(package.name), pgo.text(package.version)] - "SELECT - package.id package_id, - package_release.id package_release_id - FROM package - JOIN package_release - ON package_release.package_id = package.id - WHERE package.name = $1 - AND package_release.version = $2" - |> pgo.execute(db, args, dynamic.tuple2(dynamic.int, dynamic.int)) - |> result.map_error(error.DatabaseError) - }) - response.rows - |> list.first() - |> result.replace_error(error.UnknownError( - "No release found for " <> package.name <> "@" <> package.version, + "SELECT + package.id package_id, + package_release.id package_release_id + FROM package + JOIN package_release + ON package_release.package_id = package.id + WHERE package.name = $1 + AND package_release.version = $2" + |> pog.query + |> pog.parameter(pog.text(package.name)) + |> pog.parameter(pog.text(package.version)) + |> pog.returning(dynamic.decode2( + pair.new, + dynamic.field("package_id", dynamic.int), + dynamic.field("package_release_id", dynamic.int), )) + |> pog.execute(db) + |> result.map_error(error.DatabaseError) + |> result.try(fn(response) { + list.first(response.rows) + |> result.replace_error(error.UnknownError( + "No release found for " <> package.name <> "@" <> package.version, + )) + }) } -pub fn upsert_package_module(db: pgo.Connection, module: context.Module) { +pub fn upsert_package_module(db: pog.Connection, module: context.Module) { use response <- result.try({ - let args = [ - pgo.text(module.name), - module.module.documentation - |> string.join("\n") - |> pgo.text(), - pgo.int(module.release_id), - ] "INSERT INTO package_module (name, documentation, package_release_id) VALUES ($1, $2, $3) ON CONFLICT (name, package_release_id) DO UPDATE SET documentation = $2 RETURNING id" - |> pgo.execute(db, args, dynamic.element(0, dynamic.int)) + |> pog.query + |> pog.parameter(pog.text(module.name)) + |> pog.parameter({ + module.module.documentation + |> string.join("\n") + |> pog.text() + }) + |> pog.parameter(pog.int(module.release_id)) + |> pog.returning(dynamic.field("id", dynamic.int)) + |> pog.execute(db) |> result.map_error(error.DatabaseError) }) response.rows @@ -486,7 +513,7 @@ fn implementations_pgo(implementations: package_interface.Implementations) { } pub fn upsert_package_type_fun_signature( - db db: pgo.Connection, + db db: pog.Connection, kind kind: SignatureKind, name name: String, documentation documentation: Option(String), @@ -498,12 +525,6 @@ pub fn upsert_package_type_fun_signature( deprecation deprecation: Option(package_interface.Deprecation), implementations implementations: Option(package_interface.Implementations), ) { - let kind = case kind { - Function -> "function" - TypeAlias -> "type_alias" - TypeDefinition -> "type_definition" - Constant -> "constant" - } "INSERT INTO package_type_fun_signature ( name, documentation, @@ -527,39 +548,47 @@ pub fn upsert_package_type_fun_signature( deprecation = $9, implementations = $10 RETURNING id" - |> pgo.execute( - db, - [ - pgo.text(name), - documentation - |> option.unwrap("") - |> string.trim() - |> pgo.text(), - pgo.text(signature), - json_signature - |> json.to_string() - |> pgo.text(), - pgo.text(kind), - dynamic.unsafe_coerce(dynamic.from(parameters)), - metadata - |> json.to_string() - |> pgo.text(), - pgo.int(module_id), - deprecation - |> option.map(fn(d) { d.message }) - |> pgo.nullable(pgo.text, _), - implementations - |> option.map(implementations_pgo) - |> option.map(pgo.text) - |> option.unwrap(pgo.null()), - ], - dynamic.element(0, dynamic.int), - ) + |> pog.query + |> pog.parameter(pog.text(name)) + |> pog.parameter({ + documentation + |> option.unwrap("") + |> string.trim() + |> pog.text() + }) + |> pog.parameter(pog.text(signature)) + |> pog.parameter(json_signature |> json.to_string() |> pog.text()) + |> pog.parameter(pog.text(kind_to_string(kind))) + |> pog.parameter(coerce(parameters)) + |> pog.parameter(metadata |> json.to_string() |> pog.text()) + |> pog.parameter(pog.int(module_id)) + |> pog.parameter({ + deprecation + |> option.map(fn(d) { d.message }) + |> pog.nullable(pog.text, _) + }) + |> pog.parameter({ + implementations + |> option.map(implementations_pgo) + |> option.map(pog.text) + |> option.unwrap(pog.null()) + }) + |> pog.returning(dynamic.field("id", dynamic.int)) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -pub fn find_similar_type_names(db: pgo.Connection, name: String) { +fn kind_to_string(kind: SignatureKind) -> String { + case kind { + Function -> "function" + TypeAlias -> "type_alias" + TypeDefinition -> "type_definition" + Constant -> "constant" + } +} + +pub fn find_similar_type_names(db: pog.Connection, name: String) { "SELECT * FROM (VALUES ('Int'), @@ -577,22 +606,24 @@ pub fn find_similar_type_names(db: pgo.Connection, name: String) { FROM package_type_fun_signature WHERE (kind = 'type_definition' OR kind = 'type_alias') AND levenshtein_less_equal(name, $1, 2) <= 2" - |> pgo.execute(db, [pgo.text(name)], dynamic.element(0, dynamic.string)) + |> pog.query + |> pog.parameter(pog.text(name)) + |> pog.returning(dynamic.field("name", dynamic.string)) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -pub fn name_search(db: pgo.Connection, query: String) { - let query = pgo.text(query) +pub fn name_search(db: pog.Connection, query: String) { "SELECT DISTINCT ON (package_rank, ordering, type_name, signature_kind, module_name) s.name type_name, - s.documentation, + s.documentation documentation, s.kind signature_kind, - s.metadata, + s.metadata metadata, s.json_signature, m.name module_name, - p.name, - r.version, + p.name package_name, + r.version version, p.rank package_rank, string_to_array(regexp_replace(r.version, '([0-9]+).([0-9]+).([0-9]+).*', '\\1.\\2.\\3'), '.')::int[] AS ordering FROM package_type_fun_signature s @@ -605,23 +636,25 @@ pub fn name_search(db: pgo.Connection, query: String) { WHERE s.name = $1 ORDER BY package_rank DESC, ordering DESC, type_name, signature_kind, module_name LIMIT 100" - |> pgo.execute(db, [query], decode_type_search) + |> pog.query + |> pog.parameter(pog.text(query)) + |> pog.returning(type_search.decode) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -pub fn module_and_name_search(db: pgo.Connection, query: String) { - let query = pgo.text(query) +pub fn module_and_name_search(db: pog.Connection, query: String) { "WITH splitted_name AS (SELECT string_to_array($1, '.') AS full_name) SELECT DISTINCT ON (package_rank, ordering, type_name, signature_kind, module_name) s.name type_name, - s.documentation, + s.documentation documentation, s.kind signature_kind, - s.metadata, + s.metadata metadata, s.json_signature, m.name module_name, - p.name, - r.version, + p.name package_name, + r.version version, p.rank package_rank, string_to_array(regexp_replace(r.version, '([0-9]+).([0-9]+).([0-9]+).*', '\\1.\\2.\\3'), '.')::int[] AS ordering FROM package_type_fun_signature s @@ -637,7 +670,10 @@ pub fn module_and_name_search(db: pgo.Connection, query: String) { AND m.name LIKE '%' || lower(s_n.full_name[1]) || '%' ORDER BY package_rank DESC, ordering DESC, type_name, signature_kind, module_name LIMIT 100" - |> pgo.execute(db, [query], decode_type_search) + |> pog.query + |> pog.parameter(pog.text(query)) + |> pog.returning(type_search.decode) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } @@ -653,18 +689,16 @@ fn transform_query(q: String) { |> string.join(" ") } -pub fn content_search(db: pgo.Connection, query: String) { - let pattern = pgo.text(transform_query(query)) - let query = pgo.text(query) +pub fn content_search(db: pog.Connection, query: String) { "SELECT DISTINCT ON (package_rank, ordering, type_name, signature_kind, module_name) s.name type_name, - s.documentation, + s.documentation documentation, s.kind signature_kind, - s.metadata, + s.metadata metadata, s.json_signature, m.name module_name, - p.name, - r.version, + p.name package_name, + r.version version, p.rank package_rank, string_to_array(regexp_replace(r.version, '([0-9]+).([0-9]+).([0-9]+).*', '\\1.\\2.\\3'), '.')::int[] AS ordering FROM package_type_fun_signature s @@ -683,50 +717,25 @@ pub fn content_search(db: pgo.Connection, query: String) { ) ORDER BY package_rank DESC, ordering DESC, type_name, signature_kind, module_name LIMIT 100" - |> pgo.execute(db, [query, pattern], decode_type_search) + |> pog.query + |> pog.parameter(pog.text(query)) + |> pog.parameter(pog.text(transform_query(query))) + |> pog.returning(type_search.decode) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -fn decode_type_search(dyn) { - dynamic.decode8( - fn(a, b, c, d, e, f, g, h) { #(a, b, c, d, e, f, g, h) }, - dynamic.element(0, dynamic.string), - dynamic.element(1, dynamic.string), - dynamic.element(2, dynamic.string), - dynamic.element(3, dynamic.dynamic), - dynamic.element(4, dynamic.dynamic), - dynamic.element(5, dynamic.string), - dynamic.element(6, dynamic.string), - dynamic.element(7, dynamic.string), - )(dyn) -} - -pub fn type_search_to_json(item) { - let #(a, b, c, d, e, f, g, h) = item - json.object([ - #("name", json.string(a)), - #("documentation", json.string(b)), - #("kind", json.string(c)), - #("metadata", dynamic.unsafe_coerce(d)), - #("json_signature", dynamic.unsafe_coerce(e)), - #("module_name", json.string(f)), - #("package_name", json.string(g)), - #("version", json.string(h)), - ]) -} - -pub fn signature_search(db: pgo.Connection, q: String) { - let query = pgo.text(q) +pub fn signature_search(db: pog.Connection, q: String) { "SELECT DISTINCT ON (package_rank, ordering, type_name, signature_kind, module_name) s.name type_name, - s.documentation, + s.documentation documentation, s.kind signature_kind, - s.metadata, + s.metadata metadata, s.json_signature, m.name module_name, - p.name, - r.version, + p.name package_name, + r.version version, p.rank package_rank, string_to_array(regexp_replace(r.version, '([0-9]+).([0-9]+).([0-9]+).*', '\\1.\\2.\\3'), '.')::int[] AS ordering FROM package_type_fun_signature s @@ -739,22 +748,24 @@ pub fn signature_search(db: pgo.Connection, q: String) { WHERE to_tsvector('english', s.signature_) @@ websearch_to_tsquery($1) ORDER BY package_rank DESC, ordering DESC, type_name, signature_kind, module_name LIMIT 100" - |> pgo.execute(db, [query], decode_type_search) + |> pog.query + |> pog.parameter(pog.text(q)) + |> pog.returning(type_search.decode) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -pub fn documentation_search(db: pgo.Connection, q: String) { - let query = pgo.text(q) +pub fn documentation_search(db: pog.Connection, q: String) { "SELECT DISTINCT ON (package_rank, ordering, type_name, signature_kind, module_name) s.name type_name, - s.documentation, + s.documentation documentation, s.kind signature_kind, - s.metadata, + s.metadata metadata, s.json_signature, m.name module_name, - p.name, - r.version, + p.name package_name, + r.version version, p.rank package_rank, string_to_array(regexp_replace(r.version, '([0-9]+).([0-9]+).([0-9]+).*', '\\1.\\2.\\3'), '.')::int[] AS ordering FROM package_type_fun_signature s @@ -767,22 +778,24 @@ pub fn documentation_search(db: pgo.Connection, q: String) { WHERE to_tsvector('english', s.documentation) @@ websearch_to_tsquery($1) ORDER BY package_rank DESC, ordering DESC, type_name, signature_kind, module_name LIMIT 100" - |> pgo.execute(db, [query], decode_type_search) + |> pog.query + |> pog.parameter(pog.text(q)) + |> pog.returning(type_search.decode) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -pub fn module_search(db: pgo.Connection, q: String) { - let query = pgo.text(q) +pub fn module_search(db: pog.Connection, q: String) { "SELECT DISTINCT ON (package_rank, ordering, type_name, signature_kind, module_name) s.name type_name, - s.documentation, + s.documentation documentation, s.kind signature_kind, - s.metadata, + s.metadata metadata, s.json_signature, m.name module_name, - p.name, - r.version, + p.name package_name, + r.version version, p.rank package_rank, string_to_array(regexp_replace(r.version, '([0-9]+).([0-9]+).([0-9]+).*', '\\1.\\2.\\3'), '.')::int[] AS ordering FROM package_type_fun_signature s @@ -804,12 +817,15 @@ pub fn module_search(db: pgo.Connection, q: String) { AND r.id = m.package_release_id ) ORDER BY package_rank DESC, ordering DESC, type_name, signature_kind, module_name" - |> pgo.execute(db, [query], decode_type_search) + |> pog.query + |> pog.parameter(pog.text(q)) + |> pog.returning(type_search.decode) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -pub fn exact_type_search(db: pgo.Connection, q: List(Int)) { +pub fn exact_type_search(db: pog.Connection, q: List(Int)) { use <- bool.guard(when: list.is_empty(q), return: Ok([])) let ids = list.index_map(q, fn(_, idx) { "$" <> int.to_string(idx + 1) }) @@ -817,13 +833,13 @@ pub fn exact_type_search(db: pgo.Connection, q: List(Int)) { { "SELECT DISTINCT ON (package_rank, ordering, type_name, signature_kind, module_name) s.name type_name, - s.documentation, + s.documentation documentation, s.kind signature_kind, - s.metadata, + s.metadata metadata, s.json_signature, m.name module_name, - p.name, - r.version, + p.name package_name, + r.version version, p.rank package_rank, string_to_array(regexp_replace(r.version, '([0-9]+).([0-9]+).([0-9]+).*', '\\1.\\2.\\3'), '.')::int[] AS ordering FROM package_type_fun_signature s @@ -838,62 +854,82 @@ pub fn exact_type_search(db: pgo.Connection, q: List(Int)) { <> ") ORDER BY package_rank DESC, ordering DESC, type_name, signature_kind, module_name" } - |> pgo.execute(db, list.map(q, pgo.int), decode_type_search) + |> pog.query + |> list.fold(q, _, fn(query, q) { pog.parameter(query, pog.int(q)) }) + |> pog.returning(type_search.decode) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -pub fn select_gleam_toml(db: pgo.Connection, offset: Int) { +pub fn select_gleam_toml(db: pog.Connection, offset: Int) { "SELECT gleam_toml FROM package_release WHERE gleam_toml IS NOT NULL ORDER BY id LIMIT 100 OFFSET $1" - |> pgo.execute(db, [pgo.int(offset)], dynamic.element(0, dynamic.string)) + |> pog.query + |> pog.parameter(pog.int(offset)) + |> pog.returning(dynamic.field("gleam_toml", dynamic.string)) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } -pub fn update_package_rank(db: pgo.Connection, package: String, rank: Int) { +pub fn update_package_rank(db: pog.Connection, package: String, rank: Int) { "UPDATE package SET rank = $2 WHERE name = $1" - |> pgo.execute(db, [pgo.text(package), pgo.int(rank)], dynamic.dynamic) + |> pog.query + |> pog.parameter(pog.text(package)) + |> pog.parameter(pog.int(rank)) + |> pog.returning(dynamic.dynamic) + |> pog.execute(db) |> result.map_error(error.DatabaseError) } -pub fn select_package_repository_address(db: pgo.Connection, offset: Int) { - let decoder = fn(dyn) { - dynamic.tuple2(dynamic.int, dynamic.optional(dynamic.string))(dyn) +pub fn select_package_repository_address(db: pog.Connection, offset: Int) { + "SELECT id, repository FROM package LIMIT 100 OFFSET $1" + |> pog.query + |> pog.parameter(pog.int(offset)) + |> pog.returning(fn(dyn) { + dynamic.decode2( + pair.new, + dynamic.field("id", dynamic.int), + dynamic.field("repository", dynamic.optional(dynamic.string)), + )(dyn) |> result.map(fn(content) { case content { #(id, Some(repo)) -> Some(#(id, repo)) #(_, None) -> None } }) - } - "SELECT id, repository FROM package LIMIT 100 OFFSET $1" - |> pgo.execute(db, [pgo.int(offset)], decoder) + }) + |> pog.execute(db) |> result.map_error(error.DatabaseError) |> result.map(fn(r) { r.rows }) } pub fn update_package_popularity( - db: pgo.Connection, + db: pog.Connection, url: String, popularity: Dict(String, Int), ) { - let popularity = + "UPDATE package SET popularity = $2 WHERE repository = $1" + |> pog.query + |> pog.parameter(pog.text(url)) + |> pog.parameter({ dict.to_list(popularity) |> list.map(pair.map_second(_, json.int)) |> json.object() |> json.to_string() - |> pgo.text() - "UPDATE package SET popularity = $2 WHERE repository = $1" - |> pgo.execute(db, [pgo.text(url), popularity], dynamic.dynamic) + |> pog.text() + }) + |> pog.returning(dynamic.dynamic) + |> pog.execute(db) |> result.map_error(error.DatabaseError) } -pub fn select_package_by_popularity(db: pgo.Connection, page: Int) { +pub fn select_package_by_popularity(db: pog.Connection, page: Int) { let offset = 40 * page "SELECT name, @@ -909,37 +945,15 @@ pub fn select_package_by_popularity(db: pgo.Connection, page: Int) { ORDER BY popularity -> 'github' DESC LIMIT 40 OFFSET $1" - |> pgo.execute( - db, - [pgo.int(offset)], - dynamic.decode8( - fn(a, b, c, d, e, f, g, h) { - json.object([ - #("name", json.string(a)), - #("repository", json.nullable(b, json.string)), - #("documentation", json.nullable(c, json.string)), - #("hex-url", json.nullable(d, json.string)), - #("licenses", json.string(e)), - #("description", json.nullable(f, json.string)), - #("rank", json.int(g)), - #("popularity", json.nullable(h, json.string)), - ]) - }, - dynamic.element(0, dynamic.string), - dynamic.element(1, dynamic.optional(dynamic.string)), - dynamic.element(2, dynamic.optional(dynamic.string)), - dynamic.element(3, dynamic.optional(dynamic.string)), - dynamic.element(4, dynamic.string), - dynamic.element(5, dynamic.optional(dynamic.string)), - dynamic.element(6, dynamic.int), - dynamic.element(7, dynamic.optional(dynamic.string)), - ), - ) + |> pog.query + |> pog.parameter(pog.int(offset)) + |> pog.returning(package.decode) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) } -pub fn select_package_by_updated_at(db: pgo.Connection) { +pub fn select_package_by_updated_at(db: pog.Connection) { "SELECT name, repository, @@ -951,60 +965,41 @@ pub fn select_package_by_updated_at(db: pgo.Connection) { popularity FROM package ORDER BY updated_at DESC" - |> pgo.execute( - db, - [], - dynamic.decode8( - fn(a, b, c, d, e, f, g, h) { - json.object([ - #("name", json.string(a)), - #("repository", json.nullable(b, json.string)), - #("documentation", json.nullable(c, json.string)), - #("hex-url", json.nullable(d, json.string)), - #("licenses", json.string(e)), - #("description", json.nullable(f, json.string)), - #("rank", json.int(g)), - #("popularity", json.nullable(h, json.string)), - ]) - }, - dynamic.element(0, dynamic.string), - dynamic.element(1, dynamic.optional(dynamic.string)), - dynamic.element(2, dynamic.optional(dynamic.string)), - dynamic.element(3, dynamic.optional(dynamic.string)), - dynamic.element(4, dynamic.string), - dynamic.element(5, dynamic.optional(dynamic.string)), - dynamic.element(6, dynamic.int), - dynamic.element(7, dynamic.optional(dynamic.string)), - ), - ) + |> pog.query + |> pog.returning(package.decode) + |> pog.execute(db) |> result.map(fn(r) { r.rows }) |> result.map_error(error.DatabaseError) } pub fn insert_analytics( - db: pgo.Connection, + db: pog.Connection, id: Int, table_name: String, content: Dict(String, Int), ) { - let day = - birl.now() - |> birl.to_erlang_universal_datetime() - |> pair.map_second(fn(_) { #(0, 0, 0) }) - |> dynamic.from() - |> dynamic.unsafe_coerce() - let content = + "INSERT INTO analytics (foreign_id, table_name, content, day) + VALUES ($1, $2, $3, $4) + ON CONFLICT (foreign_id, table_name, day) DO UPDATE + SET content = $3" + |> pog.query + |> pog.parameter(pog.int(id)) + |> pog.parameter(pog.text(table_name)) + |> pog.parameter({ content |> dict.to_list() |> list.map(pair.map_second(_, json.int)) |> json.object() |> json.to_string() - |> pgo.text() - let parameters = [pgo.int(id), pgo.text(table_name), content, day] - "INSERT INTO analytics (foreign_id, table_name, content, day) - VALUES ($1, $2, $3, $4) - ON CONFLICT (foreign_id, table_name, day) DO UPDATE - SET content = $3" - |> pgo.execute(db, parameters, dynamic.dynamic) + |> pog.text() + }) + |> pog.parameter({ + birl.now() + |> birl.to_erlang_universal_datetime() + |> pair.map_second(fn(_) { #(0, 0, 0) }) + |> coerce + }) + |> pog.returning(dynamic.dynamic) + |> pog.execute(db) |> result.map_error(error.DatabaseError) } diff --git a/apps/backend/src/backend/router.gleam b/apps/backend/src/backend/router.gleam index e3fdc0b..0454e86 100644 --- a/apps/backend/src/backend/router.gleam +++ b/apps/backend/src/backend/router.gleam @@ -1,222 +1,24 @@ -import api/hex -import backend/config.{type Context} -import backend/error -import backend/gleam/type_search/msg as type_search -import backend/postgres/queries +import backend/context.{type Context} +import backend/router/handlers import backend/web -import birl import cors_builder as cors -import gleam/erlang/process import gleam/http -import gleam/int -import gleam/json -import gleam/list -import gleam/option -import gleam/result -import gleam/string_builder -import tasks/hex as syncing import wisp.{type Request, type Response} -fn empty_json() { - let content = "{}" - content - |> string_builder.from_string() - |> wisp.json_response(200) -} - -fn search(query: String, ctx: Context) { - wisp.log_notice("Searching for " <> query) - let _ = queries.upsert_search_analytics(ctx.db, query) - - let exact_type_searches = - option.then(ctx.type_search_subject, fn(subject) { - process.try_call(subject, type_search.Find(_, query), within: 25_000) - |> option.from_result - |> option.flatten - }) - |> option.unwrap([]) - |> queries.exact_type_search(ctx.db, _) - |> result.map_error(error.debug_log) - |> result.unwrap([]) - - let exact_name_matches = - queries.name_search(ctx.db, query) - |> result.map_error(error.debug_log) - |> result.unwrap([]) - |> list.filter(fn(i) { !list.contains(exact_type_searches, i) }) - - let exact_module_and_name_matches = - queries.module_and_name_search(ctx.db, query) - |> result.map_error(error.debug_log) - |> result.unwrap([]) - |> list.filter(fn(i) { - !list.contains(list.concat([exact_type_searches, exact_name_matches]), i) - }) - - let exact_matches = - list.concat([exact_name_matches, exact_module_and_name_matches]) - - let matches = - queries.content_search(ctx.db, query) - |> result.map_error(error.debug_log) - |> result.unwrap([]) - |> list.filter(fn(i) { - !list.contains(list.concat([exact_matches, exact_type_searches]), i) - }) - - let signature_searches = - queries.signature_search(ctx.db, query) - |> result.map_error(error.debug_log) - |> result.unwrap([]) - |> list.filter(fn(i) { - !list.contains( - list.concat([exact_matches, exact_type_searches, matches]), - i, - ) - }) - - let documentation_searches = - queries.documentation_search(ctx.db, query) - |> result.map_error(error.debug_log) - |> result.unwrap([]) - |> list.filter(fn(i) { - !list.contains( - list.concat([ - exact_matches, - exact_type_searches, - matches, - signature_searches, - ]), - i, - ) - }) - - let module_searches = - queries.module_search(ctx.db, query) - |> result.map_error(error.debug_log) - |> result.unwrap([]) - |> list.filter(fn(i) { - !list.contains( - list.concat([ - exact_matches, - exact_type_searches, - matches, - signature_searches, - documentation_searches, - ]), - i, - ) - }) - - json.object([ - #( - "exact-type-matches", - json.array(exact_type_searches, queries.type_search_to_json), - ), - #("exact-matches", json.array(exact_matches, queries.type_search_to_json)), - #("matches", json.array(matches, queries.type_search_to_json)), - #("searches", json.array(signature_searches, queries.type_search_to_json)), - #("docs-searches", { - json.array(documentation_searches, queries.type_search_to_json) - }), - #("module-searches", { - json.array(module_searches, queries.type_search_to_json) - }), - ]) -} - -fn encode_package(package: #(String, String, Int, option.Option(Int))) { - let #(name, repository, rank, popularity) = package - json.object([ - #("name", json.string(name)), - #("repository", json.string(repository)), - #("rank", json.int(rank)), - #("popularity", json.nullable(popularity, json.int)), - ]) -} - pub fn handle_get(req: Request, ctx: Context) { case wisp.path_segments(req) { ["healthcheck"] -> wisp.ok() - ["packages"] -> - queries.select_package_by_updated_at(ctx.db) - |> result.unwrap([]) - |> json.preprocessed_array - |> json.to_string_builder - |> wisp.json_response(200) - ["trendings"] -> - wisp.get_query(req) - |> list.find(fn(item) { item.0 == "page" }) - |> result.try(fn(item) { int.parse(item.1) }) - |> result.try_recover(fn(_) { Ok(0) }) - |> result.unwrap(0) - |> queries.select_package_by_popularity(ctx.db, _) - |> result.map(fn(content) { - content - |> json.preprocessed_array() - |> json.to_string_builder() - |> wisp.json_response(200) - }) - |> result.unwrap(wisp.internal_server_error()) - ["analytics"] -> { - { - use timeseries <- result.try(queries.get_timeseries_count(ctx.db)) - use total <- result.try(queries.get_total_searches(ctx.db)) - use signatures <- result.try(queries.get_total_signatures(ctx.db)) - use packages <- result.try(queries.get_total_packages(ctx.db)) - use #(ranked, popular) <- result.try({ - queries.select_more_popular_packages(ctx.db) - }) - let total = list.first(total) |> result.unwrap(0) - let signatures = list.first(signatures) |> result.unwrap(0) - let packages = list.first(packages) |> result.unwrap(0) - Ok(#(timeseries, total, signatures, packages, ranked, popular)) - } - |> result.map(fn(content) { - let #(timeseries, total, signatures, packages, ranked, popular) = - content - json.object([ - #("total", json.int(total)), - #("signatures", json.int(signatures)), - #("packages", json.int(packages)), - #("ranked", json.array(ranked, encode_package)), - #("popular", json.array(popular, encode_package)), - #("timeseries", { - json.array(timeseries, fn(row) { - json.object([ - #("count", json.int(row.0)), - #("date", json.string(birl.to_iso8601(row.1))), - ]) - }) - }), - ]) - |> json.to_string_builder - |> wisp.json_response(200) - }) - |> result.map_error(error.debug_log) - |> result.unwrap(wisp.internal_server_error()) - } - ["search"] -> { - wisp.get_query(req) - |> list.find(fn(item) { item.0 == "q" }) - |> result.replace_error(error.EmptyError) - |> result.map(fn(item) { search(item.1, ctx) }) - |> result.unwrap(json.object([#("error", json.string("internal"))])) - |> json.to_string_builder() - |> wisp.json_response(200) - } + ["packages"] -> handlers.packages(req, ctx) + ["trendings"] -> handlers.trendings(req, ctx) + ["analytics"] -> handlers.analytics(req, ctx) + ["search"] -> handlers.search(req, ctx) _ -> wisp.not_found() } } pub fn handle_post(req: Request, ctx: Context) { case wisp.path_segments(req) { - ["packages", "update", name] -> { - let _ = - hex.get_package(name, ctx.hex_api_key) - |> result.try(fn(package) { syncing.sync_package(ctx, package) }) - empty_json() - } + ["packages", "update", name] -> handlers.package_update(req, ctx, name) _ -> wisp.not_found() } } diff --git a/apps/backend/src/backend/router/handlers.gleam b/apps/backend/src/backend/router/handlers.gleam new file mode 100644 index 0000000..b97d9d1 --- /dev/null +++ b/apps/backend/src/backend/router/handlers.gleam @@ -0,0 +1,177 @@ +import api/hex +import backend/context.{type Context} +import backend/error +import backend/gleam/type_search/msg +import backend/postgres/queries +import data/analytics +import data/package +import data/type_search +import gleam/erlang/process +import gleam/int +import gleam/json +import gleam/list +import gleam/option +import gleam/result +import gleam/string_tree +import pog +import tasks/hex as syncing +import wisp.{type Request} + +pub fn analytics(_req: Request, ctx: Context) { + select_all_analytics(ctx) + |> result.map(fn(content) { + content + |> analytics.encode + |> json.to_string_tree + |> wisp.json_response(200) + }) + |> result.map_error(error.debug_log) + |> result.unwrap(wisp.internal_server_error()) +} + +pub fn search(req: Request, ctx: Context) { + wisp.get_query(req) + |> list.find(fn(item) { item.0 == "q" }) + |> result.replace_error(error.EmptyError) + |> result.map(fn(item) { do_search(item.1, ctx) }) + |> result.unwrap(json.object([#("error", json.string("internal"))])) + |> json.to_string_tree + |> wisp.json_response(200) +} + +pub fn packages(_req: Request, ctx: Context) { + queries.select_package_by_updated_at(ctx.db) + |> result.unwrap([]) + |> json.array(package.encode) + |> json.to_string_tree + |> wisp.json_response(200) +} + +pub fn trendings(req: Request, ctx: Context) { + wisp.get_query(req) + |> list.find(fn(item) { item.0 == "page" }) + |> result.try(fn(item) { int.parse(item.1) }) + |> result.try_recover(fn(_) { Ok(0) }) + |> result.unwrap(0) + |> queries.select_package_by_popularity(ctx.db, _) + |> result.map(fn(content) { + content + |> json.array(package.encode) + |> json.to_string_tree + |> wisp.json_response(200) + }) + |> result.unwrap(wisp.internal_server_error()) +} + +pub fn package_update(_req: Request, ctx: Context, name: String) { + let _ = do_update_package(ctx, name) + "{}" + |> string_tree.from_string + |> wisp.json_response(200) +} + +fn do_update_package(ctx: Context, name: String) { + let found_package = hex.get_package(name, ctx.hex_api_key) + use package <- result.try(found_package) + syncing.sync_package(ctx, package) +} + +fn select_all_analytics(ctx: Context) { + use timeseries <- result.try(queries.get_timeseries_count(ctx.db)) + use total_searches <- result.try(queries.get_total_searches(ctx.db)) + use signatures <- result.try(queries.get_total_signatures(ctx.db)) + use packages <- result.try(queries.get_total_packages(ctx.db)) + use #(ranked, popular) <- result.try({ + queries.select_more_popular_packages(ctx.db) + }) + let total_searches = list.first(total_searches) |> result.unwrap(0) + let total_signatures = list.first(signatures) |> result.unwrap(0) + let total_indexed = list.first(packages) |> result.unwrap(0) + Ok(analytics.Analytics( + timeseries:, + total_searches:, + total_signatures:, + total_indexed:, + ranked:, + popular:, + )) +} + +fn do_search(query: String, ctx: Context) { + wisp.log_notice("Searching for " <> query) + let _ = queries.upsert_search_analytics(ctx.db, query) + + let exact_type_searches = exec_type_search(ctx, query) + + let exact_name_matches = + exact_type_searches + |> exec_search(ctx, query, queries.name_search, _) + + let exact_module_and_name_matches = + [exact_type_searches, exact_name_matches] + |> list.flatten + |> exec_search(ctx, query, queries.module_and_name_search, _) + + let exact_matches = + list.flatten([exact_name_matches, exact_module_and_name_matches]) + + let matches = + [exact_matches, exact_type_searches] + |> list.flatten + |> exec_search(ctx, query, queries.content_search, _) + + let signature_searches = + [exact_matches, exact_type_searches, matches] + |> list.flatten + |> exec_search(ctx, query, queries.signature_search, _) + + let documentation_searches = + [exact_matches, exact_type_searches, matches, signature_searches] + |> list.flatten + |> exec_search(ctx, query, queries.documentation_search, _) + + let module_searches = + [ + exact_matches, + exact_type_searches, + matches, + signature_searches, + documentation_searches, + ] + |> list.flatten + |> exec_search(ctx, query, queries.module_search, _) + + json.object([ + #("exact-type-matches", json.array(exact_type_searches, type_search.encode)), + #("exact-matches", json.array(exact_matches, type_search.encode)), + #("matches", json.array(matches, type_search.encode)), + #("searches", json.array(signature_searches, type_search.encode)), + #("docs-searches", json.array(documentation_searches, type_search.encode)), + #("module-searches", { json.array(module_searches, type_search.encode) }), + ]) +} + +fn exec_type_search(ctx: Context, query: String) { + option.then(ctx.type_search_subject, fn(subject) { + process.try_call(subject, msg.Find(_, query), within: 25_000) + |> option.from_result + |> option.flatten + }) + |> option.unwrap([]) + |> queries.exact_type_search(ctx.db, _) + |> result.map_error(error.debug_log) + |> result.unwrap([]) +} + +fn exec_search( + ctx: Context, + query: String, + run: fn(pog.Connection, String) -> + Result(List(type_search.TypeSearch), error.Error), + previous: List(type_search.TypeSearch), +) -> List(type_search.TypeSearch) { + run(ctx.db, query) + |> result.map_error(error.debug_log) + |> result.unwrap([]) + |> list.filter(fn(i) { !list.contains(previous, i) }) +} diff --git a/apps/backend/src/setup.gleam b/apps/backend/src/backend/setup.gleam similarity index 89% rename from apps/backend/src/setup.gleam rename to apps/backend/src/backend/setup.gleam index 661df93..2e94433 100644 --- a/apps/backend/src/setup.gleam +++ b/apps/backend/src/backend/setup.gleam @@ -1,4 +1,4 @@ -import backend/config +import backend/context import radiate import wisp @@ -7,7 +7,7 @@ fn print_radiate_update(_state: state, path: String) { } pub fn radiate() { - case config.is_dev() { + case context.is_dev() { False -> Nil True -> { let assert Ok(_) = diff --git a/apps/backend/src/backend/web.gleam b/apps/backend/src/backend/web.gleam index e7b117e..29aa89a 100644 --- a/apps/backend/src/backend/web.gleam +++ b/apps/backend/src/backend/web.gleam @@ -1,4 +1,4 @@ -import backend/config +import backend/context import cors_builder as cors_ import gleam/http import wisp.{type Request, type Response} @@ -14,14 +14,7 @@ pub fn foundations(req: Request, handler: Handler) -> Response { } pub fn cors() { - let origin = case config.is_dev() { - True -> cors_.allow_origin(_, "http://localhost:5173") - False -> fn(cors) { - cors - |> cors_.allow_origin("https://gloogle.run") - |> cors_.allow_origin("https://www.gloogle.run") - } - } + let origin = select_origin() cors_.new() |> origin() |> cors_.allow_method(http.Get) @@ -32,3 +25,16 @@ pub fn cors() { |> cors_.allow_header("sentry-trace") |> cors_.max_age(86_400) } + +fn select_origin() { + case context.read_environment() { + context.Development -> cors_.allow_origin(_, "http://localhost:5173") + context.Production -> allow_production + } +} + +fn allow_production(cors: cors_.Cors) -> cors_.Cors { + cors + |> cors_.allow_origin("https://gloogle.run") + |> cors_.allow_origin("https://www.gloogle.run") +} diff --git a/apps/backend/src/backend/workers.gleam b/apps/backend/src/backend/workers.gleam new file mode 100644 index 0000000..439a4f4 --- /dev/null +++ b/apps/backend/src/backend/workers.gleam @@ -0,0 +1,35 @@ +import gleam/function +import gleam/otp/supervisor +import processes/periodic +import tasks/hex +import tasks/popularity +import tasks/ranking +import tasks/timeseries + +pub fn sync_new_gleam_releases_ten_secondly(ctx, children) { + use <- add_periodic_worker(children, waiting: 10_000) + hex.sync_new_gleam_releases(ctx) +} + +pub fn compute_ranking_daily(ctx, children) { + use <- add_periodic_worker(children, waiting: 86_400_000) + ranking.compute_ranking(ctx) +} + +pub fn compute_popularity_daily(ctx, children) { + use <- add_periodic_worker(children, waiting: 86_400_000) + popularity.compute_popularity(ctx) +} + +pub fn store_timeseries_hourly(ctx, children) { + use <- add_periodic_worker(children, waiting: 3_600_000) + timeseries.store_timeseries(ctx) +} + +fn add_periodic_worker(children, waiting delay, do work) { + use _ <- function.tap(children) + supervisor.add(children, { + use _ <- supervisor.worker() + periodic.periodically(do: work, waiting: delay) + }) +} diff --git a/apps/backend/src/gloogle_hex_ffi.erl b/apps/backend/src/backend_ffi.erl similarity index 76% rename from apps/backend/src/gloogle_hex_ffi.erl rename to apps/backend/src/backend_ffi.erl index aed54c9..6c46943 100644 --- a/apps/backend/src/gloogle_hex_ffi.erl +++ b/apps/backend/src/backend_ffi.erl @@ -1,5 +1,6 @@ --module(gloogle_hex_ffi). --export([extract_tar/4, remove_tar/1, is_match/2, get_home/0, set_level/1]). +-module(backend_ffi). + +-export([extract_tar/4, remove_tar/1, is_match/2, get_home/0, set_level/1, coerce/1]). package_interface_path(ContentDest, BaseName) -> BuildFolder = <<"/build/dev/docs/">>, @@ -20,14 +21,22 @@ extract_tar(Binary, BaseName, Version, Slug) -> ContentDest = <>, Content = <>, case erl_tar:extract({binary, Binary}, [{cwd, PackagePath}]) of - {error, _} -> {error, nil}; + {error, _} -> + {error, nil}; _ -> - Url = <<"https://hexdocs.pm/", BaseName/binary, "/", Version/binary, "/package-interface.json">>, + Url = + <<"https://hexdocs.pm/", + BaseName/binary, + "/", + Version/binary, + "/package-interface.json">>, erl_tar:extract(Content, [{cwd, ContentDest}, compressed]), {PackageInterface, Result} = case httpc:request(Url) of - {ok, {{_, 200, _}, _, HttpBody}} -> save_file(ContentDest, HttpBody); - {ok, {200, HttpBody}} -> save_file(ContentDest, HttpBody); + {ok, {{_, 200, _}, _, HttpBody}} -> + save_file(ContentDest, HttpBody); + {ok, {200, HttpBody}} -> + save_file(ContentDest, HttpBody); {error, _} -> BuildCmd = <<"cd ", ContentDest/binary, " && gleam docs build">>, Res = os:cmd(binary_to_list(BuildCmd)), @@ -45,15 +54,22 @@ remove_tar(Slug) -> is_match(Version, Requirement) -> case verl:is_match(Version, Requirement) of - {error, _} -> {error, nil}; - Bool -> {ok, Bool} + {error, _} -> + {error, nil}; + Bool -> + {ok, Bool} end. get_home() -> case init:get_argument(home) of - {ok, Content} -> {ok, unicode:characters_to_binary(Content)}; - error -> {error, nil} + {ok, Content} -> + {ok, unicode:characters_to_binary(Content)}; + error -> + {error, nil} end. set_level(Level) -> logger:set_primary_config(level, Level). + +coerce(A) -> + A. diff --git a/apps/backend/src/gleam/verl.gleam b/apps/backend/src/gleam/verl.gleam index ca41040..250edda 100644 --- a/apps/backend/src/gleam/verl.gleam +++ b/apps/backend/src/gleam/verl.gleam @@ -1,4 +1,4 @@ -@external(erlang, "gloogle_hex_ffi", "is_match") +@external(erlang, "backend_ffi", "is_match") pub fn is_match( version version: BitArray, requirement requirement: BitArray, diff --git a/apps/backend/src/helpers.gleam b/apps/backend/src/helpers.gleam index ed4b920..a790c60 100644 --- a/apps/backend/src/helpers.gleam +++ b/apps/backend/src/helpers.gleam @@ -5,14 +5,16 @@ import gleam/float import gleam/json import gleam/list import gleam/pair -import gleam/pgo import gleam/result +import pog -pub fn convert_time(time: Time) -> pgo.Value { +@external(erlang, "backend_ffi", "coerce") +fn coerce(a: a) -> b + +pub fn convert_time(time: Time) -> pog.Value { time - |> birl.to_erlang_universal_datetime() - |> dynamic.from() - |> dynamic.unsafe_coerce() + |> birl.to_erlang_universal_datetime + |> coerce } pub fn decode_time(data: Dynamic) { diff --git a/apps/backend/src/periodic.gleam b/apps/backend/src/processes/periodic.gleam similarity index 100% rename from apps/backend/src/periodic.gleam rename to apps/backend/src/processes/periodic.gleam diff --git a/apps/backend/src/processes/retrier.gleam b/apps/backend/src/processes/retrier.gleam new file mode 100644 index 0000000..2ee1c0f --- /dev/null +++ b/apps/backend/src/processes/retrier.gleam @@ -0,0 +1,75 @@ +import backend/error.{type Error} +import gleam/bool +import gleam/erlang/process.{type Subject} +import gleam/function +import gleam/iterator.{type Iterator} +import gleam/otp/actor +import prng/random +import wisp + +pub opaque type Message { + Rerun +} + +type State(a) { + State( + self: Subject(Message), + work: fn(Int) -> Result(a, Error), + random_ints: Iterator(Int), + interval: Int, + iterations: Int, + ) +} + +pub const one_minute: Int = 60_000 + +fn enqueue_next_rerun(state: State(a)) { + let assert iterator.Next(cooldown, acc) = iterator.step(state.random_ints) + process.send_after(state.self, state.interval + cooldown, Rerun) + State(..state, random_ints: acc) +} + +/// Repeatedly call a function, leaving `interval` milliseconds between each call. +/// When the `work` function returns an error, it is printed. +pub fn retry(do work: fn(Int) -> Result(a, Error)) { + let _ = start_retrier(work) + Nil +} + +fn start_retrier(work: fn(Int) -> Result(a, Error)) { + fn() { init(every: one_minute, do: work) } + |> actor.Spec(loop:, init_timeout: 100) + |> actor.start_spec +} + +fn init( + every interval: Int, + do work: fn(Int) -> Result(a, Error), +) -> actor.InitResult(State(a), Message) { + let self = process.new_subject() + let random_ints = random.to_random_iterator(random.int(1000, 5000)) + let state = State(self:, work:, interval:, iterations: 10, random_ints:) + process.new_selector() + |> process.selecting(self, function.identity) + |> actor.Ready(state, _) + |> function.tap(fn(_) { process.send(state.self, Rerun) }) +} + +fn loop(message: Message, state: State(a)) -> actor.Next(Message, State(a)) { + case message, state.work(state.iterations) { + Rerun, Ok(_) -> actor.Stop(process.Normal) + Rerun, Error(error) -> { + wisp.log_notice("Process on error") + error.log_error(error) + use <- bool.lazy_guard(when: state.iterations == 0, return: stop_process) + State(..state, iterations: state.iterations - 1) + |> enqueue_next_rerun() + |> actor.continue() + } + } +} + +fn stop_process() { + wisp.log_notice("Stopping process after 10 iterations") + actor.Stop(process.Normal) +} diff --git a/apps/backend/src/retrier.gleam b/apps/backend/src/retrier.gleam deleted file mode 100644 index 1453bd6..0000000 --- a/apps/backend/src/retrier.gleam +++ /dev/null @@ -1,84 +0,0 @@ -import backend/error.{type Error} -import gleam/erlang/process.{type Subject} -import gleam/function -import gleam/iterator.{type Iterator} -import gleam/otp/actor -import prng/random -import wisp - -pub opaque type Message { - Rerun -} - -type State(a) { - State( - self: Subject(Message), - work: fn(Int) -> Result(a, Error), - random_ints: Iterator(Int), - interval: Int, - iterations: Int, - ) -} - -pub const one_minute: Int = 60_000 - -fn enqueue_next_rerun(state: State(a)) { - let assert iterator.Next(cooldown, acc) = iterator.step(state.random_ints) - process.send_after(state.self, state.interval + cooldown, Rerun) - State(..state, random_ints: acc) -} - -/// Repeatedly call a function, leaving `interval` milliseconds between each call. -/// When the `work` function returns an error it is printed. -pub fn retry( - do work: fn(Int) -> Result(a, Error), -) -> Result(Subject(Message), actor.StartError) { - fn() { init(one_minute, work) } - |> actor.Spec(loop: loop, init_timeout: 100) - |> actor.start_spec() -} - -fn init( - interval: Int, - work: fn(Int) -> Result(a, Error), -) -> actor.InitResult(State(a), Message) { - let subject = process.new_subject() - let random_ints = random.to_random_iterator(random.int(1000, 5000)) - let state = - State( - self: subject, - work: work, - interval: interval, - iterations: 10, - random_ints: random_ints, - ) - process.new_selector() - |> process.selecting(subject, function.identity) - |> actor.Ready(state, _) - |> function.tap(fn(_) { process.send(state.self, Rerun) }) -} - -fn loop(message: Message, state: State(a)) -> actor.Next(Message, State(a)) { - case message { - Rerun -> { - case state.work(state.iterations) { - Ok(_) -> actor.Stop(process.Normal) - Error(error) -> { - wisp.log_notice("Process on error") - error.log_error(error) - case state.iterations == 0 { - True -> { - wisp.log_notice("Stopping process after 10 iterations") - actor.Stop(process.Normal) - } - False -> { - State(..state, iterations: state.iterations - 1) - |> enqueue_next_rerun() - |> actor.continue() - } - } - } - } - } - } -} diff --git a/apps/backend/src/tasks/hex.gleam b/apps/backend/src/tasks/hex.gleam index 94ae811..5990189 100644 --- a/apps/backend/src/tasks/hex.gleam +++ b/apps/backend/src/tasks/hex.gleam @@ -1,14 +1,15 @@ import api/hex as api import api/hex_repo import api/signatures -import backend/config.{type Context} +import backend/context.{type Context} import backend/data/hex_read.{type HexRead} import backend/error.{type Error} -import backend/gleam/context +import backend/gleam/context as gcontext import backend/gleam/type_search/msg as type_search import backend/postgres/queries import birl.{type Time} import birl/duration +import gleam/bool import gleam/erlang/process.{type Subject} import gleam/function import gleam/hexpm.{type Package} @@ -16,11 +17,10 @@ import gleam/int import gleam/list import gleam/option.{type Option, None, Some} import gleam/order -import gleam/otp/supervisor -import gleam/pgo import gleam/result import gleam/string -import retrier +import pog +import processes/retrier import wisp type State { @@ -30,29 +30,30 @@ type State { newest: Time, hex_api_key: String, last_logged: Time, - db: pgo.Connection, + db: pog.Connection, type_search_subject: Option(Subject(type_search.Msg)), ) } -pub fn sync_new_gleam_releases( - ctx: Context, - children: supervisor.Children(Nil), -) -> Result(HexRead, Error) { +type WorkMode { + WorkAsync + WorkSync +} + +pub fn sync_new_gleam_releases(ctx: Context) -> Result(HexRead, Error) { wisp.log_info("Syncing new releases from Hex") use limit <- result.try(queries.get_last_hex_date(ctx.db)) - use latest <- result.try(sync_packages( - State( + use latest <- result.try({ + sync_packages(State( page: 1, - limit: limit, + limit:, newest: limit, hex_api_key: ctx.hex_api_key, last_logged: birl.now(), db: ctx.db, type_search_subject: ctx.type_search_subject, - ), - children, - )) + )) + }) let latest = queries.upsert_most_recent_hex_timestamp(ctx.db, latest) wisp.log_info("") wisp.log_info("Up to date!") @@ -73,10 +74,7 @@ fn first_timestamp(packages: List(hexpm.Package), state: State) -> Time { |> result.unwrap(state.newest) } -fn sync_packages( - state: State, - children: supervisor.Children(Nil), -) -> Result(Time, Error) { +fn sync_packages(state: State) -> Result(Time, Error) { let page = state.page let api_key = state.hex_api_key use all_packages <- result.try(api.get_api_packages_page(page, api_key)) @@ -84,56 +82,52 @@ fn sync_packages( let new_packages = take_fresh_packages(all_packages, state.limit) use state <- result.try({ list.try_fold(new_packages, state, { - do_sync_package(Some(children), force: False) + do_sync_package(WorkAsync, force: False) }) }) case list.length(all_packages) == list.length(new_packages) { _ if all_packages == [] -> Ok(state.newest) False -> Ok(state.newest) - True -> sync_packages(State(..state, page: state.page + 1), children) + True -> sync_packages(State(..state, page: state.page + 1)) } } +pub fn sync_package(ctx: Context, package: hexpm.Package) { + State( + page: -1, + limit: birl.now(), + newest: birl.now(), + hex_api_key: ctx.hex_api_key, + last_logged: birl.now(), + db: ctx.db, + type_search_subject: ctx.type_search_subject, + ) + |> do_sync_package(WorkSync, force: True)(package) + |> result.replace_error(error.EmptyError) + |> result.replace(Nil) +} + fn do_sync_package( - children: Option(supervisor.Children(Nil)), + work_mode work_mode: WorkMode, force force_old_release_update: Bool, ) { fn(state: State, package: hexpm.Package) -> Result(State, Error) { let secret = state.hex_api_key - use releases <- result.try(lookup_gleam_releases(package, secret: secret)) - case releases { - [] -> Ok(log_if_needed(state, package.updated_at)) - _ -> { - use _ <- result.map(insert_package_and_releases( - package, - releases, - state, - children, - force_old_release_update, - )) - State(..state, last_logged: birl.now()) - } - } + use releases <- result.try(lookup_gleam_releases(package, secret:)) + use <- bool.lazy_guard(when: list.is_empty(releases), return: fn() { + Ok(log_if_needed(state, package.updated_at)) + }) + use _ <- result.map(insert_package_and_releases( + package, + releases, + state, + work_mode, + force_old_release_update, + )) + State(..state, last_logged: birl.now()) } } -pub fn sync_package(ctx: Context, package: hexpm.Package) { - do_sync_package(None, force: True)( - State( - page: -1, - limit: birl.now(), - newest: birl.now(), - hex_api_key: ctx.hex_api_key, - last_logged: birl.now(), - db: ctx.db, - type_search_subject: ctx.type_search_subject, - ), - package, - ) - |> result.replace_error(error.EmptyError) - |> result.replace(Nil) -} - fn log_retirement_data(release: String, retirement: hexpm.ReleaseRetirement) { wisp.log_info("Release " <> release <> " is retired.") case retirement.message { @@ -169,9 +163,8 @@ fn extract_release_interfaces_from_hex( use data <- result.map({ hex_repo.get_package_infos(package.name, release.version) }) - let interface = Some(data.2) - let toml = Some(data.3) - let _ = queries.upsert_release(state.db, id, release, interface, toml) + let _ = + queries.upsert_release(state.db, id, release, Some(data.2), Some(data.3)) #(data.0, data.1) } @@ -204,8 +197,7 @@ fn save_retirement_data( option.Some(retirement) -> { let release = package.name <> " v" <> release.version log_retirement_data(release, retirement) - let _ = - queries.add_package_gleam_retirement(state.db, retirement, release_id) + let _ = queries.add_package_retirement(state.db, retirement, release_id) Nil } } @@ -215,7 +207,7 @@ fn insert_package_and_releases( package: hexpm.Package, releases: List(hexpm.Release), state: State, - children: Option(supervisor.Children(Nil)), + work_async: WorkMode, force_old_release_update: Bool, ) { let secret = state.hex_api_key @@ -246,23 +238,18 @@ fn insert_package_and_releases( wisp.log_debug("Handling release " <> r.version) use interfaces <- result.map(extract_release_interfaces_from_db(state, id, r)) save_retirement_data(state, interfaces.0, package, r) - let _ = case children { - None -> { + case work_async { + WorkSync -> { let _ = do_extract_package(state, id, r, package, interfaces, False) Nil } - Some(children) -> { - supervisor.add(children, { - use _ <- supervisor.worker() - use iterations <- retrier.retry() - let it = int.to_string(iterations) - wisp.log_notice("Trying iteration " <> it <> " for " <> release) - do_extract_package(state, id, r, package, interfaces, iterations == 0) - }) - Nil + WorkAsync -> { + use iterations <- retrier.retry + let it = int.to_string(iterations) + wisp.log_notice("Trying iteration " <> it <> " for " <> release) + do_extract_package(state, id, r, package, interfaces, iterations == 0) } } - Nil } fn do_extract_package( @@ -271,17 +258,17 @@ fn do_extract_package( release: hexpm.Release, package: hexpm.Package, interfaces: #(Int, Option(String), Option(String)), - ignore_errors: Bool, + ignore_parameters_errors: Bool, ) { - use #(package, gleam_toml) <- result.try({ + use #(package_interface, gleam_toml) <- result.try({ extract_release_interfaces(state, id, package, release, interfaces) }) - context.Context( - state.db, - package, - gleam_toml, - ignore_errors, - state.type_search_subject, + gcontext.Context( + db: state.db, + package_interface:, + gleam_toml:, + ignore_parameters_errors:, + type_search_subject: state.type_search_subject, ) |> signatures.extract_signatures() |> result.map(fn(content) { @@ -295,22 +282,21 @@ fn lookup_gleam_releases( package: hexpm.Package, secret hex_api_key: String, ) -> Result(List(hexpm.Release), Error) { - let lookup = - list.try_map(package.releases, api.lookup_release(_, hex_api_key)) - use releases <- result.map(lookup) - list.filter(releases, fn(r) { list.contains(r.meta.build_tools, "gleam") }) + package.releases + |> list.try_map(api.lookup_release(_, hex_api_key)) + |> result.map(fn(releases) { + use release <- list.filter(releases) + list.contains(release.meta.build_tools, "gleam") + }) } fn log_if_needed(state: State, time: Time) -> State { let interval = duration.new([#(5, duration.Second)]) let print_deadline = birl.add(state.last_logged, interval) - case birl.compare(print_deadline, birl.now()) == order.Lt { - False -> state - True -> { - wisp.log_info("Still syncing, up to " <> birl.to_iso8601(time)) - State(..state, last_logged: birl.now()) - } - } + let should_log = birl.compare(print_deadline, birl.now()) == order.Lt + use <- bool.guard(when: !should_log, return: state) + wisp.log_info("Still syncing, up to " <> birl.to_iso8601(time)) + State(..state, last_logged: birl.now()) } pub fn take_fresh_packages(packages: List(Package), limit: Time) { diff --git a/apps/backend/src/tasks/popularity.gleam b/apps/backend/src/tasks/popularity.gleam index 86fdd49..c2d3bb1 100644 --- a/apps/backend/src/tasks/popularity.gleam +++ b/apps/backend/src/tasks/popularity.gleam @@ -1,5 +1,5 @@ import api/github -import backend/config.{type Context} +import backend/context.{type Context} import backend/postgres/queries import gleam/bool import gleam/dict @@ -11,8 +11,8 @@ import wisp pub fn compute_popularity(ctx: Context) { case ctx.env { - config.Development -> Ok(Nil) - config.Production -> { + context.Development -> Ok(Nil) + context.Production -> { wisp.log_info("Syncing popularity") do_compute_popularity(ctx, offset: 0) |> function.tap(fn(_) { wisp.log_info("Syncing popularity finished!") }) diff --git a/apps/backend/src/tasks/ranking.gleam b/apps/backend/src/tasks/ranking.gleam index a06f06e..22fbe40 100644 --- a/apps/backend/src/tasks/ranking.gleam +++ b/apps/backend/src/tasks/ranking.gleam @@ -1,4 +1,4 @@ -import backend/config.{type Context} +import backend/context.{type Context} import backend/error.{type Error} import backend/postgres/queries import gleam/dict.{type Dict} @@ -52,7 +52,7 @@ fn get_dependencies(toml: Dict(String, tom.Toml)) { fn add_dependencies(in usages: Usages, from toml: Dict(String, tom.Toml)) { use usages, dep <- list.fold(from: usages, over: get_dependencies(toml)) - dict.update(usages, dep, fn(value) { option.unwrap(value, 0) + 1 }) + dict.upsert(usages, dep, fn(value) { option.unwrap(value, 0) + 1 }) } fn save_packages_rank(ctx: Context, usages: Usages) { diff --git a/apps/backend/src/tasks/timeseries.gleam b/apps/backend/src/tasks/timeseries.gleam index 252bed4..aaf2c5f 100644 --- a/apps/backend/src/tasks/timeseries.gleam +++ b/apps/backend/src/tasks/timeseries.gleam @@ -1,4 +1,4 @@ -import backend/config.{type Context} +import backend/context.{type Context} import backend/error import backend/postgres/queries import gleam/function @@ -10,12 +10,9 @@ pub fn store_timeseries(ctx: Context) { wisp.log_info("Storing analytics timeseries") let query = queries.select_last_day_search_analytics(ctx.db) use analytics <- result.try(query) - use _ <- function.tap({ - result.all({ - use analytic <- list.map(analytics) - queries.upsert_search_analytics_timeseries(ctx.db, analytic) - }) - |> result.map_error(error.debug_log) - }) - wisp.log_info("Storing analytics finished!") + analytics + |> list.map(queries.upsert_search_analytics_timeseries(ctx.db, _)) + |> result.all + |> result.map_error(error.debug_log) + |> function.tap(fn(_) { wisp.log_info("Storing analytics finished!") }) } diff --git a/apps/backend/src/wisp/logger.gleam b/apps/backend/src/wisp/logger.gleam index c9325a1..3208016 100644 --- a/apps/backend/src/wisp/logger.gleam +++ b/apps/backend/src/wisp/logger.gleam @@ -1,4 +1,6 @@ +import envoy import gleam +import gleam/result import gleam/string pub type Level { @@ -12,10 +14,16 @@ pub type Level { Debug } -@external(erlang, "gloogle_hex_ffi", "set_level") +@external(erlang, "backend_ffi", "set_level") pub fn set_level(level: Level) -> Nil -pub fn parse(level: String) -> Result(Level, Nil) { +pub fn read_level() { + envoy.get("LOG_LEVEL") + |> result.try(parse) + |> result.unwrap(Info) +} + +fn parse(level: String) -> Result(Level, Nil) { case string.lowercase(level) { "emergency" -> Ok(Emergency) "alert" -> Ok(Alert) diff --git a/apps/backend/test/backend_test.gleam b/apps/backend/test/backend_test.gleam index 4ad1a9e..4f2f121 100644 --- a/apps/backend/test/backend_test.gleam +++ b/apps/backend/test/backend_test.gleam @@ -1,13 +1,13 @@ import backend/gleam/parse import backend/gleam/type_search -import gleam/erlang/os +import envoy import gleam/function import gleam/int import gleam/option -import gleam/pgo import gleam/result import gleeunit import gleeunit/should +import pog const signature = "fn use_callback(a, b) -> c" @@ -18,24 +18,30 @@ pub fn main() { } fn postgres_connect() { - let host = os.get_env("POSTGRES_HOST") |> result.unwrap("localhost") - pgo.Config( - ..pgo.default_config(), + let host = envoy.get("POSTGRES_HOST") |> result.unwrap("localhost") + pog.Config( + ..pog.default_config(), host: host, database: "gloogle", user: "gloogle", password: option.Some("gloogle"), ssl: False, ) - |> pgo.connect + |> pog.connect } // gleeunit test functions end in `_test` pub fn type_search_test() { let db = postgres_connect() let index = int.random(1000) - use kind <- result.try(parse.parse_function(signature) |> result.nil_error) - use skind <- result.try(parse.parse_function(search_test) |> result.nil_error) + use kind <- result.try({ + parse.parse_function(signature) + |> result.replace_error(Nil) + }) + use skind <- result.try({ + parse.parse_function(search_test) + |> result.replace_error(Nil) + }) let search = type_search.add(type_search.empty(), kind, index) type_search.find(search, skind, db) |> function.tap(should.equal(_, Ok([index]))) diff --git a/apps/frontend/gleam.toml b/apps/frontend/gleam.toml index 0e45249..67e8da5 100644 --- a/apps/frontend/gleam.toml +++ b/apps/frontend/gleam.toml @@ -9,16 +9,21 @@ version = "1.0.0" typescript_declarations = true [dependencies] -gleam_javascript = "~> 0.8" -gleam_json = ">= 1.0.1 and < 2.0.0" +birl = ">= 1.7.1 and < 2.0.0" +bright = {path = "../../packages/bright"} +gleam_fetch = ">= 0.4.0 and < 1.0.0" +gleam_http = ">= 3.7.1 and < 4.0.0" +gleam_javascript = ">= 0.13.0 and < 1.0.0" +gleam_json = ">= 2.1.0 and < 3.0.0" +gleam_regexp = ">= 1.0.0 and < 2.0.0" gleam_stdlib = "~> 0.34 or ~> 1.0" -lustre_http = "~> 0.5" -plinth = ">= 0.2.0 and < 1.0.0" -sketch = ">= 2.2.2 and < 3.0.0" -lustre = { path = "../../packages/lustre" } -vitools = { path = "../../packages/vitools" } +grille_pain = ">= 1.1.0 and < 2.0.0" +interfaces = {path = "../../packages/interfaces"} +lustre = ">= 4.6.1 and < 5.0.0" modem = ">= 2.0.0 and < 3.0.0" -birl = ">= 1.7.1 and < 2.0.0" +sketch = ">= 3.0.0 and < 4.0.0" +sketch_magic = {path = "../../packages/sketch_magic"} +vitools = {path = "../../packages/vitools"} [dev-dependencies] gleeunit = "~> 1.0" diff --git a/apps/frontend/index.html b/apps/frontend/index.html index 076e761..e13e627 100644 --- a/apps/frontend/index.html +++ b/apps/frontend/index.html @@ -56,6 +56,6 @@
- + diff --git a/apps/frontend/manifest.toml b/apps/frontend/manifest.toml index bf2483b..495dcfb 100644 --- a/apps/frontend/manifest.toml +++ b/apps/frontend/manifest.toml @@ -2,35 +2,73 @@ # You typically do not need to edit this file packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "bright", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre", "lustre_dev_tools"], source = "local", path = "../../packages/bright" }, { name = "conversation", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "conversation", source = "hex", outer_checksum = "908B46F60444442785A495197D482558AD8B849C3714A38FAA1940358CC8CCCD" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, + { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, + { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, + { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, + { name = "gleam_community_ansi", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "4CD513FC62523053E62ED7BAC2F36136EC17D6A8942728250A9A00A15E340E4B" }, + { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, + { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, + { name = "gleam_erlang", version = "0.32.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "B18643083A0117AC5CFD0C1AEEBE5469071895ECFA426DCC26517A07F6AD9948" }, { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_javascript", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "483631D3001FCE8EB12ADEAD5E1B808440038E96F93DA7A32D326C82F480C0B2" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" }, + { name = "gleam_http", version = "3.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "A9EE0722106FCCAB8AD3BF9D0A3EFF92BFE8561D59B83BAE96EB0BE1938D4E0F" }, + { name = "gleam_httpc", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "76FEEC99473E568EBA34336A37CF3D54629ACE77712950DC9BB097B5FD664664" }, + { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" }, + { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, + { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, + { name = "gleam_package_interface", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "CF3BFC5D0997750D9550D8D73A90F4B8D71C6C081B20ED4E70FFBE1E99AFC3C2" }, + { name = "gleam_regexp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "A3655FDD288571E90EE9C4009B719FEF59FA16AFCDF3952A76A125AF23CF1592" }, + { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, + { name = "glearray", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "B99767A9BC63EF9CC8809F66C7276042E5EFEACAA5B25188B552D3691B91AC6D" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, - { name = "lustre", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], source = "local", path = "../../packages/lustre" }, - { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" }, - { name = "modem", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "FF07984B15D553B15A45722F64BC440CA22B6139930E95E16EB956F1E24886AD" }, - { name = "plinth", version = "0.4.9", build_tools = ["gleam"], requirements = ["conversation", "gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "663C788C00FF908662B47B78B1CEBE1260AB814B45531AA42EBAEE974CDC7E27" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "sketch", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "lustre", "plinth"], otp_app = "sketch", source = "hex", outer_checksum = "C4BDDB207A3CC94AC83BFB4D1300C51A9D0048569213ED4B35C9D982B305AF9D" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, + { name = "glint", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5F6720081150AED8023131B0F3A35F9B0D6426A96CE02BEC52AD7018DF70566A" }, + { name = "glisten", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "912132751031473CB38F454120124FFC96AF6B0EA33D92C9C90DB16327A2A972" }, + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, + { name = "grille_pain", version = "1.1.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib", "lustre", "plinth", "sketch", "sketch_lustre"], otp_app = "grille_pain", source = "hex", outer_checksum = "718CB2468EF77EDECE148A98948EF8CC376D3CB96ACCE4983BC6C2F43A254D45" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "interfaces", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_json", "gleam_stdlib"], source = "local", path = "../../packages/interfaces" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "lustre", version = "4.6.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "BDF833368F6C8F152F948D5B6A79866E9881CB80CB66C0685B3327E7DCBFB12F" }, + { name = "lustre_dev_tools", version = "1.6.2", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_regexp", "gleam_stdlib", "glint", "glisten", "lustre", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "A0CBC323AA7E03EC91D785CEB644776082D76BE46F1624FB920BB92BD79853F7" }, + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, + { name = "mist", version = "3.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "CDA1A74E768419235E16886463EC4722EFF4AB3F8D820A76EAD45D7C167D7282" }, + { name = "modem", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "B446119A85DC9C31FEA0DCB204072E40356CB246C9D091CBBF2E4678CDB05A2D" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, + { name = "plinth", version = "0.5.4", build_tools = ["gleam"], requirements = ["conversation", "gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "C2BC6A3D1F9741014D7FD18625A9258DF60CF3132ED299C63BB88F15FFDCEE53" }, + { name = "ranger", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "B8F3AFF23A3A5B5D9526B8D18E7C43A7DFD3902B151B97EC65397FE29192B695" }, + { name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" }, + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, + { name = "sketch", version = "3.1.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "sketch", source = "hex", outer_checksum = "85E41BC0BE2E5D8985EF34F0D2FD7315329A463FC845570EC5D399F20F8AFA08" }, + { name = "sketch_lustre", version = "1.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre", "plinth", "sketch"], otp_app = "sketch_lustre", source = "hex", outer_checksum = "DD5437B10D4BB8AB45A19820B17883188B8568B6ED7885D7D073A983F4984E79" }, + { name = "sketch_magic", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre", "sketch"], source = "local", path = "../../packages/sketch_magic" }, + { name = "snag", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "08E9EB87C413457DB1DD66CD704C6878DACC9C93B418600F63873D0CD224E756" }, + { name = "spinner", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "9EE43AA33BE2DA5731B8F3F170AAB59AF1A815AFA5BF615F12C1B91F3B03F157" }, + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, + { name = "tom", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "228E667239504B57AD05EC3C332C930391592F6C974D0EFECF32FFD0F3629A27" }, { name = "vitools", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../../packages/vitools" }, + { name = "wisp", version = "1.3.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "D12384EE63ADEE833B40A6D26EF8014A6E4BFC10EC2CDC8B57D325DD4B52740E" }, ] [requirements] birl = { version = ">= 1.7.1 and < 2.0.0" } -gleam_javascript = { version = "~> 0.8" } -gleam_json = { version = ">= 1.0.1 and < 2.0.0" } +bright = { path = "../../packages/bright" } +gleam_fetch = { version = ">= 0.4.0 and < 1.0.0" } +gleam_http = { version = ">= 3.7.1 and < 4.0.0" } +gleam_javascript = { version = ">= 0.13.0 and < 1.0.0" } +gleam_json = { version = ">= 2.1.0 and < 3.0.0" } +gleam_regexp = { version = ">= 1.0.0 and < 2.0.0" } gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } gleeunit = { version = "~> 1.0" } -lustre = { path = "../../packages/lustre" } -lustre_http = { version = "~> 0.5" } +grille_pain = { version = ">= 1.1.0 and < 2.0.0" } +interfaces = { path = "../../packages/interfaces" } +lustre = { version = ">= 4.6.1 and < 5.0.0" } modem = { version = ">= 2.0.0 and < 3.0.0" } -plinth = { version = ">= 0.2.0 and < 1.0.0" } -sketch = { version = ">= 2.2.2 and < 3.0.0" } +sketch = { version = ">= 3.0.0 and < 4.0.0" } +sketch_magic = { path = "../../packages/sketch_magic" } vitools = { path = "../../packages/vitools" } diff --git a/apps/frontend/package.json b/apps/frontend/package.json index e9c6a5f..d63e2b0 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -9,7 +9,6 @@ "frontend:preview": "vite preview" }, "dependencies": { - "@chouqueth/gleam": "^1.4.1", "@gleam-lang/highlight.js-gleam": "^1.5.0", "@sentry/browser": "^8.0.0", "chart.js": "^4.4.4", @@ -19,13 +18,10 @@ "marked-highlight": "^2.1.1" }, "devDependencies": { + "@chouqueth/gleam": "^1.6.2", "@sentry/vite-plugin": "^2.16.1", "@trivago/prettier-plugin-sort-imports": "^4.3.0", - "@types/dompurify": "^3.0.5", - "dotenv": "^16.4.5", "prettier": "^3.2.5", - "ts-gleam": "^1.0.1", - "typescript": "^5.4.2", "vite": "^5.1.6", "vite-gleam": "^0.4.0" } diff --git a/apps/frontend/src/chart.gleam b/apps/frontend/src/chart.gleam index 182c76f..d2ca603 100644 --- a/apps/frontend/src/chart.gleam +++ b/apps/frontend/src/chart.gleam @@ -7,19 +7,13 @@ pub type Dataset { pub fn line_chart(datasets: Dataset) { let datasets = attribute.property("datasets", datasets) - element.element( - "line-chart", - [attribute.style([#("display", "block")]), datasets], - [], - ) + let attributes = [attribute.style([#("display", "block")]), datasets] + element.element("line-chart", attributes, []) } pub fn bar_chart(color: String, datasets: Dataset) { let datasets = attribute.property("datasets", datasets) let color = attribute.property("color", color) - element.element( - "bar-chart", - [attribute.style([#("display", "block")]), datasets, color], - [], - ) + let attributes = [attribute.style([#("display", "block")]), datasets, color] + element.element("bar-chart", attributes, []) } diff --git a/apps/frontend/src/data/metadata.gleam b/apps/frontend/src/data/metadata.gleam deleted file mode 100644 index ee2bc91..0000000 --- a/apps/frontend/src/data/metadata.gleam +++ /dev/null @@ -1,27 +0,0 @@ -import data/implementations.{type Implementations, Implementations} -import decoder_extra -import gleam/dynamic -import gleam/option.{type Option} - -pub type Metadata { - Metadata( - deprecation: Option(String), - implementations: Option(Implementations), - ) -} - -pub fn decode_metadata(dyn) { - dynamic.decode2( - Metadata, - decoder_extra.completely_option("deprecation"), - dynamic.optional_field( - "implementations", - dynamic.decode3( - Implementations, - dynamic.field("gleam", dynamic.bool), - dynamic.field("uses_erlang_externals", dynamic.bool), - dynamic.field("uses_javascript_externals", dynamic.bool), - ), - ), - )(dyn) -} diff --git a/apps/frontend/src/data/model.gleam b/apps/frontend/src/data/model.gleam index 4b7e742..13faa2f 100644 --- a/apps/frontend/src/data/model.gleam +++ b/apps/frontend/src/data/model.gleam @@ -1,17 +1,19 @@ import birl +import bright +import data/analytics import data/kind import data/msg.{type Msg} import data/package.{type Package} -import data/search_result.{type SearchResult, type SearchResults} +import data/search_result.{type SearchResults, SearchResults} +import data/type_search.{type TypeSearch} import frontend/router import frontend/view/body/cache import gleam/dict.{type Dict} -import gleam/function import gleam/int import gleam/list import gleam/option.{type Option} import gleam/pair -import gleam/regex +import gleam/regexp import gleam/result import gleam/string import lustre/element.{type Element} @@ -19,8 +21,11 @@ import lustre/element.{type Element} pub type Index = List(#(#(String, String), List(#(String, String)))) -pub type Model { - Model( +pub type Model = + bright.Bright(Data, Computed) + +pub type Data { + Data( input: String, search_results: Dict(String, SearchResults), index: Index, @@ -42,18 +47,22 @@ pub type Model { total_signatures: Int, total_packages: Int, timeseries: List(#(Int, birl.Time)), - ranked: List(msg.Package), - popular: List(msg.Package), + ranked: List(analytics.Package), + popular: List(analytics.Package), ) } +pub type Computed { + Computed +} + @external(javascript, "../gloogle.ffi.mjs", "isMobile") fn is_mobile() -> Bool -pub fn init() { +pub fn init_data() { let search_results = search_result.Start let index = compute_index(search_results) - Model( + Data( input: "", search_results: dict.new(), index: index, @@ -80,36 +89,36 @@ pub fn init() { ) } -pub fn update_route(model: Model, route: router.Route) { - Model(..model, route: route) +pub fn update_route(model: Data, route: router.Route) { + Data(..model, route: route) } -pub fn update_submitted_input(model: Model) { - Model(..model, submitted_input: model.input) +pub fn update_submitted_input(model: Data) { + Data(..model, submitted_input: model.input) } -pub fn update_is_mobile(model: Model, is_mobile: Bool) { - Model(..model, is_mobile: is_mobile) +pub fn update_is_mobile(model: Data, is_mobile: Bool) { + Data(..model, is_mobile: is_mobile) } -pub fn update_trendings(model: Model, trendings: List(Package)) { +pub fn update_trendings(model: Data, trendings: List(Package)) { model.trendings |> option.unwrap([]) |> list.append(trendings) |> option.Some - |> fn(t) { Model(..model, trendings: t) } + |> fn(t) { Data(..model, trendings: t) } } -pub fn toggle_loading(model: Model) { - Model(..model, loading: !model.loading) +pub fn toggle_loading(model: Data) { + Data(..model, loading: !model.loading) } -pub fn update_input(model: Model, content: String) { - Model(..model, input: content) +pub fn update_input(model: Data, content: String) { + Data(..model, input: content) } -pub fn update_analytics(model: Model, analytics: msg.Analytics) { - Model( +pub fn update_analytics(model: Data, analytics: analytics.Analytics) { + Data( ..model, timeseries: analytics.timeseries, total_searches: analytics.total_searches, @@ -120,7 +129,7 @@ pub fn update_analytics(model: Model, analytics: msg.Analytics) { ) } -pub fn search_key(key key: String, model model: Model) { +pub fn search_key(key key: String, model model: Data) { key <> string.inspect([ model.keep_functions, @@ -138,7 +147,7 @@ fn default_search_key(key key: String) { } pub fn update_search_results( - model: Model, + model: Data, key: String, search_results: SearchResults, ) { @@ -146,7 +155,7 @@ pub fn update_search_results( let index = compute_index(search_results) let view_cache = case search_results { search_result.Start | search_result.InternalServerError -> model.view_cache - search_result.SearchResults(types, e, m, s, d, mods) -> + SearchResults(types, e, m, s, d, mods) -> cache.cache_search_results( model.submitted_input, index, @@ -159,7 +168,7 @@ pub fn update_search_results( ) |> dict.insert(model.view_cache, key, _) } - Model( + Data( ..model, search_results: dict.insert(model.search_results, key, search_results), index: index, @@ -187,10 +196,10 @@ fn is_higher(new: List(Int), old: List(Int)) { fn extract_package_version( acc: Dict(String, String), - search_result: search_result.SearchResult, + search_result: TypeSearch, ) -> Dict(String, String) { - let assert Ok(re) = regex.from_string("^[0-9]*.[0-9]*.[0-9]*$") - case regex.check(re, search_result.version) { + let assert Ok(re) = regexp.from_string("^[0-9]*.[0-9]*.[0-9]*$") + case regexp.check(re, search_result.version) { False -> acc True -> case dict.get(acc, search_result.package_name) { @@ -219,7 +228,7 @@ fn extract_package_version( } } -pub fn update_search_results_filter(model: Model) { +pub fn update_search_results_filter(model: Data) { let default_key = default_search_key(model.submitted_input) let show_old = case model.show_old_packages { True -> fn(_) { True } @@ -230,7 +239,7 @@ pub fn update_search_results_filter(model: Model) { case search_results { search_result.Start | search_result.InternalServerError -> dict.new() - search_result.SearchResults(t, e, m, s, d, mods) -> { + SearchResults(t, e, m, s, d, mods) -> { dict.new() |> list.fold(t, _, extract_package_version) |> list.fold(e, _, extract_package_version) @@ -242,7 +251,7 @@ pub fn update_search_results_filter(model: Model) { } } } - fn(a: search_result.SearchResult) { + fn(a: TypeSearch) { case dict.get(last_versions, a.package_name) { Error(_) -> False Ok(content) -> content == a.version @@ -252,21 +261,21 @@ pub fn update_search_results_filter(model: Model) { } let or_filters = [ - #(model.keep_functions, fn(s: search_result.SearchResult) { - s.kind == kind.Function + #(model.keep_functions, fn(s: TypeSearch) { + s.signature_kind == kind.Function }), - #(model.keep_types, fn(s: search_result.SearchResult) { - s.kind == kind.TypeDefinition + #(model.keep_types, fn(s: TypeSearch) { + s.signature_kind == kind.TypeDefinition }), - #(model.keep_aliases, fn(s: search_result.SearchResult) { - s.kind == kind.TypeAlias + #(model.keep_aliases, fn(s: TypeSearch) { + s.signature_kind == kind.TypeAlias }), ] |> list.filter(fn(a) { a.0 }) |> list.map(pair.second) let and_filters = [ - #(model.keep_documented, fn(s: search_result.SearchResult) { + #(model.keep_documented, fn(s: TypeSearch) { string.length(s.documentation) > 0 }), ] @@ -275,11 +284,11 @@ pub fn update_search_results_filter(model: Model) { let filter = fn(s) { case list.is_empty(or_filters) { True -> True - False -> list.any(or_filters, function.apply1(_, s)) + False -> list.any(or_filters, fn(f) { f(s) }) } && case list.is_empty(and_filters) { True -> True - False -> list.any(and_filters, function.apply1(_, s)) + False -> list.any(and_filters, fn(f) { f(s) }) } && show_old(s) } @@ -290,8 +299,8 @@ pub fn update_search_results_filter(model: Model) { let search_results = case search_results { search_result.Start | search_result.InternalServerError -> search_results - search_result.SearchResults(t, e, m, s, d, mods) -> - search_result.SearchResults( + SearchResults(t, e, m, s, d, mods) -> + SearchResults( t |> list.filter(filter), e |> list.filter(filter), m |> list.filter(filter), @@ -310,7 +319,7 @@ pub fn update_search_results_filter(model: Model) { let view_cache = case search_results { search_result.Start | search_result.InternalServerError -> model.view_cache - search_result.SearchResults(types, e, m, s, d, mods) -> + SearchResults(types, e, m, s, d, mods) -> cache.cache_search_results( model.submitted_input, index, @@ -323,7 +332,7 @@ pub fn update_search_results_filter(model: Model) { ) |> dict.insert(model.view_cache, key, _) } - Model( + Data( ..model, search_results: dict.insert(model.search_results, key, search_results), index: index, @@ -333,8 +342,8 @@ pub fn update_search_results_filter(model: Model) { } } -pub fn reset(model: Model) { - Model( +pub fn reset(model: Data) { + Data( search_results: model.search_results, input: "", index: [], @@ -364,7 +373,7 @@ pub fn reset(model: Model) { fn compute_index(search_results: SearchResults) -> Index { case search_results { search_result.Start | search_result.InternalServerError -> [] - search_result.SearchResults(types, exact, others, searches, docs, modules) -> { + SearchResults(types, exact, others, searches, docs, modules) -> { [] |> insert_module_names(types) |> insert_module_names(exact) @@ -377,11 +386,11 @@ fn compute_index(search_results: SearchResults) -> Index { } } -fn insert_module_names(index: Index, search_results: List(SearchResult)) { +fn insert_module_names(index: Index, search_results: List(TypeSearch)) { use acc, val <- list.fold(search_results, index) let key = #(val.package_name, val.version) list.key_find(acc, key) |> result.unwrap([]) - |> fn(i) { list.prepend(i, #(val.module_name, val.name)) } + |> fn(i) { list.prepend(i, #(val.module_name, val.type_name)) } |> fn(i) { list.key_set(acc, key, i) } } diff --git a/apps/frontend/src/data/msg.gleam b/apps/frontend/src/data/msg.gleam index 26229e5..4136bf3 100644 --- a/apps/frontend/src/data/msg.gleam +++ b/apps/frontend/src/data/msg.gleam @@ -1,10 +1,9 @@ -import birl +import data/analytics import data/package import data/search_result.{type SearchResults} +import frontend/discuss import frontend/router -import gleam/option -import lustre_http as http -import plinth/browser/event.{type Event} +import gleam/dynamic.{type Dynamic} pub type Filter { Functions @@ -16,39 +15,18 @@ pub type Filter { VectorSearch } -pub type Package { - Package( - name: String, - repository: String, - rank: Int, - popularity: option.Option(Int), - ) -} - -pub type Analytics { - Analytics( - total_searches: Int, - total_signatures: Int, - total_indexed: Int, - timeseries: List(#(Int, birl.Time)), - ranked: List(Package), - popular: List(Package), - ) -} - pub type Msg { - None - Packages(packages: Result(List(package.Package), http.HttpError)) - OnSearchFocus(event: Event) - SubmitSearch - UpdateIsMobile(is_mobile: Bool) - SearchResults(input: String, result: Result(SearchResults, http.HttpError)) - Trendings(result: Result(List(package.Package), http.HttpError)) - UpdateInput(String) - Reset - ScrollTo(String) - OnEscape - OnAnalytics(Result(Analytics, http.HttpError)) - OnRouteChange(router.Route) - OnCheckFilter(Filter, Bool) + ApiReturnedAnalytics(analytics: analytics.Analytics) + ApiReturnedPackages(packages: List(package.Package)) + ApiReturnedSearchResults(input: String, search_results: SearchResults) + ApiReturnedTrendings(trendings: List(package.Package)) + AppRequiredDiscussToast(message: discuss.DiscussError) + BrowserChangedRoute(route: router.Route) + BrowserResizedViewport(is_mobile: Bool) + UserClickedSidebarName(id: String) + UserFocusedSearch(event: Dynamic) + UserInputtedSearch(query: String) + UserPressedEscape + UserSubmittedSearch + UserToggledFilter(filter: Filter, value: Bool) } diff --git a/apps/frontend/src/data/search_result.gleam b/apps/frontend/src/data/search_result.gleam index 82f487f..d5da578 100644 --- a/apps/frontend/src/data/search_result.gleam +++ b/apps/frontend/src/data/search_result.gleam @@ -1,58 +1,22 @@ -import data/kind.{type Kind} -import data/metadata.{type Metadata} -import data/signature.{type Signature} +import data/type_search.{type TypeSearch} import frontend/view/helpers import gleam/dynamic -import gleam/list -import gleam/option.{None, Some} -import gleam/result - -pub type SearchResult { - SearchResult( - documentation: String, - module_name: String, - name: String, - kind: Kind, - package_name: String, - json_signature: Signature, - metadata: Metadata, - version: String, - ) -} pub type SearchResults { Start InternalServerError SearchResults( - exact_type_matches: List(SearchResult), - exact_name_matches: List(SearchResult), - name_signature_matches: List(SearchResult), - vector_signature_searches: List(SearchResult), - docs_searches: List(SearchResult), - module_searches: List(SearchResult), + exact_type_matches: List(TypeSearch), + exact_name_matches: List(TypeSearch), + name_signature_matches: List(TypeSearch), + vector_signature_searches: List(TypeSearch), + docs_searches: List(TypeSearch), + module_searches: List(TypeSearch), ) } -pub fn decode_search_result(dyn) { - dynamic.decode8( - SearchResult, - dynamic.field("documentation", dynamic.string), - dynamic.field("module_name", dynamic.string), - dynamic.field("name", dynamic.string), - dynamic.field("kind", kind.decode_kind), - dynamic.field("package_name", dynamic.string), - dynamic.field("json_signature", signature.decode_signature), - dynamic.field("metadata", metadata.decode_metadata), - dynamic.field("version", dynamic.string), - )(dyn) - |> result.map(Some) - |> result.try_recover(fn(_) { Ok(None) }) -} - pub fn decode_search_results_list(dyn) { - use data <- result.map(dynamic.list(decode_search_result)(dyn)) - use item <- list.filter_map(data) - option.to_result(item, "") + dynamic.list(type_search.decode)(dyn) } pub fn decode_search_results(dyn) { @@ -72,11 +36,11 @@ pub fn decode_search_results(dyn) { ])(dyn) } -pub fn hexdocs_link(search_result: SearchResult) { +pub fn hexdocs_link(search_result: TypeSearch) { helpers.hexdocs_link( package: search_result.package_name, version: search_result.version, module: search_result.module_name, - name: search_result.name, + name: search_result.type_name, ) } diff --git a/apps/frontend/src/decoder_extra.gleam b/apps/frontend/src/decoder_extra.gleam deleted file mode 100644 index b2905f9..0000000 --- a/apps/frontend/src/decoder_extra.gleam +++ /dev/null @@ -1,10 +0,0 @@ -import gleam/dynamic.{type Dynamic} -import gleam/option -import gleam/result - -pub fn completely_option(field: String) { - fn(dyn: Dynamic) { - dynamic.optional_field(field, dynamic.optional(dynamic.string))(dyn) - |> result.map(fn(res) { option.flatten(res) }) - } -} diff --git a/apps/frontend/src/bar_chart.element.mjs b/apps/frontend/src/elements/bar_chart.element.mjs similarity index 91% rename from apps/frontend/src/bar_chart.element.mjs rename to apps/frontend/src/elements/bar_chart.element.mjs index c4c0fec..de691fd 100644 --- a/apps/frontend/src/bar_chart.element.mjs +++ b/apps/frontend/src/elements/bar_chart.element.mjs @@ -1,5 +1,9 @@ import { Chart } from 'chart.js/auto' +export function register() { + customElements.define('bar-chart', BarChart) +} + export class BarChart extends HTMLElement { static observedAttributes = ['datasets'] @@ -87,14 +91,4 @@ export class BarChart extends HTMLElement { }, }) } - - // Lifecycle functions. - disconnectedCallback() {} - adoptedCallback() {} - - attributeChangedCallback() {} - - static register() { - customElements.define('bar-chart', BarChart) - } } diff --git a/apps/frontend/src/line_chart.element.mjs b/apps/frontend/src/elements/line_chart.element.mjs similarity index 90% rename from apps/frontend/src/line_chart.element.mjs rename to apps/frontend/src/elements/line_chart.element.mjs index e27fb9a..4c6f322 100644 --- a/apps/frontend/src/line_chart.element.mjs +++ b/apps/frontend/src/elements/line_chart.element.mjs @@ -1,5 +1,9 @@ import { Chart } from 'chart.js/auto' +export function register() { + customElements.define('line-chart', LineChart) +} + export class LineChart extends HTMLElement { static observedAttributes = ['datasets'] @@ -76,14 +80,4 @@ export class LineChart extends HTMLElement { }, }) } - - // Lifecycle functions. - disconnectedCallback() {} - adoptedCallback() {} - - attributeChangedCallback() {} - - static register() { - customElements.define('line-chart', LineChart) - } } diff --git a/apps/frontend/src/frontend.gleam b/apps/frontend/src/frontend.gleam index 7c44d5a..552a3c2 100644 --- a/apps/frontend/src/frontend.gleam +++ b/apps/frontend/src/frontend.gleam @@ -1,284 +1,66 @@ -import birl +import bright import data/model.{type Model} import data/msg.{type Msg} -import data/package -import data/search_result -import frontend/config -import frontend/errors -import frontend/ffi +import frontend/effects/api +import frontend/effects/window import frontend/router +import frontend/setup +import frontend/update import frontend/view -import frontend/view/body/search_result as sr -import gleam/bool -import gleam/dict -import gleam/dynamic -import gleam/io -import gleam/option.{None, Some} -import gleam/pair -import gleam/result -import gleam/uri.{type Uri} - -// import grille_pain -// import grille_pain/lustre/toast -// import grille_pain/options -import lustre import lustre/effect -import lustre/lazy -import lustre/update -import lustre_http as http -import modem -import plinth/browser/event.{type Event} -import sketch/lustre as sketch -import sketch/options as sketch_options -import toast/error as toast_error - -fn focus(on id: String, event event: Event) { - use _ <- effect.from() - use <- bool.guard(when: ffi.is_active(id), return: Nil) - event.prevent_default(event) - ffi.focus(on: id, event: event) -} - -fn unfocus() { - use _ <- effect.from() - ffi.unfocus() -} - -fn subscribe_focus() { - use dispatch <- effect.from() - use event <- ffi.subscribe_focus() - case event.key(event) { - "Escape" -> dispatch(msg.OnEscape) - _ -> dispatch(msg.OnSearchFocus(event)) - } -} - -fn subscribe_is_mobile() { - use dispatch <- effect.from() - use is_mobile <- ffi.suscribe_is_mobile() - dispatch(msg.UpdateIsMobile(is_mobile)) -} pub fn main() { - let assert Ok(cache) = sketch.setup(sketch_options.node()) - let assert Ok(_) = lazy.setup() - let assert Ok(_) = sr.setup() - // let assert Ok(_) = - // options.default() - // |> options.timeout(5000) - // |> grille_pain.setup() - - let assert Ok(_) = - view.view - |> sketch.compose(cache) - |> lustre.application(init, update, _) - |> lustre.start("#app", Nil) -} - -fn decode_package(dyn) { - dynamic.decode4( - msg.Package, - dynamic.field("name", dynamic.string), - dynamic.field("repository", dynamic.string), - dynamic.field("rank", dynamic.int), - dynamic.field("popularity", dynamic.optional(dynamic.int)), - )(dyn) -} - -fn init(_) { - let initial = - modem.initial_uri() - |> result.map(router.parse_uri) - |> result.unwrap(router.Home) - |> handle_route_change(model.init(), _) - submit_search(initial.0) - |> update.add_effect(modem.init(on_url_change)) - |> update.add_effect(router.update_page_title({ initial.0 }.route)) - |> update.add_effect(subscribe_focus()) - |> update.add_effect(subscribe_is_mobile()) - |> update.add_effect( - http.expect_json(dynamic.list(package.decoder), msg.Trendings) - |> http.get(config.api_endpoint() <> "/trendings", _), - ) - |> update.add_effect( - http.expect_json(dynamic.list(package.decoder), msg.Packages) - |> http.get(config.api_endpoint() <> "/packages", _), - ) - |> update.add_effect( - msg.OnAnalytics - |> http.expect_json( - dynamic.decode6( - msg.Analytics, - dynamic.field("total", dynamic.int), - dynamic.field("signatures", dynamic.int), - dynamic.field("packages", dynamic.int), - dynamic.field("timeseries", { - dynamic.list(dynamic.decode2( - pair.new, - dynamic.field("count", dynamic.int), - dynamic.field("date", fn(dyn) { - dynamic.string(dyn) - |> result.then(fn(t) { - birl.parse(t) - |> result.replace_error([]) - }) - }), - )) - }), - dynamic.field("ranked", dynamic.list(decode_package)), - dynamic.field("popular", dynamic.list(decode_package)), - ), - _, - ) - |> http.get(config.api_endpoint() <> "/analytics", _), - ) -} - -fn on_url_change(uri: Uri) -> Msg { - router.parse_uri(uri) - |> msg.OnRouteChange() -} - -fn update(model: Model, msg: Msg) { - case io.debug(msg) { - msg.UpdateInput(content) -> update_input(model, content) - msg.SubmitSearch -> submit_search(model) - msg.Reset -> reset(model) - msg.None -> update.none(model) - msg.Packages(Ok(packages)) -> model.Model(..model, packages:) |> update.none - msg.Packages(_) -> update.none(model) - msg.ScrollTo(id) -> scroll_to(model, id) - msg.OnRouteChange(route) -> handle_route_change(model, route) - msg.Trendings(trendings) -> handle_trendings(model, trendings) - msg.OnSearchFocus(event) -> - update.effect(model, focus(on: "search-input", event: event)) - msg.OnEscape -> update.effect(model, unfocus()) - msg.UpdateIsMobile(is_mobile) -> - model - |> model.update_is_mobile(is_mobile) - |> update.none - msg.SearchResults(input, search_results) -> - handle_search_results(model, input, search_results) - msg.OnCheckFilter(filter, value) -> - handle_oncheck_filter(model, filter, value) - msg.OnAnalytics(analytics) -> { - case analytics { - Error(_) -> #(model, effect.none()) - Ok(analytics) -> - model - |> model.update_analytics(analytics) - |> update.none() - } - } - } -} - -fn handle_oncheck_filter(model, filter, value) { - case filter, value { - msg.Functions, value -> model.Model(..model, keep_functions: value) - msg.Types, value -> model.Model(..model, keep_types: value) - msg.Aliases, value -> model.Model(..model, keep_aliases: value) - msg.Documented, value -> model.Model(..model, keep_documented: value) - msg.ShowOldPackages, value -> model.Model(..model, show_old_packages: value) - msg.VectorSearch, value -> model.Model(..model, show_vector_search: value) - msg.DocumentationSearch, value -> - model.Model(..model, show_documentation_search: value) - } - |> model.update_search_results_filter - |> update.none -} - -fn update_input(model: Model, content: String) { + let assert Ok(_) = setup.sketch() + let assert Ok(_) = setup.components() + let assert Ok(_) = setup.grille_pain() + let assert Ok(_) = setup.start_application(init, update, view.view) +} + +fn init(_) -> #(Model, effect.Effect(Msg)) { + let route = setup.initial_route() + let model = bright.init(model.init_data(), model.Computed) + use model <- bright.start(model) + use model <- bright.update(model, update.handle_changed_route(_, route)) + use model <- bright.update(model, update.handle_submitted_search) model - |> model.update_input(content) - |> update.none -} - -fn reset(model: Model) { + |> bright.run(fn(_, _) { setup.modem() }) + |> bright.run(fn(data, _) { router.update_page_title(data.route) }) + |> bright.run(fn(_, _) { window.subscribe_focus() }) + |> bright.run(fn(_, _) { window.subscribe_is_mobile() }) + |> bright.run(fn(_, _) { api.get_trendings() }) + |> bright.run(fn(_, _) { api.get_packages() }) + |> bright.run(fn(_, _) { api.get_analytics() }) +} + +fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { + use model <- bright.start(model) + use model <- bright.update(model, update_data(_, msg)) model - |> model.reset - |> update.none } -fn submit_search(model: Model) { - use <- bool.guard(when: model.input == "", return: #(model, effect.none())) - use <- bool.guard(when: model.loading, return: #(model, effect.none())) - let new_model = model.update_submitted_input(model) - case dict.get(new_model.search_results, new_model.submitted_input) { - Ok(_) -> { - let new_route = router.Search(new_model.submitted_input) - let is_same_route = new_model.route == new_route - use <- bool.guard(when: is_same_route, return: update.none(new_model)) - new_model - |> update.effect({ - Some("q=" <> new_model.submitted_input) - |> modem.push("search", _, None) - }) - |> update.add_effect(unfocus()) - } - Error(_) -> - msg.SearchResults(input: model.input, result: _) - |> http.expect_json(search_result.decode_search_results, _) - |> http.get(config.api_endpoint() <> "/search?q=" <> model.input, _) - |> update.effect(model.toggle_loading(new_model), _) - |> update.add_effect(unfocus()) +fn update_data(model: model.Data, msg: Msg) { + case msg { + msg.ApiReturnedAnalytics(analytics:) -> + update.handle_analytics(model, analytics) + msg.ApiReturnedPackages(packages:) -> + update.handle_packages(model, packages) + msg.ApiReturnedSearchResults(input:, search_results:) -> + update.handle_search_results(model, input, search_results) + msg.ApiReturnedTrendings(trendings:) -> + update.handle_trendings(model, trendings) + msg.AppRequiredDiscussToast(message:) -> + update.handle_discuss_toast(model, message) + msg.BrowserChangedRoute(route:) -> update.handle_changed_route(model, route) + msg.BrowserResizedViewport(is_mobile:) -> + update.handle_resized_viewport(model, is_mobile) + msg.UserClickedSidebarName(id:) -> + update.handle_clicked_sidebar_name(model, id) + msg.UserFocusedSearch(event:) -> update.handle_focused_search(model, event) + msg.UserInputtedSearch(query:) -> + update.handle_inputted_search(model, query) + msg.UserPressedEscape -> update.handle_pressed_escape(model) + msg.UserSubmittedSearch -> update.handle_submitted_search(model) + msg.UserToggledFilter(filter:, value:) -> + update.handle_toggle_filter(model, filter, value) } } - -fn scroll_to(model: Model, id: String) { - ffi.scroll_to(element: id) - |> effect.from - |> update.effect(model, _) -} - -fn handle_search_results( - model: Model, - input: String, - search_results: Result(search_result.SearchResults, http.HttpError), -) { - let toast = display_toast(search_results) - search_results - |> result.map(model.update_search_results(model, input, _)) - |> result.unwrap(model) - |> model.toggle_loading - |> update.effect(toast) - |> update.add_effect(modem.push("/search", Some("q=" <> input), None)) -} - -fn handle_route_change(model: Model, route: router.Route) { - let model = model.update_route(model, route) - case route { - router.Home -> model.update_input(model, "") - router.Packages -> model.update_input(model, "") - router.Trending -> model.update_input(model, "") - router.Analytics -> model.update_input(model, "") - router.Search(q) -> - model.update_input(model, q) - |> model.update_submitted_input - } - |> update.effect(router.update_page_title(route)) -} - -fn display_toast( - search_results: Result(search_result.SearchResults, http.HttpError), -) { - search_results - |> result.map_error(fn(error) { - toast_error.describe_http_error(error) - |> option.map(errors.capture_message) - // |> option.map(toast.error) - |> option.map(fn(_) { effect.none() }) - }) - |> result.unwrap_error(option.None) - |> option.unwrap(effect.none()) -} - -fn handle_trendings( - model: Model, - trendings: Result(List(package.Package), http.HttpError), -) { - trendings - |> result.map(model.update_trendings(model, _)) - |> result.unwrap(model) - |> update.none -} diff --git a/apps/frontend/src/frontend.ts b/apps/frontend/src/frontend.mjs similarity index 55% rename from apps/frontend/src/frontend.ts rename to apps/frontend/src/frontend.mjs index 173bd1f..91cd215 100644 --- a/apps/frontend/src/frontend.ts +++ b/apps/frontend/src/frontend.mjs @@ -1,27 +1,18 @@ -// @ts-ignore import gleamHljs from '@gleam-lang/highlight.js-gleam' import hljs from 'highlight.js/lib/core' import plaintext from 'highlight.js/lib/languages/plaintext' -// @ts-ignore -import { BarChart } from './bar_chart.element.mjs' -// @ts-ignore +import * as barChart from './elements/bar_chart.element.mjs' +import * as lineChart from './elements/line_chart.element.mjs' import { main } from './frontend.gleam' -// @ts-ignore -import { LineChart } from './line_chart.element.mjs' import './stylesheets/all.css' import './stylesheets/hljs-theme.css' import './stylesheets/main.css' import './stylesheets/normalize.css' -LineChart.register() -BarChart.register() -// @ts-ignore -Element.prototype._attachShadow = Element.prototype.attachShadow -Element.prototype.attachShadow = function () { - // @ts-ignore - return this._attachShadow({ mode: 'open' }) -} +lineChart.register() +barChart.register() hljs.registerLanguage('gleam', gleamHljs) hljs.registerLanguage('plaintext', plaintext) + document.addEventListener('DOMContentLoaded', main) diff --git a/apps/frontend/src/frontend/config.gleam b/apps/frontend/src/frontend/config.gleam deleted file mode 100644 index e960b1b..0000000 --- a/apps/frontend/src/frontend/config.gleam +++ /dev/null @@ -1,8 +0,0 @@ -import vitools - -pub fn api_endpoint() { - case vitools.is_dev() { - True -> "http://localhost:3000" - False -> "https://api.gloogle.run" - } -} diff --git a/apps/frontend/src/frontend/discuss.gleam b/apps/frontend/src/frontend/discuss.gleam new file mode 100644 index 0000000..35e74f3 --- /dev/null +++ b/apps/frontend/src/frontend/discuss.gleam @@ -0,0 +1,136 @@ +import gleam/bool +import gleam/dynamic.{type Decoder, type Dynamic} +import gleam/fetch +import gleam/http +import gleam/http/request +import gleam/http/response +import gleam/javascript/promise +import gleam/list +import gleam/result +import gleam/string +import gleam/uri +import vitools + +pub type DiscussError { + InternalServerError + InvalidJsonBody + DecodeError(List(dynamic.DecodeError)) + NetworkError + NotFound +} + +pub opaque type Discuss(a) { + Discuss( + path: String, + query: List(#(String, String)), + method: http.Method, + decoder: Decoder(a), + on_success: fn(a) -> Nil, + on_error: fn(DiscussError) -> Nil, + ) +} + +pub fn about(segments: List(String)) { + let path = encode_path(segments) + Discuss( + path:, + query: [], + method: http.Get, + decoder: fn(a: Dynamic) { Ok(a) }, + on_success: fn(_) { Nil }, + on_error: fn(_) { Nil }, + ) +} + +pub fn via(discuss: Discuss(a), method method: http.Method) { + Discuss(..discuss, method:) +} + +pub fn query(discuss: Discuss(a), query: List(#(String, String))) { + Discuss(..discuss, query:) +} + +pub fn expect(discuss: Discuss(a), format decoder: Decoder(b)) { + let Discuss(path:, query:, method:, ..) = discuss + Discuss( + path:, + decoder:, + method:, + query:, + on_success: fn(_) { Nil }, + on_error: fn(_) { Nil }, + ) +} + +pub fn on_success(discuss: Discuss(a), run on_success: fn(a) -> Nil) { + Discuss(..discuss, on_success:) +} + +pub fn on_error(discuss: Discuss(a), run on_error: fn(DiscussError) -> Nil) { + Discuss(..discuss, on_error:) +} + +pub fn start(discuss: Discuss(a)) { + let assert Ok(req) = endpoint() + req + |> request.set_path(discuss.path) + |> request.set_query(discuss.query) + |> fetch.send() + |> promise.try_await(fetch.read_json_body) + |> promise.map(fn(result) { result.map_error(result, map_fetch_error) }) + |> promise.tap(fn(res) { + case res { + Error(error) -> discuss.on_error(error) + Ok(res) -> { + let warn = fn(error) { fn() { discuss.on_error(error) } } + use <- handle_status(res, 404, warn(NotFound)) + use <- handle_status(res, 500, warn(InternalServerError)) + case discuss.decoder(res.body) { + Error(error) -> discuss.on_error(DecodeError(error)) + Ok(res) -> discuss.on_success(res) + } + } + } + }) +} + +fn encode_path(segments: List(String)) { + segments + |> list.map(uri.percent_encode) + |> string.join("/") + |> prepend_slash +} + +fn prepend_slash(path) { + case path { + "/" <> path -> path + path -> "/" <> path + } +} + +fn endpoint() { + case vitools.is_dev() { + True -> "http://localhost:3000" + False -> "https://api.gloogle.run" + } + |> request.to +} + +fn map_fetch_error(error: fetch.FetchError) { + case error { + fetch.InvalidJsonBody -> InvalidJsonBody + fetch.NetworkError(_) -> NetworkError + fetch.UnableToReadBody -> InvalidJsonBody + } +} + +fn handle_status( + response: response.Response(a), + status_code: Int, + return: fn() -> b, + continuation: fn() -> b, +) { + let is_matching_status = status_code == response.status + use <- bool.lazy_guard(when: is_matching_status, return:) + continuation() +} diff --git a/apps/frontend/src/frontend/effects/api.gleam b/apps/frontend/src/frontend/effects/api.gleam new file mode 100644 index 0000000..378ab70 --- /dev/null +++ b/apps/frontend/src/frontend/effects/api.gleam @@ -0,0 +1,53 @@ +import data/analytics +import data/model.{type Data} +import data/msg +import data/package +import data/search_result +import frontend/discuss +import gleam/dynamic +import lustre/effect + +pub fn get_trendings() { + use dispatch <- effect.from + discuss.about(["trendings"]) + |> discuss.expect(dynamic.list(package.decode)) + |> discuss.on_success(fn(m) { dispatch(msg.ApiReturnedTrendings(m)) }) + |> discuss.on_error(fn(e) { dispatch(msg.AppRequiredDiscussToast(e)) }) + |> discuss.start + Nil +} + +pub fn get_packages() { + use dispatch <- effect.from + discuss.about(["packages"]) + |> discuss.expect(dynamic.list(package.decode)) + |> discuss.on_success(fn(m) { dispatch(msg.ApiReturnedPackages(m)) }) + |> discuss.on_error(fn(e) { dispatch(msg.AppRequiredDiscussToast(e)) }) + |> discuss.start + Nil +} + +pub fn get_analytics() { + use dispatch <- effect.from + discuss.about(["analytics"]) + |> discuss.expect(analytics.decode) + |> discuss.on_success(fn(m) { dispatch(msg.ApiReturnedAnalytics(m)) }) + |> discuss.on_error(fn(e) { dispatch(msg.AppRequiredDiscussToast(e)) }) + |> discuss.start + Nil +} + +pub fn get_search(data: Data) { + use dispatch <- effect.from + discuss.about(["search"]) + |> discuss.query([#("q", data.input)]) + |> discuss.expect(search_result.decode_search_results) + |> discuss.on_success(fn(search_results) { + data.input + |> msg.ApiReturnedSearchResults(input: _, search_results:) + |> dispatch + }) + |> discuss.on_error(fn(e) { dispatch(msg.AppRequiredDiscussToast(e)) }) + |> discuss.start + Nil +} diff --git a/apps/frontend/src/frontend/effects/document.gleam b/apps/frontend/src/frontend/effects/document.gleam new file mode 100644 index 0000000..ccf3219 --- /dev/null +++ b/apps/frontend/src/frontend/effects/document.gleam @@ -0,0 +1,9 @@ +import lustre/effect + +pub fn update_title(title: String) { + use _ <- effect.from + do_update_title(title) +} + +@external(javascript, "../../gloogle.ffi.mjs", "updateTitle") +fn do_update_title(title: String) -> Nil diff --git a/apps/frontend/src/frontend/effects/window.gleam b/apps/frontend/src/frontend/effects/window.gleam new file mode 100644 index 0000000..35d03bf --- /dev/null +++ b/apps/frontend/src/frontend/effects/window.gleam @@ -0,0 +1,60 @@ +import data/msg +import gleam/bool +import gleam/dynamic.{type Dynamic} +import lustre/effect +import lustre/event + +pub fn focus(on id: String, event event: Dynamic) { + use _ <- effect.from() + use <- bool.guard(when: do_is_active(id), return: Nil) + event.prevent_default(event) + do_focus(on: id, event: event) +} + +pub fn blur() { + use _ <- effect.from() + do_blur() +} + +pub fn subscribe_focus() { + use dispatch <- effect.from() + use event <- do_subscribe_focus() + case do_key(event) { + Ok("Escape") -> dispatch(msg.UserPressedEscape) + _ -> dispatch(msg.UserFocusedSearch(event)) + } +} + +pub fn subscribe_is_mobile() { + use dispatch <- effect.from() + use is_mobile <- do_suscribe_is_mobile() + dispatch(msg.BrowserResizedViewport(is_mobile)) +} + +pub fn scroll_to(element id: String) { + use _ <- effect.from + do_scroll_to(id) +} + +// FFI + +@external(javascript, "../../gloogle.ffi.mjs", "subscribeFocus") +fn do_subscribe_focus(callback: fn(Dynamic) -> Nil) -> Nil + +@external(javascript, "../../gloogle.ffi.mjs", "subscribeIsMobile") +fn do_suscribe_is_mobile(callback: fn(Bool) -> Nil) -> Nil + +@external(javascript, "../../gloogle.ffi.mjs", "focus") +fn do_focus(on id: String, event event: a) -> Nil + +@external(javascript, "../../gloogle.ffi.mjs", "isActive") +fn do_is_active(element id: String) -> Bool + +@external(javascript, "../../gloogle.ffi.mjs", "eventKey") +fn do_key(event: Dynamic) -> Result(String, Nil) + +@external(javascript, "../../gloogle.ffi.mjs", "blur") +fn do_blur() -> Nil + +@external(javascript, "../../gloogle.ffi.mjs", "scrollTo") +pub fn do_scroll_to(element id: String) -> Nil diff --git a/apps/frontend/src/frontend/ffi.gleam b/apps/frontend/src/frontend/ffi.gleam deleted file mode 100644 index d8f2118..0000000 --- a/apps/frontend/src/frontend/ffi.gleam +++ /dev/null @@ -1,22 +0,0 @@ -import plinth/browser/event.{type Event} - -@external(javascript, "../gloogle.ffi.mjs", "scrollTo") -pub fn scroll_to(element id: String) -> fn(dispatch) -> Nil - -@external(javascript, "../gloogle.ffi.mjs", "subscribeIsMobile") -pub fn suscribe_is_mobile(callback: fn(Bool) -> Nil) -> Nil - -@external(javascript, "../gloogle.ffi.mjs", "subscribeFocus") -pub fn subscribe_focus(callback: fn(Event) -> Nil) -> Nil - -@external(javascript, "../gloogle.ffi.mjs", "focus") -pub fn focus(on id: String, event event: a) -> Nil - -@external(javascript, "../gloogle.ffi.mjs", "isActive") -pub fn is_active(element id: String) -> Bool - -@external(javascript, "../gloogle.ffi.mjs", "unfocus") -pub fn unfocus() -> Nil - -@external(javascript, "../gloogle.ffi.mjs", "updateTitle") -pub fn update_title(title: String) -> Nil diff --git a/apps/frontend/src/frontend/router.gleam b/apps/frontend/src/frontend/router.gleam index f5377f1..d6433c3 100644 --- a/apps/frontend/src/frontend/router.gleam +++ b/apps/frontend/src/frontend/router.gleam @@ -1,9 +1,9 @@ -import frontend/ffi +import frontend/effects/document import gleam/list import gleam/option import gleam/result import gleam/uri.{type Uri} -import lustre/effect +import modem pub type Route { Home @@ -37,12 +37,32 @@ fn handle_search_path(uri: Uri) { } pub fn update_page_title(route: Route) { - use _ <- effect.from() case route { - Home -> ffi.update_title("Gloogle") - Packages -> ffi.update_title("Gloogle — Packages") - Search(q) -> ffi.update_title("Gloogle — Search " <> q) - Trending -> ffi.update_title("Gloogle — Trending") - Analytics -> ffi.update_title("Gloogle — Analytics") + Home -> document.update_title("Gloogle") + Packages -> document.update_title("Gloogle — Packages") + Search(q) -> document.update_title("Gloogle — Search " <> q) + Trending -> document.update_title("Gloogle — Trending") + Analytics -> document.update_title("Gloogle — Analytics") } } + +pub fn to_uri(route: Route) -> Uri { + let assert Ok(uri) = case route { + Home -> uri.parse("/") + Search(query:) -> uri.parse("/search?" <> query) + Packages -> uri.parse("/packages") + Trending -> uri.parse("/trending") + Analytics -> uri.parse("/analytics") + } + uri +} + +pub fn push(route: Route) { + let uri = to_uri(route) + modem.push(uri.path, uri.query, uri.fragment) +} + +pub fn replace(route: Route) { + let uri = to_uri(route) + modem.replace(uri.path, uri.query, uri.fragment) +} diff --git a/apps/frontend/src/frontend/setup.gleam b/apps/frontend/src/frontend/setup.gleam new file mode 100644 index 0000000..bf13b25 --- /dev/null +++ b/apps/frontend/src/frontend/setup.gleam @@ -0,0 +1,46 @@ +import data/msg +import frontend/router +import frontend/view/body/search_result as sr +import gleam/result +import grille_pain +import grille_pain/options +import lustre +import lustre/lazy +import modem +import sketch +import sketch/magic + +pub fn sketch() { + use cache <- result.try(sketch.cache(strategy: sketch.Ephemeral)) + use _ <- result.try(magic.setup(cache)) + Ok(Nil) +} + +pub fn components() { + use _ <- result.try(lazy.setup()) + use _ <- result.try(sr.setup()) + Ok(Nil) +} + +pub fn grille_pain() { + options.default() + |> options.timeout(5000) + |> grille_pain.setup() +} + +pub fn start_application(init, update, view) { + lustre.application(init, update, view) + |> lustre.start("#app", Nil) +} + +pub fn initial_route() { + modem.initial_uri() + |> result.map(router.parse_uri) + |> result.unwrap(router.Home) +} + +pub fn modem() { + use uri <- modem.init + router.parse_uri(uri) + |> msg.BrowserChangedRoute +} diff --git a/apps/frontend/src/frontend/update.gleam b/apps/frontend/src/frontend/update.gleam new file mode 100644 index 0000000..73b1a31 --- /dev/null +++ b/apps/frontend/src/frontend/update.gleam @@ -0,0 +1,132 @@ +import data/analytics +import data/model.{type Data} +import data/msg +import data/package +import data/search_result +import frontend/discuss +import frontend/effects/api +import frontend/effects/window +import frontend/errors +import frontend/router +import gleam/bool +import gleam/dict +import gleam/dynamic.{type Dynamic} +import gleam/list +import gleam/option +import gleam/pair +import grille_pain/lustre/toast +import lustre/effect +import toast/error as toast_error + +pub fn handle_analytics(model: Data, analytics: analytics.Analytics) { + model + |> model.update_analytics(analytics) + |> pair.new(effect.none()) +} + +pub fn handle_packages(model: Data, packages: List(package.Package)) { + model.Data(..model, packages:) + |> pair.new(effect.none()) +} + +pub fn handle_search_results( + model: Data, + input: String, + search_results: search_result.SearchResults, +) { + search_results + |> model.update_search_results(model, input, _) + |> model.toggle_loading + |> pair.new(router.push(router.Search("q=" <> input))) +} + +pub fn handle_trendings(model: Data, trendings: List(package.Package)) { + trendings + |> model.update_trendings(model, _) + |> pair.new(effect.none()) +} + +pub fn handle_discuss_toast(model: Data, message: discuss.DiscussError) { + message + |> toast_error.describe_http_error + |> option.map(errors.capture_message) + |> option.map(toast.error) + |> option.unwrap(effect.none()) + |> pair.new(model, _) +} + +pub fn handle_changed_route(model: Data, route: router.Route) { + let model = model.update_route(model, route) + case route { + router.Home -> model.update_input(model, "") + router.Packages -> model.update_input(model, "") + router.Trending -> model.update_input(model, "") + router.Analytics -> model.update_input(model, "") + router.Search(q) -> + model.update_input(model, q) + |> model.update_submitted_input + } + |> pair.new(router.update_page_title(route)) +} + +pub fn handle_resized_viewport(model: Data, is_mobile: Bool) { + model + |> model.update_is_mobile(is_mobile) + |> pair.new(effect.none()) +} + +pub fn handle_clicked_sidebar_name(model: Data, id: String) { + window.scroll_to(element: id) + |> pair.new(model, _) +} + +pub fn handle_focused_search(model: Data, event: Dynamic) { + #(model, window.focus(on: "search-input", event: event)) +} + +pub fn handle_inputted_search(model: Data, content: String) { + model + |> model.update_input(content) + |> pair.new(effect.none()) +} + +pub fn handle_pressed_escape(model: Data) { + #(model, window.blur()) +} + +pub fn handle_submitted_search(model: Data) { + use <- bool.guard(when: model.input == "", return: #(model, effect.none())) + use <- bool.guard(when: model.loading, return: #(model, effect.none())) + let new_model = model.update_submitted_input(model) + case dict.get(new_model.search_results, new_model.submitted_input) { + Ok(_) -> { + let new_route = router.Search(new_model.submitted_input) + let is_same_route = new_model.route == new_route + use <- bool.guard(when: is_same_route, return: #(new_model, effect.none())) + [router.push(router.Search("q=" <> new_model.submitted_input))] + |> list.prepend(window.blur()) + |> effect.batch + |> pair.new(new_model, _) + } + Error(_) -> { + let effects = effect.batch([api.get_search(model), window.blur()]) + model.toggle_loading(new_model) + |> pair.new(effects) + } + } +} + +pub fn handle_toggle_filter(model, filter, value) { + case filter, value { + msg.Functions, value -> model.Data(..model, keep_functions: value) + msg.Types, value -> model.Data(..model, keep_types: value) + msg.Aliases, value -> model.Data(..model, keep_aliases: value) + msg.Documented, value -> model.Data(..model, keep_documented: value) + msg.ShowOldPackages, value -> model.Data(..model, show_old_packages: value) + msg.VectorSearch, value -> model.Data(..model, show_vector_search: value) + msg.DocumentationSearch, value -> + model.Data(..model, show_documentation_search: value) + } + |> model.update_search_results_filter + |> pair.new(effect.none()) +} diff --git a/apps/frontend/src/frontend/view.gleam b/apps/frontend/src/frontend/view.gleam index 1989f1a..02dd890 100644 --- a/apps/frontend/src/frontend/view.gleam +++ b/apps/frontend/src/frontend/view.gleam @@ -1,3 +1,4 @@ +import bright import data/model.{type Model} import frontend/colors/palette import frontend/router @@ -5,37 +6,37 @@ import frontend/view/body/body import frontend/view/footer/footer import frontend/view/navbar/navbar import lustre/element as el -import sketch -import sketch/lustre/element as l +import sketch as s +import sketch/magic +import sketch/magic/element/html as h import sketch/media import sketch/size.{px} fn layout(attributes, children) { - l.memo("div", attributes, children, [ - sketch.display("grid"), - sketch.grid_template_areas( - "\"sidebar navbar\" - \"sidebar main\" - \"sidebar footer\"", - ), - sketch.property("--a-color", palette.dark.faff_pink), - sketch.grid_template_columns("auto 1fr"), - sketch.grid_template_rows("auto 1fr auto"), - sketch.min_height(size.vh(100)), - sketch.media(media.max_width(px(700)), [ - sketch.grid_template_areas("\"navbar\" \"main\" \"footer\""), - sketch.grid_template_columns("1fr"), + s.class([ + s.display("grid"), + s.grid_template_areas(["sidebar navbar", "sidebar main", "sidebar footer"]), + s.property("--a-color", palette.dark.faff_pink), + s.grid_template_columns("auto 1fr"), + s.grid_template_rows("auto 1fr auto"), + s.min_height(size.vh(100)), + s.media(media.max_width(px(700)), [ + s.grid_template_areas(["navbar", "main", "footer"]), + s.grid_template_columns("1fr"), ]), ]) + |> h.div(attributes, children) } pub fn view(model: Model) { + use <- magic.render([magic.node()]) + use data, _computed <- bright.view(model) layout([], [ - navbar.navbar(model), - body.body(model), - case model.route { + navbar.navbar(data), + body.body(data), + case data.route { router.Home -> footer.view() - router.Search(_) -> footer.search_bar(model) + router.Search(_) -> footer.search_bar(data) _ -> el.none() }, ]) diff --git a/apps/frontend/src/frontend/view/body/body.gleam b/apps/frontend/src/frontend/view/body/body.gleam index 59b192f..2eb8fc3 100644 --- a/apps/frontend/src/frontend/view/body/body.gleam +++ b/apps/frontend/src/frontend/view/body/body.gleam @@ -1,6 +1,6 @@ import birl import chart.{Dataset} -import data/model.{type Model} +import data/model.{type Data} import data/msg import data/search_result import frontend/icons @@ -22,13 +22,13 @@ import lustre/element/html as h import lustre/event as e import lustre/lazy -fn view_search_input(model: Model) { +fn view_search_input(model: Data) { let has_content = { model.input |> string.length() |> fn(input) { input != 0 } } - h.form([a.class("search-wrapper"), e.on_submit(msg.SubmitSearch)], [ + h.form([a.class("search-wrapper"), e.on_submit(msg.UserSubmittedSearch)], [ h.div([a.class("search-title-wrapper")], [ h.div([a.class("search-title")], [ h.img([ @@ -67,7 +67,7 @@ fn empty_state( ]) } -pub fn view_trending(model: Model) { +pub fn view_trending(_model: Data) { el.none() // case model.trendings { // None -> @@ -137,7 +137,7 @@ pub fn view_trending(model: Model) { // } } -fn sidebar(model: Model) { +fn sidebar(model: Data) { use <- bool.guard(when: model.is_mobile, return: el.none()) let disabled = case model.route { router.Search(..) -> a.style([#("opacity", "1")]) @@ -152,7 +152,7 @@ fn sidebar(model: Model) { ]), h.form([a.class("sidebar-title-inside")], [h.text("Gloogle")]), ]), - h.form([e.on_submit(msg.SubmitSearch)], [ + h.form([e.on_submit(msg.UserSubmittedSearch)], [ search_input.view(model.loading, model.input, small: True), ]), h.div([a.class("sidebar-filter"), disabled], [el.text("Filters")]), @@ -208,7 +208,7 @@ fn checkbox(active: Bool, msg: msg.Filter, name: String) { a.class("sidebar-checkbox-2"), a.type_("checkbox"), a.checked(active), - e.on_check(msg.OnCheckFilter(msg, _)), + e.on_check(msg.UserToggledFilter(msg, _)), ]), ]), h.div([a.class("sidebar-filter-name")], [el.text(name)]), @@ -251,7 +251,7 @@ fn analytics_box(title: String, count: Int) { ]) } -fn popularity_chart(model: Model) { +fn popularity_chart(model: Data) { let data = list.filter(model.popular, fn(p) { p.repository != "https://github.com/gleam-lang/gleam" @@ -270,7 +270,7 @@ fn popularity_chart(model: Model) { }) } -fn ranked_chart(model: Model) { +fn ranked_chart(model: Data) { let data = list.filter(model.ranked, fn(p) { p.name != "gleam_stdlib" && p.name != "gleeunit" @@ -284,7 +284,7 @@ fn ranked_chart(model: Model) { }) } -fn analytics_chart(model: Model) { +fn analytics_chart(model: Data) { let data = model.timeseries use <- bool.guard(when: list.is_empty(data), return: el.none()) chart.line_chart({ @@ -322,7 +322,7 @@ fn analytics_title(title: String, border: Bool) { ]) } -fn view_analytics(model: Model) { +fn view_analytics(model: Data) { el.fragment([ sidebar(model), h.main([a.class("main"), a.style([#("padding", "24px 36px")])], [ @@ -374,7 +374,7 @@ fn view_analytics(model: Model) { ]) } -fn view_packages(model: Model) { +fn view_packages(model: Data) { el.fragment([ sidebar(model), h.main( @@ -429,7 +429,7 @@ fn view_packages(model: Model) { ]) } -pub fn body(model: Model) { +pub fn body(model: Data) { case model.route { router.Home -> h.main([a.class("main")], [view_search_input(model)]) router.Packages -> view_packages(model) diff --git a/apps/frontend/src/frontend/view/body/cache.gleam b/apps/frontend/src/frontend/view/body/cache.gleam index 8068bf3..8706ca0 100644 --- a/apps/frontend/src/frontend/view/body/cache.gleam +++ b/apps/frontend/src/frontend/view/body/cache.gleam @@ -1,5 +1,5 @@ import data/msg -import data/search_result +import data/type_search import frontend/view/body/search_result as sr import frontend/view/types as t import gleam/list @@ -8,7 +8,7 @@ import lustre/element as el import lustre/element/html as h import lustre/event as e -fn view_search_results(search_results: List(search_result.SearchResult)) { +fn view_search_results(search_results: List(type_search.TypeSearch)) { list.map(search_results, sr.view) |> list.intersperse(h.div([a.class("search-result-separator")], [])) |> el.fragment @@ -32,11 +32,13 @@ fn sidebar( ..list.map(modules, fn(module) { let #(module, name) = module let id = package.0 <> "@" <> package.1 <> "-" <> module <> "-" <> name - h.div([a.class("sidebar-module-name"), e.on_click(msg.ScrollTo(id))], [ - t.keyword(module), - h.text("."), - t.fun(name), - ]) + h.div( + [ + a.class("sidebar-module-name"), + e.on_click(msg.UserClickedSidebarName(id)), + ], + [t.keyword(module), h.text("."), t.fun(name)], + ) }) ]) } @@ -74,12 +76,12 @@ fn types_separator() { pub fn cache_search_results( search: String, index: List(#(#(String, String), List(#(String, String)))), - types: List(search_result.SearchResult), - exact: List(search_result.SearchResult), - others: List(search_result.SearchResult), - searches: List(search_result.SearchResult), - docs_searches: List(search_result.SearchResult), - modules_searches: List(search_result.SearchResult), + types: List(type_search.TypeSearch), + exact: List(type_search.TypeSearch), + others: List(type_search.TypeSearch), + searches: List(type_search.TypeSearch), + docs_searches: List(type_search.TypeSearch), + modules_searches: List(type_search.TypeSearch), ) { h.div([a.class("search-results-wrapper")], [ sidebar(search, index), diff --git a/apps/frontend/src/frontend/view/body/search_result.gleam b/apps/frontend/src/frontend/view/body/search_result.gleam index 59ea5b9..750cc09 100644 --- a/apps/frontend/src/frontend/view/body/search_result.gleam +++ b/apps/frontend/src/frontend/view/body/search_result.gleam @@ -1,5 +1,6 @@ import data/implementations -import data/search_result.{type SearchResult} +import data/search_result +import data/type_search.{type TypeSearch} import frontend/colors/palette import frontend/icons import frontend/view/body/signature @@ -11,6 +12,7 @@ import gleam/dict import gleam/dynamic import gleam/list import gleam/option +import gleam/pair import gleam/result import lustre import lustre/attribute as a @@ -18,14 +20,13 @@ import lustre/effect import lustre/element import lustre/element/html as h import lustre/event as e -import lustre/update pub type Model { - Model(item: option.Option(SearchResult), opened: Bool) + Model(item: option.Option(TypeSearch), opened: Bool) } pub type Msg { - Received(option.Option(SearchResult)) + Received(option.Option(TypeSearch)) ToggleOpen } @@ -41,7 +42,7 @@ pub fn setup() { |> lustre.register("search-result") } -pub fn view(item: SearchResult) { +pub fn view(item: TypeSearch) { let attributes = [a.property("item", item)] element.element("search-result", attributes, []) } @@ -51,7 +52,7 @@ fn update(model, msg) { ToggleOpen -> Model(..model, opened: !model.opened) Received(search_result) -> Model(item: search_result, opened: False) } - |> update.none + |> pair.new(effect.none()) } fn implementation_pill(item) { @@ -82,7 +83,7 @@ fn internal_view(model: Model) -> element.Element(Msg) { use <- bool.guard(when: option.is_none(model.item), return: element.none()) let assert option.Some(item) = model.item let package_id = item.package_name <> "@" <> item.version - let id = package_id <> "-" <> item.module_name <> "-" <> item.name + let id = package_id <> "-" <> item.module_name <> "-" <> item.type_name h.div([a.class("search-result"), a.id(id)], [ h.div([a.class("search-details")], [ h.div([a.class("search-details-name")], [ @@ -99,7 +100,7 @@ fn internal_view(model: Model) -> element.Element(Msg) { ]) } -fn view_name(item: SearchResult) { +fn view_name(item: TypeSearch) { let class = a.class("qualified-name") let href = search_result.hexdocs_link(item) h.a([class, a.target("_blank"), a.rel("noreferrer"), a.href(href)], [ @@ -108,11 +109,11 @@ fn view_name(item: SearchResult) { t.dark_white("."), t.keyword(item.module_name), t.dark_white("."), - t.fun(item.name), + t.fun(item.type_name), ]) } -fn view_documentation_arrow(model: Model, item: SearchResult) { +fn view_documentation_arrow(model: Model, item: TypeSearch) { use <- bool.guard(when: item.documentation == "", return: element.none()) let no_implementation = option.is_none(item.metadata.implementations) use <- bool.guard(when: no_implementation, return: element.none()) @@ -132,14 +133,14 @@ fn view_documentation_arrow(model: Model, item: SearchResult) { ]) } -fn view_implementation_pills(model: Model, item: SearchResult) { +fn view_implementation_pills(model: Model, item: TypeSearch) { use <- bool.guard(when: !model.opened, return: element.none()) item.metadata.implementations |> option.map(implementation_pills) |> option.unwrap(element.none()) } -fn view_documentation(model: Model, item: SearchResult) { +fn view_documentation(model: Model, item: TypeSearch) { use <- bool.guard(when: item.documentation == "", return: element.none()) use <- bool.guard(when: !model.opened, return: element.none()) h.div([a.class("documentation")], [documentation.view(item.documentation)]) diff --git a/apps/frontend/src/frontend/view/body/signature.gleam b/apps/frontend/src/frontend/view/body/signature.gleam index 5e17df1..081ee3e 100644 --- a/apps/frontend/src/frontend/view/body/signature.gleam +++ b/apps/frontend/src/frontend/view/body/signature.gleam @@ -1,6 +1,5 @@ -import data/msg -import data/search_result import data/signature.{type Parameter, type Type, Parameter} +import data/type_search import frontend/view/helpers import frontend/view/types as t import gleam/bool @@ -33,7 +32,7 @@ fn render_parameters(count: Int) { do_render_parameters(0, int.max(count - 1, 0), []) |> list.reverse() |> list.intersperse(h.text(", ")) - |> fn(t) { list.concat([[h.text("(")], t, [h.text(")")]]) } + |> fn(t) { list.flatten([[h.text("(")], t, [h.text(")")]]) } } } @@ -41,7 +40,7 @@ fn view_type(type_: Type, indent: Int) -> List(el.Element(msg)) { case type_ { signature.Tuple(width, elements) -> { let inline = width + indent <= 80 - list.concat([ + list.flatten([ [helpers.idt(indent), h.text("#(")], case inline { False -> [ @@ -49,13 +48,13 @@ fn view_type(type_: Type, indent: Int) -> List(el.Element(msg)) { ..{ list.map(elements, view_type(_, indent + 2)) |> list.intersperse([h.text(","), helpers.newline()]) - |> list.concat() + |> list.flatten() } ] True -> list.map(elements, view_type(_, 0)) |> list.intersperse([h.text(", ")]) - |> list.concat() + |> list.flatten() }, [ bool.guard(inline, h.text(""), fn() { helpers.idt(indent) }), @@ -65,19 +64,19 @@ fn view_type(type_: Type, indent: Int) -> List(el.Element(msg)) { } signature.Fn(width, parameters, return) -> { let inline = width + indent <= 80 - list.concat([ + list.flatten([ [helpers.idt(indent), t.keyword("fn"), h.text("(")], case inline { True -> list.map(parameters, view_type(_, 0)) |> list.intersperse([h.text(", ")]) - |> list.concat() + |> list.flatten() False -> [ helpers.newline(), ..{ list.map(parameters, view_type(_, indent + 2)) |> list.intersperse([h.text(","), helpers.newline()]) - |> list.concat() + |> list.flatten() } ] }, @@ -113,7 +112,7 @@ fn view_type(type_: Type, indent: Int) -> List(el.Element(msg)) { signature.Named(width, name, package, module, parameters, version) -> { let inline = width + indent <= 80 let is_params = !list.is_empty(parameters) - list.concat([ + list.flatten([ [ helpers.idt(indent), case version { @@ -143,13 +142,13 @@ fn view_type(type_: Type, indent: Int) -> List(el.Element(msg)) { True -> list.map(parameters, view_type(_, 0)) |> list.intersperse([h.text(", ")]) - |> list.concat() + |> list.flatten() False -> [ helpers.newline(), ..{ list.map(parameters, view_type(_, indent + 2)) |> list.intersperse([h.text(","), helpers.newline()]) - |> list.concat() + |> list.flatten() } ] }, @@ -167,7 +166,7 @@ fn view_type(type_: Type, indent: Int) -> List(el.Element(msg)) { fn view_parameter(parameter: Parameter, indent: Int) { let Parameter(width, label, type_) = parameter - list.concat([ + list.flatten([ case label { None -> [el.none()] Some(label) -> [helpers.idt(indent), t.label(label), h.text(": ")] @@ -183,7 +182,7 @@ fn view_parameter(parameter: Parameter, indent: Int) { fn view_type_constructor(constructor: signature.TypeConstructor, indent: Int) { let inline = constructor.params_width <= 70 let has_params = !list.is_empty(constructor.parameters) - list.concat([ + list.flatten([ [ helpers.idt(indent), t.type_(constructor.name), @@ -194,17 +193,17 @@ fn view_type_constructor(constructor: signature.TypeConstructor, indent: Int) { ], case inline { False -> - list.concat([ + list.flatten([ [helpers.newline()], list.map(constructor.parameters, view_parameter(_, { indent + 2 })) |> list.intersperse([h.text(","), helpers.newline()]) - |> list.concat(), + |> list.flatten(), [h.text(","), helpers.newline()], ]) True -> list.map(constructor.parameters, view_parameter(_, 0)) |> list.intersperse([h.text(", ")]) - |> list.concat() + |> list.flatten() }, case has_params, inline { False, _ -> [] @@ -214,11 +213,15 @@ fn view_type_constructor(constructor: signature.TypeConstructor, indent: Int) { ]) } -pub fn view_signature(item: search_result.SearchResult) -> List(el.Element(msg)) { +pub fn view_signature(item: type_search.TypeSearch) -> List(el.Element(msg)) { case item.json_signature { signature.TypeDefinition(parameters, constructors) -> - list.concat([ - [t.keyword("type "), t.fun(item.name), ..render_parameters(parameters)], + list.flatten([ + [ + t.keyword("type "), + t.fun(item.type_name), + ..render_parameters(parameters) + ], case constructors { [] -> [] _ -> [h.text(" {"), helpers.newline()] @@ -229,7 +232,7 @@ pub fn view_signature(item: search_result.SearchResult) -> List(el.Element(msg)) constructors |> list.map(view_type_constructor(_, 2)) |> list.intersperse([helpers.newline()]) - |> list.concat() + |> list.flatten() }, case constructors { [] -> [] @@ -237,18 +240,18 @@ pub fn view_signature(item: search_result.SearchResult) -> List(el.Element(msg)) }, ]) signature.Constant(width, type_) -> - list.concat([ - [t.keyword("const "), t.fun(item.name), h.text(" = ")], + list.flatten([ + [t.keyword("const "), t.fun(item.type_name), h.text(" = ")], case width > 80 { True -> [helpers.newline(), ..view_type(type_, 2)] False -> view_type(type_, 0) }, ]) signature.TypeAlias(width, parameters, alias) -> { - list.concat([ + list.flatten([ [ t.keyword("type "), - t.type_(item.name), + t.type_(item.type_name), ..render_parameters(parameters) ], [h.text(" = ")], @@ -259,11 +262,11 @@ pub fn view_signature(item: search_result.SearchResult) -> List(el.Element(msg)) ]) } signature.Function(_width, params_width, name, return, parameters) -> { - list.concat([ + list.flatten([ [t.keyword("fn "), t.fun(name), h.text("(")], case params_width > 70 { True -> - list.concat([ + list.flatten([ [helpers.newline(), helpers.idt(2)], list.map(parameters, view_parameter(_, 2)) |> list.intersperse([ @@ -271,13 +274,13 @@ pub fn view_signature(item: search_result.SearchResult) -> List(el.Element(msg)) helpers.newline(), helpers.idt(2), ]) - |> list.concat(), + |> list.flatten(), [h.text(","), helpers.newline()], ]) False -> list.map(parameters, view_parameter(_, 0)) |> list.intersperse([h.text(", ")]) - |> list.concat() + |> list.flatten() }, [ h.text(") -> "), diff --git a/apps/frontend/src/frontend/view/body/styles.gleam b/apps/frontend/src/frontend/view/body/styles.gleam index f4bd558..2567b99 100644 --- a/apps/frontend/src/frontend/view/body/styles.gleam +++ b/apps/frontend/src/frontend/view/body/styles.gleam @@ -1,62 +1,54 @@ import frontend/colors/palette -import gleam/bool -import gleam/int -import gleam/list import lustre/attribute as a import lustre/element import sketch as s -import sketch/lustre/element as el +import sketch/magic/element/html as h import sketch/media -import sketch/size.{percent, px, vh, vw} +import sketch/size.{px, vh, vw} pub fn implementations_pill_container(attrs, children) { - let id = "implementations_pill_container" - el.memo_dynamic("div", attrs, children, id, [ + s.class([ s.display("flex"), s.align_items("center"), s.font_size(px(14)), s.font_weight("300"), s.gap(px(6)), ]) + |> h.div(attrs, children) } -pub fn implementations_pill(background, color, attributes, children) { - let id = "implementations-pill-" <> background - s.dynamic(id, [ +pub fn implementations_pill(background, attributes, children) { + s.class([ s.background(background), s.border_radius(px(6)), s.width(px(8)), s.height(px(8)), ]) - |> s.memo() - |> s.to_lustre() - |> list.prepend(attributes, _) - |> element.element("div", _, children) + |> h.div(attributes, children) } pub fn implementations_pill_wrapper(attributes, children) { - let id = "implementations_pill_wrapper" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.display("flex"), s.align_items("center"), s.gap(px(12)), s.justify_content("end"), ]) + |> h.div(attributes, children) } pub fn search_result(attributes, children) { - let id = "search_result" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.overflow("hidden"), s.display("flex"), s.flex_direction("column"), s.gap(px(12)), ]) + |> h.div(attributes, children) } pub fn search_results_wrapper(attributes, children) { - let id = "search_results_wrapper" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.display("grid"), s.padding_right(px(48)), s.gap(px(36)), @@ -67,103 +59,89 @@ pub fn search_results_wrapper(attributes, children) { s.padding_("0 24px"), ]), ]) + |> h.div(attributes, children) } pub fn external_icon_wrapper(attrs, children) { - let id = "external_icon_wrapper" - el.memo_dynamic("div", attrs, children, id, [ - s.width(px(16)), - s.height(px(16)), - ]) + s.class([s.width(px(16)), s.height(px(16))]) + |> h.div(attrs, children) } pub fn search_details(attributes, children) { - let id = "search_details" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.display("flex"), s.gap(px(12)), s.align_items("center"), s.media(media.max_width(px(700)), [s.flex_direction("column")]), ]) + |> h.div(attributes, children) } pub fn search_details_title(attributes, children) { - let id = "search_details_title" - el.memo_dynamic("div", attributes, children, id, [ - s.display("flex"), - s.align_items("center"), - s.gap(px(12)), - ]) + s.class([s.display("flex"), s.align_items("center"), s.gap(px(12))]) + |> h.div(attributes, children) } pub fn qualified_name(attributes, children) { - let id = "qualified_name" - el.memo_dynamic("a", attributes, children, id, [ + s.class([ s.overflow("hidden"), s.text_overflow("ellipsis"), - s.direction("rtl"), s.text_decoration("none"), s.hover([s.text_decoration("underline")]), ]) + |> h.a(attributes, children) } pub fn search_body(attributes, children) { - let id = "search_body" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.background("rgba(254, 254, 252, 0.05)"), s.border_radius(px(12)), s.padding_("12px 24px"), s.border("1px solid rgba(254, 254, 252, .1)"), ]) + |> h.div(attributes, children) } pub fn signature(attributes, children) { - let id = "signature" - el.memo_dynamic("code", attributes, children, id, [ + s.class([ s.white_space("pre-wrap"), s.display("block"), s.line_height("1.6"), s.overflow("auto"), ]) + |> h.code(attributes, children) } pub fn documentation(attributes, children) { - let id = "documentation" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.display("flex"), s.flex_direction("column"), s.gap(px(12)), s.padding_top(px(6)), ]) + |> h.div(attributes, children) } pub fn search_result_separator() { - let id = "search_result_separator" - el.memo_dynamic("div", [], [], id, [ + s.class([ s.height(px(1)), s.background("rgba(254, 254, 252, 0.1)"), s.margin_("6px 0"), ]) + |> h.div([], []) } pub fn documentation_title(attributes, children) { - let id = "documentation_title" - el.memo_dynamic("div", attributes, children, id, [ - s.color(palette.dark.dark_white), - ]) + s.class([s.color(palette.dark.dark_white)]) + |> h.div(attributes, children) } pub fn search_wrapper(attributes, children) { - let id = "search_wrapper" - el.memo_dynamic("form", attributes, children, id, [ + s.class([ s.display("grid"), s.grid_template_rows("auto auto auto"), s.grid_template_columns("auto auto auto"), - s.grid_template_areas( - "\"title . .\" - \"input input input\" - \". . submit\"", - ), + s.grid_template_areas(["title . .", "input input input", ". . submit"]), s.padding(px(48)), s.gap(px(24)), s.max_width(px(700)), @@ -172,13 +150,12 @@ pub fn search_wrapper(attributes, children) { s.media(media.max_width(px(700)), [ s.max_width(vw(100)), s.padding(px(24)), - s.grid_template_areas( - "\"title title title\" - \"input input input\" - \". . submit\"", - ), + s.grid_template_areas([ + "title title title", "input input input", ". . submit", + ]), ]), ]) + |> h.form(attributes, children) } pub fn search_title_() { @@ -192,38 +169,30 @@ pub fn search_title_() { } pub fn search_title(attributes, children) { - let id = "search_title" - el.memo_dynamic("div", attributes, children, id, [ - s.compose(search_title_()), - s.font_size(size.rem_(2.5)), - ]) + s.class([s.compose(search_title_()), s.font_size(size.rem(2.5))]) + |> h.div(attributes, children) } pub fn search_title_wrapper(attributes, children) { - let id = "search_title_wrapper" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.grid_area("title"), s.display("flex"), s.flex_direction("column"), s.gap(px(9)), - s.font_size(size.rem_(0.9)), + s.font_size(size.rem(0.9)), s.color(palette.dark.dark_white), s.line_height("1.3"), ]) + |> h.div(attributes, children) } pub fn search_lucy(size, attributes) { - let id = "search-lucy-" <> int.to_string(size) - s.dynamic(id, [s.width(px(size))]) - |> s.memo() - |> s.to_lustre() - |> list.prepend(attributes, _) - |> element.element("img", _, []) + s.class([s.width(px(size))]) + |> h.img(attributes) } pub fn search_submit(attributes) { - let id = "search_submit" - el.memo_dynamic("input", attributes, [], id, [ + s.class([ s.grid_area("submit"), s.appearance("none"), s.border("none"), @@ -234,18 +203,18 @@ pub fn search_submit(attributes) { s.padding_right(px(24)), s.padding_left(px(24)), s.color(palette.dark.charcoal), - s.font_size(size.rem(1)), + s.font_size(size.rem(1.0)), s.outline("none"), s.transition("background .3s"), s.active([s.background(palette.dark.dark_faff_pink)]), s.focus([s.background(palette.dark.dark_faff_pink)]), s.disabled([s.background(palette.dark.unexpected_aubergine)]), ]) + |> h.input(attributes) } pub fn matches_titles(attributes, children) { - let id = "matches_titles" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.line_height("1.3"), s.color(palette.dark.dark_white), s.display("flex"), @@ -254,19 +223,16 @@ pub fn matches_titles(attributes, children) { s.font_size(px(12)), s.media(media.max_width(px(700)), [s.flex_direction("column")]), ]) + |> h.div(attributes, children) } pub fn matches_title(attributes, children) { - let id = "matches_title" - el.memo_dynamic("div", attributes, children, id, [ - s.color(palette.dark.white), - s.font_size(px(18)), - ]) + s.class([s.color(palette.dark.white), s.font_size(px(18))]) + |> h.div(attributes, children) } pub fn empty_state(attributes, children) { - let id = "empty_state" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.grid_row("span 3"), s.display("flex"), s.align_items("center"), @@ -277,16 +243,16 @@ pub fn empty_state(attributes, children) { s.margin_("auto"), s.padding(px(24)), ]) + |> h.div(attributes, children) } pub fn empty_state_lucy(attributes) { - let id = "empty_state_lucy" - el.memo_dynamic("img", attributes, [], id, [s.width(px(100))]) + s.class([s.width(px(100))]) + |> h.img(attributes) } pub fn empty_state_titles(attributes, children) { - let id = "empty_state_titles" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.font_size(px(20)), s.display("flex"), s.flex_direction("column"), @@ -294,19 +260,16 @@ pub fn empty_state_titles(attributes, children) { s.line_height("1.3"), s.max_width(px(400)), ]) + |> h.div(attributes, children) } pub fn empty_state_subtitle(attributes, children) { - let id = "empty_state_subtitle" - el.memo_dynamic("div", attributes, children, id, [ - s.font_size(px(16)), - s.color(palette.dark.dark_white), - ]) + s.class([s.font_size(px(16)), s.color(palette.dark.dark_white)]) + |> h.div(attributes, children) } pub fn sidebar_wrapper(attributes, children) { - let id = "sidebar_wrapper" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.position("sticky"), s.top(px(0)), s.display("flex"), @@ -326,32 +289,28 @@ pub fn sidebar_wrapper(attributes, children) { }), s.media(media.max_width(px(700)), [s.display("none")]), ]) + |> h.div(attributes, children) } pub fn sidebar_wrapper_title(attrs, children) { - let id = "sidebar_wrapper_title" - el.memo_dynamic("div", attrs, children, id, [ - s.padding_("4px 4px"), - s.color("#ffffff99"), - ]) + s.class([s.padding_("4px 4px"), s.color("#ffffff99")]) + |> h.div(attrs, children) } pub fn sidebar_package_name(attributes, children) { - let id = "sidebar_package_name" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.padding_left(px(8)), s.border_radius(px(6)), s.overflow("hidden"), s.text_overflow("ellipsis"), ]) + |> h.div(attributes, children) } pub fn sidebar_module_name(attributes, children) { - let id = "sidebar_module_name" - el.memo_dynamic("button", attributes, children, id, [ + s.class([ s.text_overflow("ellipsis"), s.overflow("hidden"), - s.direction("rtl"), s.text_align("left"), s.cursor("pointer"), s.hover([s.text_decoration("underline")]), @@ -366,75 +325,71 @@ pub fn sidebar_module_name(attributes, children) { s.property("padding-inline", "0"), s.padding_left(px(16)), ]) + |> h.button(attributes, children) } pub fn sidebar_package_wrapper(attributes, children) { - let id = "sidebar_package_wrapper" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.display("flex"), s.flex_direction("column"), s.gap(px(9)), s.font_size(px(14)), s.max_width(px(250)), ]) + |> h.div(attributes, children) } pub fn main(attributes, children) { - let id = "main" - el.memo_dynamic("main", attributes, children, id, [ + s.class([ s.grid_area("main"), s.display("flex"), s.flex_direction("column"), s.gap(px(24)), ]) + |> h.main(attributes, children) } pub fn sidebar_title(attrs, children) { - let id = "sidebar_title" - el.memo_dynamic("a", attrs, children, id, [ + s.class([ s.display("flex"), s.align_items("center"), s.gap(px(16)), s.color("inherit"), s.text_decoration("none"), ]) + |> h.a(attrs, children) } pub fn sidebar_title_inside(attrs, children) { - let id = "sidebar_title_inside" - el.memo_dynamic("div", attrs, children, id, []) + s.class([]) |> h.div(attrs, children) } pub fn form_wrapper(attrs, children) { - let id = "form_wrapper" - el.memo_dynamic("form", attrs, children, id, []) + s.class([]) |> h.form(attrs, children) } pub fn sidebar_filter(attrs, children) { - let id = "sidebar_filter" - el.memo_dynamic("div", attrs, children, id, [ + s.class([ s.padding_top(px(12)), s.padding_left(px(12)), s.color("rgba(254, 254, 252, .6)"), ]) + |> h.div(attrs, children) } pub fn sidebar_filters(attrs, children) { - let id = "sidebar_filters" - el.memo_dynamic("div", attrs, children, id, [ + s.class([ s.display("flex"), s.flex_direction("column"), s.gap(px(18)), s.padding_("0 12px"), ]) + |> h.div(attrs, children) } pub fn sidebar_checkbox(active, attrs) { - let val = bool.to_string(active) - let id1 = "sidebar-checkbox-div-" <> val - let id2 = "sidebar-checkbox-input-" <> val element.fragment([ - el.memo_dynamic("div", [], [], id1, [ + s.class([ s.width(px(16)), s.height(px(16)), s.background(case active { @@ -443,85 +398,75 @@ pub fn sidebar_checkbox(active, attrs) { }), s.border("1px solid rgba(254, 254, 252, .1)"), s.border_radius(px(4)), - ]), - el.memo_dynamic( - "input", - [a.type_("checkbox"), a.checked(active), ..attrs], - [], - id2, - [s.position("fixed"), s.top(px(-1000)), s.width(px(1)), s.height(px(1))], - ), + ]) + |> h.div([], []), + s.class([ + s.position("fixed"), + s.top(px(-1000)), + s.width(px(1)), + s.height(px(1)), + ]) + |> h.input([a.type_("checkbox"), a.checked(active), ..attrs]), ]) } pub fn sidebar_spacer(attrs, children) { - let id = "sidebar_spacer" - el.memo_dynamic("div", attrs, children, id, [s.flex("1")]) + s.class([s.flex("1")]) |> h.div(attrs, children) } pub fn filter_separator(attrs, children) { - let id = "filter_separator" - el.memo_dynamic("div", attrs, children, id, [ - s.height(px(1)), - s.background("rgba(254, 254, 252, .1)"), - ]) + s.class([s.height(px(1)), s.background("rgba(254, 254, 252, .1)")]) + |> h.div(attrs, children) } pub fn sidebar_filter_line(attrs, children) { - let id = "sidebar_filter_line" - el.memo_dynamic("label", attrs, children, id, [ + s.class([ s.display("flex"), s.gap(px(9)), s.cursor("pointer"), s.align_items("center"), ]) + |> h.label(attrs, children) } pub fn sidebar_filter_name(attrs, children) { - let id = "sidebar_filter_name" - el.memo_dynamic("div", attrs, children, id, [s.color("white")]) + s.class([s.color("white")]) + |> h.div(attrs, children) } pub fn sidebar_links(attrs, children) { - let id = "sidebar_links" - el.memo_dynamic("div", attrs, children, id, [ + s.class([ s.display("flex"), s.flex_direction("column"), s.gap(px(24)), s.padding(px(12)), ]) + |> h.div(attrs, children) } pub fn sidebar_link_wrapper(attrs, children) { - let id = "sidebar_link_wrapper" - el.memo_dynamic("a", attrs, children, id, [ + s.class([ s.display("flex"), s.align_items("baseline"), s.gap(px(9)), s.text_decoration("none"), s.color("inherit"), ]) + |> h.a(attrs, children) } pub fn sidebar_icon(attrs, children) { - let id = "sidebar_icon" - el.memo_dynamic("div", attrs, children, id, [ - s.width(px(12)), - s.height(px(12)), - ]) + s.class([s.width(px(12)), s.height(px(12))]) + |> h.div(attrs, children) } pub fn sidebar_link(attrs, children) { - let id = "sidebar_link" - el.memo_dynamic("div", attrs, children, id, [ - s.font_size(px(14)), - s.color("rgba(254, 254, 252, 0.6)"), - ]) + s.class([s.font_size(px(14)), s.color("rgba(254, 254, 252, 0.6)")]) + |> h.div(attrs, children) } pub fn items_wrapper(attributes, children) { - let id = "items_wrapper" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.display("flex"), s.flex_direction("column"), s.gap(px(24)), @@ -530,75 +475,62 @@ pub fn items_wrapper(attributes, children) { s.width(size.percent(100)), s.overflow("hidden"), ]) + |> h.div(attributes, children) } pub fn named_type_button(attributes, children) { - let id = "named_type_button" - el.memo_dynamic("a", attributes, children, id, [s.color("#e5c07b")]) + s.class([s.color("#e5c07b")]) + |> h.a(attributes, children) } pub fn search_title_with_hint(attributes, children) { - let id = "search_title_with_hint" - el.memo_dynamic("div", attributes, children, id, [ - s.display("flex"), - s.gap(px(12)), - ]) + s.class([s.display("flex"), s.gap(px(12))]) + |> h.div(attributes, children) } pub fn pre_alpha_title(attributes, children) { - let id = "pre_alpha_title" - el.memo_dynamic("div", attributes, children, id, [s.font_size(px(16))]) + s.class([s.font_size(px(16))]) + |> h.div(attributes, children) } pub fn loading_trending(attributes, children) { - let id = "loading_trending" - el.memo_dynamic("div", attributes, children, id, []) + s.class([]) + |> h.div(attributes, children) } pub fn no_trendings(attributes, children) { - let id = "no_trendings" - el.memo_dynamic("div", attributes, children, id, []) + s.class([]) |> h.div(attributes, children) } pub fn trendings_wrapper(attributes, children) { - let id = "trendings_wrapper" - el.memo_dynamic("div", attributes, children, id, [s.padding_("12px 48px")]) + s.class([s.padding_("12px 48px")]) |> h.div(attributes, children) } pub fn trendings_title(attributes, children) { - let id = "trendings_title" - el.memo_dynamic("div", attributes, children, id, [s.margin_bottom(px(24))]) + s.class([s.margin_bottom(px(24))]) |> h.div(attributes, children) } pub fn trendings_grid(attributes, children) { - let id = "trendings_grid" - el.memo_dynamic("div", attributes, children, id, [ + s.class([ s.display("grid"), // s.grid_template_columns("repeat(auto-fill, minmax(350px, 1fr))"), s.align_items("start"), s.gap(px(24)), ]) + |> h.div(attributes, children) } pub fn trendings_card(attributes, children) { - let id = "trendings_card" - el.memo_dynamic("div", attributes, children, id, [ - s.background(palette.dark.unexpected_aubergine), - ]) + s.class([s.background(palette.dark.unexpected_aubergine)]) + |> h.div(attributes, children) } pub fn documentation_links(attributes, children) { - let id = "documentation_links" - el.memo_dynamic("div", attributes, children, id, [ - s.display("flex"), - s.justify_content("space-between"), - ]) + s.class([s.display("flex"), s.justify_content("space-between")]) + |> h.div(attributes, children) } pub fn licenses(attributes, children) { - let id = "licenses" - el.memo_dynamic("div", attributes, children, id, [ - s.display("flex"), - s.gap(px(6)), - ]) + s.class([s.display("flex"), s.gap(px(6))]) + |> h.div(attributes, children) } diff --git a/apps/frontend/src/frontend/view/documentation.gleam b/apps/frontend/src/frontend/view/documentation.gleam index f7515e8..f071971 100644 --- a/apps/frontend/src/frontend/view/documentation.gleam +++ b/apps/frontend/src/frontend/view/documentation.gleam @@ -19,7 +19,7 @@ pub fn view(document: String) { |> string.split("\n") |> list.map(remove_leading_space) |> string.join("\n") - |> converter() + |> converter |> a.attribute("dangerous-unescaped-html", _) h.div([content, a.class("documentation")], []) } diff --git a/apps/frontend/src/frontend/view/footer/footer.gleam b/apps/frontend/src/frontend/view/footer/footer.gleam index db73cb0..dff4a6c 100644 --- a/apps/frontend/src/frontend/view/footer/footer.gleam +++ b/apps/frontend/src/frontend/view/footer/footer.gleam @@ -1,4 +1,4 @@ -import data/model.{type Model} +import data/model.{type Data} import data/msg import frontend/view/footer/links.{links} import frontend/view/footer/styles as s @@ -39,10 +39,10 @@ pub fn view() { ]) } -pub fn search_bar(model: Model) { +pub fn search_bar(model: Data) { use <- bool.guard(when: !model.is_mobile, return: el.none()) h.div([a.class("footer-search")], [ - h.form([e.on_submit(msg.SubmitSearch)], [ + h.form([e.on_submit(msg.UserSubmittedSearch)], [ search_input.view(model.loading, model.input, small: True), ]), ]) diff --git a/apps/frontend/src/frontend/view/footer/styles.gleam b/apps/frontend/src/frontend/view/footer/styles.gleam index 5f8bf7f..1e2b30d 100644 --- a/apps/frontend/src/frontend/view/footer/styles.gleam +++ b/apps/frontend/src/frontend/view/footer/styles.gleam @@ -1,10 +1,10 @@ import frontend/colors/palette import sketch as s -import sketch/lustre/element as l +import sketch/magic/element/html as h import sketch/size.{px} pub fn footer(attributes, children) { - l.memo("footer", attributes, children, [ + s.class([ s.background("var(--sidebar-background)"), s.display("flex"), s.flex_direction("column"), @@ -14,28 +14,27 @@ pub fn footer(attributes, children) { s.margin_top(px(48)), s.grid_area("footer"), ]) + |> h.footer(attributes, children) } pub fn footer_built(attributes, children) { - l.memo("div", attributes, children, [ + s.class([ s.align_items("center"), s.justify_content("center"), - s.font_size(size.rem_(0.8)), + s.font_size(size.rem(0.8)), s.line_height("1.3"), s.text_align("center"), ]) + |> h.div(attributes, children) } pub fn footer_subtitles(attributes, children) { - l.memo("div", attributes, children, [ - s.display("flex"), - s.flex_direction("column"), - s.gap(px(4)), - ]) + s.class([s.display("flex"), s.flex_direction("column"), s.gap(px(4))]) + |> h.div(attributes, children) } pub fn footer_links(attributes, children) { - l.memo("div", attributes, children, [ + s.class([ s.display("grid"), s.grid_template_columns("repeat(3, 1fr)"), s.grid_template_rows("repeat(6, auto)"), @@ -43,29 +42,33 @@ pub fn footer_links(attributes, children) { s.max_width(px(700)), s.width(size.percent(100)), ]) + |> h.div(attributes, children) } pub fn footer_section(attributes, children) { - l.memo("div", attributes, children, [ + s.class([ s.display("grid"), s.grid_template_columns("1fr"), s.grid_template_rows("subgrid"), s.grid_row("1 / 7"), ]) + |> h.div(attributes, children) } pub fn foot_title(attributes, children) { - l.memo("div", attributes, children, [ + s.class([ s.color("var(--input-text-color)"), s.font_weight("500"), s.padding_("6px 0px"), ]) + |> h.div(attributes, children) } pub fn foot_lk(attributes, children) { - l.memo("a", attributes, children, [ - s.font_size(size.rem_(0.9)), + s.class([ + s.font_size(size.rem(0.9)), s.color(palette.dark.dark_white), s.text_decoration("none"), ]) + |> h.a(attributes, children) } diff --git a/apps/frontend/src/frontend/view/navbar/navbar.gleam b/apps/frontend/src/frontend/view/navbar/navbar.gleam index dce12a2..243b4f7 100644 --- a/apps/frontend/src/frontend/view/navbar/navbar.gleam +++ b/apps/frontend/src/frontend/view/navbar/navbar.gleam @@ -1,4 +1,4 @@ -import data/model.{type Model} +import data/model.{type Data} import frontend/router import frontend/view/navbar/styles as s import lustre/attribute as a @@ -11,7 +11,7 @@ fn navbar_links() { ]) } -pub fn navbar(model: Model) { +pub fn navbar(model: Data) { let transparent = model.route == router.Home s.navbar(transparent, [a.class("navbar")], [ case model.route { diff --git a/apps/frontend/src/frontend/view/navbar/styles.gleam b/apps/frontend/src/frontend/view/navbar/styles.gleam index 9a28011..df54e98 100644 --- a/apps/frontend/src/frontend/view/navbar/styles.gleam +++ b/apps/frontend/src/frontend/view/navbar/styles.gleam @@ -1,42 +1,44 @@ -import frontend/colors/palette import frontend/view/body/styles as body_styles -import gleam/bool import sketch as s -import sketch/lustre/element as l +import sketch/magic/element/html as h import sketch/media import sketch/size.{px, vw} pub const search_lucy = body_styles.search_lucy pub fn search_input_wrapper(attributes, children) { - l.memo("form", attributes, children, [s.width_("100%")]) + s.class([s.width_("100%")]) + |> h.form(attributes, children) } pub fn navbar_search_title(attributes, children) { - l.memo("a", attributes, children, [ - s.font_size(size.rem_(1.2)), + s.class([ + s.font_size(size.rem(1.2)), s.compose(body_styles.search_title_()), s.text_decoration("none"), s.cursor("pointer"), ]) + |> h.a(attributes, children) } pub fn nav_links(attributes, children) { - l.memo("div", attributes, children, [ + s.class([ s.display("flex"), s.align_items("baseline"), s.gap(px(48)), s.padding(px(48)), s.media(media.max_width(px(700)), [s.padding(px(12)), s.gap(px(24))]), ]) + |> h.div(attributes, children) } pub fn coming_soon(attributes, children) { - l.memo("span", attributes, children, [s.font_size(size.rem_(0.7))]) + s.class([s.font_size(size.rem(0.7))]) + |> h.span(attributes, children) } pub fn trending(attributes, children) { - l.memo("div", attributes, children, [ + s.class([ s.display("flex"), s.flex_direction("column"), s.gap(px(3)), @@ -45,18 +47,16 @@ pub fn trending(attributes, children) { s.white_space("nowrap"), s.opacity(0.3), ]) + |> h.div(attributes, children) } pub fn nav_link(attributes, children) { - l.memo("a", attributes, children, [ - s.color("var(--text-color)"), - s.text_decoration("none"), - ]) + s.class([s.color("var(--text-color)"), s.text_decoration("none")]) + |> h.a(attributes, children) } pub fn navbar(transparent: Bool, attributes, children) { - let id = "navbar-transparent-" <> bool.to_string(transparent) - l.dynamic("nav", attributes, children, id, [ + s.class([ s.position("sticky"), s.top(px(0)), s.justify_content("end"), @@ -88,18 +88,20 @@ pub fn navbar(transparent: Bool, attributes, children) { s.padding_("18px 24px"), ]), ]) + |> h.nav(attributes, children) } pub fn navbar_search(attributes, children) { - l.memo("div", attributes, children, [ + s.class([ s.display("flex"), s.gap(px(48)), s.align_items("center"), s.flex("1"), s.media(media.max_width(px(700)), [s.gap(px(24))]), ]) + |> h.div(attributes, children) } pub fn title(a, c) { - l.memo("div", a, c, []) + h.div_(a, c) } diff --git a/apps/frontend/src/frontend/view/search_input/search_input.gleam b/apps/frontend/src/frontend/view/search_input/search_input.gleam index 74b5d2d..afdf0e9 100644 --- a/apps/frontend/src/frontend/view/search_input/search_input.gleam +++ b/apps/frontend/src/frontend/view/search_input/search_input.gleam @@ -11,7 +11,7 @@ pub fn view(loading loading: Bool, input input: String, small small: Bool) { s.search_input_content([ a.id("search-input"), a.placeholder("Search for a function, or a type"), - e.on_input(msg.UpdateInput), + e.on_input(msg.UserInputtedSearch), a.value(input), a.attribute("autocorrect", "off"), a.attribute("autocapitalize", "none"), diff --git a/apps/frontend/src/frontend/view/search_input/styles.gleam b/apps/frontend/src/frontend/view/search_input/styles.gleam index a739886..9369423 100644 --- a/apps/frontend/src/frontend/view/search_input/styles.gleam +++ b/apps/frontend/src/frontend/view/search_input/styles.gleam @@ -1,21 +1,20 @@ -import gleam/bool import sketch as s -import sketch/lustre/element as l +import sketch/magic/element/html as h import sketch/media import sketch/size.{px} pub fn search_with_filters(attributes, children) { - l.element("div", attributes, children, [ + s.class([ s.grid_area("input"), s.display("flex"), s.flex_direction("column"), s.gap(px(12)), ]) + |> h.div(attributes, children) } pub fn search_input_wrapper(loading: Bool, children) { - let id = "search-input-wrapper-" <> bool.to_string(loading) - l.dynamic("div", [], children, id, [ + s.class([ s.border_radius(px(12)), s.overflow("hidden"), s.padding( @@ -34,12 +33,11 @@ pub fn search_input_wrapper(loading: Bool, children) { False -> "paused" }), ]) + |> h.div([], children) } pub fn search_input(loading, small, children) { - let id_ = - "search-input-" <> bool.to_string(loading) <> "-" <> bool.to_string(small) - l.dynamic("div", [], children, id_, [ + s.class([ s.display("flex"), s.gap(px(6)), s.border_radius(px(8)), @@ -59,10 +57,11 @@ pub fn search_input(loading, small, children) { }), ), ]) + |> h.div([], children) } pub fn search_input_content(attributes) { - l.element("input", attributes, [], [ + s.class([ s.appearance("none"), s.border("none"), s.outline("none"), @@ -71,10 +70,11 @@ pub fn search_input_content(attributes) { s.background("transparent"), s.color("inherit"), ]) + |> h.input(attributes) } pub fn shortcut_hint(attrs, children) { - l.element("div", attrs, children, [ + s.class([ s.white_space("nowrap"), s.font_size(px(11)), s.border("1px solid var(--text-color)"), @@ -83,4 +83,5 @@ pub fn shortcut_hint(attrs, children) { s.opacity(0.4), s.media(media.max_width(px(700)), [s.display("none")]), ]) + |> h.div(attrs, children) } diff --git a/apps/frontend/src/gleam/coerce.gleam b/apps/frontend/src/gleam/coerce.gleam deleted file mode 100644 index 3f1da9e..0000000 --- a/apps/frontend/src/gleam/coerce.gleam +++ /dev/null @@ -1,2 +0,0 @@ -@external(javascript, "../gloogle.ffi.mjs", "coerce") -pub fn coerce(value: a) -> b diff --git a/apps/frontend/src/gloogle.ffi.mjs b/apps/frontend/src/gloogle.ffi.mjs index 9a6644c..bffca58 100644 --- a/apps/frontend/src/gloogle.ffi.mjs +++ b/apps/frontend/src/gloogle.ffi.mjs @@ -1,4 +1,6 @@ -function findChild(shadowRoot, id) { +import * as gleam from './gleam.mjs' + +function findSearchResultChild(shadowRoot, id) { for (const node of shadowRoot.querySelectorAll('search-result')) { const elem = node.shadowRoot.getElementById(id) if (elem) return elem @@ -6,29 +8,25 @@ function findChild(shadowRoot, id) { } export function scrollTo(id) { - return function (_) { - const cache = document.getElementsByTagName('lazy-node') - if (!cache?.[0]) return - const elem = findChild(cache[0].shadowRoot, id) - if (!elem) return - const elemRect = elem.getBoundingClientRect() - const navbarRect = document - .getElementsByClassName('navbar')?.[0] - ?.getBoundingClientRect() - const bodyRect = document.body.getBoundingClientRect() - const offset = elemRect.top - bodyRect.top - (navbarRect?.height ?? 0) - 12 - window.scrollTo({ top: offset, behavior: 'smooth' }) - } + const cache = document.getElementsByTagName('lazy-node') + if (!cache?.[0]) return + const elem = findSearchResultChild(cache[0].shadowRoot, id) + if (!elem) return + const elemRect = elem.getBoundingClientRect() + const navbarRect = document + .getElementsByClassName('navbar')?.[0] + ?.getBoundingClientRect() + const bodyRect = document.body.getBoundingClientRect() + const top = elemRect.top - bodyRect.top - (navbarRect?.height ?? 0) - 12 + window.scrollTo({ top, behavior: 'smooth' }) } export function captureMessage(content) { - if (is_dev()) return content - if ( - typeof Sentry !== 'undefined' && - Sentry?.captureMessage && - typeof Sentry.captureMessage === 'function' - ) - Sentry.captureMessage(content) + const isDev = !!import.meta.env.DEV + if (isDev) return content + const isSentryDefined = typeof Sentry !== 'undefined' + const canCaptureMessage = Sentry?.captureMessage === 'function' + if (isSentryDefined && canCaptureMessage) Sentry.captureMessage(content) return content } @@ -66,7 +64,7 @@ export function focus(id, event) { } } -export function unfocus() { +export function blur() { const element = document.activeElement if (element) { element.blur() @@ -78,11 +76,12 @@ export function isMobile() { } export function subscribeIsMobile(callback) { - window.matchMedia('(max-width: 700px)').addEventListener('change', event => { - if (event.matches) { - callback(true) - } else { - callback(false) - } - }) + window + .matchMedia('(max-width: 700px)') + .addEventListener('change', event => callback(!!event.matches)) +} + +export function eventKey(event) { + if (event.key) return new gleam.Ok(event.key) + return new gleam.Error() } diff --git a/apps/frontend/src/lustre/effect/extra.gleam b/apps/frontend/src/lustre/effect/extra.gleam index 7203431..6cff119 100644 --- a/apps/frontend/src/lustre/effect/extra.gleam +++ b/apps/frontend/src/lustre/effect/extra.gleam @@ -2,16 +2,6 @@ import gleam/dynamic.{type Dynamic} import gleam/javascript/promise.{type Promise} import lustre/effect.{type Effect} -pub fn then(effect: Effect(a), next: fn(a) -> Effect(b)) -> Effect(b) { - // This is necessary because `perform` needs an implementation for - // `effect.emit` but we can't handle that. If you're not using components - // this will never come up. - let dummy_emit = fn(_, _) { Nil } - use dispatch <- effect.from() - let run_next = fn(a) { effect.perform(next(a), dispatch, dummy_emit) } - effect.perform(effect, run_next, dummy_emit) -} - pub fn from_promise(promise promise: Promise(a)) { use dispatch <- effect.from() promise.tap(promise, fn(content) { dispatch(content) }) diff --git a/apps/frontend/src/lustre/update.gleam b/apps/frontend/src/lustre/update.gleam deleted file mode 100644 index 27ecdb2..0000000 --- a/apps/frontend/src/lustre/update.gleam +++ /dev/null @@ -1,18 +0,0 @@ -import lustre/effect.{type Effect} - -pub fn none(model: model) { - #(model, effect.none()) -} - -pub fn effect(model: model, effect: Effect(msg)) { - #(model, effect) -} - -pub fn effects(model: model, effects: List(Effect(msg))) { - #(model, effect.batch(effects)) -} - -pub fn add_effect(tuple: #(model, Effect(msg)), effect: Effect(msg)) { - let #(model, fst_effect) = tuple - #(model, effect.batch([fst_effect, effect])) -} diff --git a/apps/frontend/src/stylesheets/all.css b/apps/frontend/src/stylesheets/all.css index 36e4a1a..608ba60 100644 --- a/apps/frontend/src/stylesheets/all.css +++ b/apps/frontend/src/stylesheets/all.css @@ -114,7 +114,6 @@ lazy-node:has(:not(:defined)) { .qualified-name { overflow: hidden; text-overflow: ellipsis; - direction: rtl; text-decoration: none; } @@ -335,7 +334,6 @@ lazy-node:has(:not(:defined)) { .sidebar-module-name { text-overflow: ellipsis; overflow: hidden; - direction: rtl; text-align: left; cursor: pointer; appearance: none; diff --git a/apps/frontend/src/toast/error.gleam b/apps/frontend/src/toast/error.gleam index 3a5db81..dc5bb74 100644 --- a/apps/frontend/src/toast/error.gleam +++ b/apps/frontend/src/toast/error.gleam @@ -1,50 +1,18 @@ -import gleam/dynamic -import gleam/int -import gleam/json -import gleam/list +import frontend/discuss +import gleam/io import gleam/option.{Some} -import gleam/string -import lustre_http as http -pub fn describe_json_error(error: json.DecodeError) { +pub fn describe_http_error(error: discuss.DiscussError) { case error { - json.UnexpectedEndOfInput -> - "Impossible to parse the response. Unexpected end of input." - json.UnexpectedByte(byte, position) -> { - "Impossible to parse the response. Unexpected byte. " - <> { byte <> " at position " <> int.to_string(position) } + discuss.InternalServerError -> + Some("Internal server error. Please try again later.") + discuss.NetworkError -> Some("Network error. Please try again later.") + discuss.NotFound -> + Some("Resource not found. Make sure you have the correct URL.") + discuss.InvalidJsonBody -> Some("Invalid JSON body. Please, retry later.") + discuss.DecodeError(error) -> { + io.debug(error) + Some("Format error. Please try again later.") } - json.UnexpectedSequence(byte, position) -> { - "Impossible to parse the response. Unexpected sequence. " - <> { byte <> " at position " <> int.to_string(position) } - } - json.UnexpectedFormat(errors) -> { - string.append("Impossible to parse the response. Unexpected format. ", { - errors - |> list.map(fn(error) { - let dynamic.DecodeError(expected, found, path) = error - let expected = "Expected value: " <> expected - let found = "Found: " <> found - let path = "Path: " <> string.join(path, ".") - expected <> ". " <> found <> ". " <> path <> "." - }) - |> string.join("\n") - }) - } - } - |> Some -} - -pub fn describe_http_error(error: http.HttpError) { - case error { - http.BadUrl(url) -> Some("Bad URL: " <> url) - http.InternalServerError(error) -> - Some({ "Internal server error. Please try again later. " <> error }) - http.JsonError(error) -> describe_json_error(error) - http.NetworkError -> Some("Network error. Please try again later.") - http.NotFound -> Some("Resource not found. Please try again later.") - http.OtherError(error_code, description) -> - Some("Error " <> int.to_string(error_code) <> ". " <> description) - http.Unauthorized -> Some("Operation unauthorized.") } } diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json deleted file mode 100644 index e96e887..0000000 --- a/apps/frontend/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "plugins": [{ "name": "ts-gleam" }], - "allowArbitraryExtensions": true - }, - "include": ["src"] -} diff --git a/apps/frontend/vite.config.js b/apps/frontend/vite.config.js index fedf98b..14b50eb 100644 --- a/apps/frontend/vite.config.js +++ b/apps/frontend/vite.config.js @@ -1,5 +1,4 @@ import { sentryVitePlugin } from '@sentry/vite-plugin' -import 'dotenv/config' import { defineConfig } from 'vite' import gleam from 'vite-gleam' diff --git a/erlang_ls.config b/erlang_ls.config new file mode 100644 index 0000000..ac4b8fd --- /dev/null +++ b/erlang_ls.config @@ -0,0 +1,8 @@ +app_dirs: + - "packages/*" + - "apps/*" +include_dirs: + - "packages" + - "apps" + - "packages/*/build/dev/erlang" + - "apps/*/build/dev/erlang" diff --git a/packages/bright/.github/workflows/demo.yml b/packages/bright/.github/workflows/demo.yml new file mode 100644 index 0000000..af1a259 --- /dev/null +++ b/packages/bright/.github/workflows/demo.yml @@ -0,0 +1,54 @@ +name: Deploy demo + +on: + push: + branches: [main] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup BEAM + uses: erlef/setup-beam@v1 + with: + otp-version: "27.0.0" + gleam-version: "1.6.1" + rebar3-version: "3" + # elixir-version: "1.15.4" + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Downloading dependencies + run: gleam deps download + working-directory: e2e/sample + - name: Build demo + run: gleam run -m lustre/dev build + working-directory: e2e/sample + - name: Copy files + run: | + mkdir _site + cp -r e2e/sample/priv _site + cp e2e/sample/index.html _site + - name: Upload artifacts + uses: actions/upload-pages-artifact@v3 + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/packages/tom/.github/workflows/test.yml b/packages/bright/.github/workflows/test.yml similarity index 77% rename from packages/tom/.github/workflows/test.yml rename to packages/bright/.github/workflows/test.yml index cf2096e..6026a40 100644 --- a/packages/tom/.github/workflows/test.yml +++ b/packages/bright/.github/workflows/test.yml @@ -11,11 +11,11 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 with: - otp-version: "26.0.2" - gleam-version: "0.32.4" + otp-version: "27.1.2" + gleam-version: "1.6.1" rebar3-version: "3" # elixir-version: "1.15.4" - run: gleam deps download diff --git a/packages/bright/.gitignore b/packages/bright/.gitignore new file mode 100644 index 0000000..93d449b --- /dev/null +++ b/packages/bright/.gitignore @@ -0,0 +1,7 @@ +*.beam +*.ez +/build +erl_crash.dump +sample.mjs +/index.html +.DS_Store diff --git a/packages/bright/.prettierrc.json b/packages/bright/.prettierrc.json new file mode 100644 index 0000000..cce9d3c --- /dev/null +++ b/packages/bright/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "semi": false +} diff --git a/packages/bright/LICENCE b/packages/bright/LICENCE new file mode 100644 index 0000000..62fede3 --- /dev/null +++ b/packages/bright/LICENCE @@ -0,0 +1,7 @@ +Copyright 2024 Guillaume Hivert + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/bright/README.md b/packages/bright/README.md new file mode 100644 index 0000000..b12db53 --- /dev/null +++ b/packages/bright/README.md @@ -0,0 +1,272 @@ +# Bright + +Bright is a library to help you manage your `model` and your `update` function in +a [Lustre](https://lustre.build) application. As you probably know, in Lustre, +your model is the only mutable place of the application, and centralize every +data your application uses. If you're coming from the JS world, it can seems +weird at first, because it's usual to have multiple storage places in an +application, whether they're contexts, stores, observables, or anything else. + +In such a centralized model, everything is simpler, and just works. However, +managing your model and your updates can quickly become a mess, with dependent +data, data splitting, normalization, etc. Bright comes in to help you avoid such +states, by both helping your to maintain a properly defined model, but also +to define dependant data in an easy way. Bright also provides a powerful caching +system to guarantee to not compute the same information twice! + +> As usual, a demo is worth a thousand words, so take a look at +> [https://bright.chouquette.dev](https://bright.chouquette.dev)! + +If you're used to stores & data management, you can skip the next section, see +you in the [getting started](#getting-started)! + +## Installation + +```sh +gleam add bright@1 +``` + +## Dependent data? Caching system? + +To sum up simply, a data is dependent on another data when the latter data is +required to compute the former. If you have worked with relational data, you +already encountered it. Let's take an example. + +Imagine you have a list of users on one side, with their info (name, age for example) +and ID, and you have a list of addresses referring to a user by their ID. You want to +create a page displaying the user info and all of their addresses. +Intuitively, you would display the user info, and then you would find all the user +addresses in the address list to display them. + +```gleam +pub type User { + User( + name: String, + age: Int, + id: String + ) +} + +pub type Address { + Address( + street: String, + city: String, + country: String, + user_id: String + ) +} + +pub fn display_user_page(model, user_id) { + // First find the user. + use user <- result.try(list.find(model.users, fn (user) { user.id == user_id })) + // After the user has been found, filter the addresses to find them. + use addresses <- list.filter(model.addresses, fn (address) { address.user_id == user.id }) + // Do the display here. +} +``` + +While it works perfectly in this example, what if every time you need to access +those data? Recompute the same data over and over again, in every part of the +application? We can do better: define the user and its address in one record, +and use it everywhere! + +```gleam +pub type UserAndAddress { + UserAndAddress( + user: User, + addresses: List(Address) + ) +} + +// Compute the data, and store it in your model. +pub fn compute_user_address(model) { + list.map(model.users, fn (user) { + let addresses = list.filter(model.addresses, fn (address) { address.user_id == user.id }) + UserAndAddresses(user:, addresses:) + }) +} + +// The data is in your model. +pub fn display_user_page(model, user_id) { + // Find directly everything! + use UserAndAddress(user:, addresses:) <- result.try({ + list.find(model.users_and_addresses, fn (user) { user.id == user_id }) + }) + // Do the display here. +} +``` + +You have defined here a derived, computed data that depends on two previous data +you own. Now, every time you need to access the user, you have the address bundled! +But in a classic application, you would have to define that computation by yourself, +and make sure to keep it in sync after each update. That's where Bright comes in, +and do the hard work for you! Instead of having to think to synchronize the data +when a new data comes in, let Bright does it for you. And with built-in caching +on-demand, Bright will never recompute the same data twice for intense computations. + +## Getting Started + +Bright handles the hard task of computing the derived data when needed, and as +such, you have to initialize it at first. Bright accepts two types of data: +your main model, holding the raw data, and your derived, computed data. Because +Gleam is strongly-typed language, you'll have to define those data by hand. +A counter will be used to illustrate how to use it. + +```gleam +/// Define your raw data. No derived data will reside here. +pub type Data { + Data(counter: Int) +} + +/// Define your derived data. No raw data will reside here. +pub type Computed { + Computed(double: Int) +} + +/// Define an alias, to simplify reference to the model. +pub type Model = + Bright(Data, Computed) + +// In your init function, you'll initialize Bright. You can use it as-is as a +// replacement for your model. +pub fn init() { + let data = Data(counter: 0) + let computed = Computed(double: 0) + let model = bright.init(data, computed) + // bright.return is a helper to write nice little DSL. + bright.return(model) +} +``` + +Once Bright is initialized, you have to modify a bit your update function. Now, +instead of simply receiving the message and modifying your model, you'll have to +run that modification through Bright. You can then chain your computation calls +to the Bright object. And of course, derived data can be computed from pre-computed +derived data! + +```gleam +pub type Msg { + Increment + Decrement +} + +pub fn update(model: Model, msg: Msg) { + // By using function capture, we can easily use our update function here. + // bright.update will automatically run your update against data, here our + // Data record. Like every update function, that function have to return + // a #(Data, Effect(Msg)). The message will automatically be batched with + // next messages. + // Finally, Bright(Data, Computed) is returned, with Data updated. To let you + // continue the chain. + use model <- bright.update(model, update_data(_, msg)) + model + // bright.compute will compute the new derived data, and let you set it in + // the computed. You can also simply return the original computed, in which + // case the data is not updated. + |> bright.compute(fn(data, computed) { Computed(..computed, double: d.counter * 2) }) + // bright.lazy_compute will compute the new derived data, if and only if the + // selector you pass as the first argument changed between two renders. + // In case the selector did not change, the old data is kept in memory for the + // next render. + |> bright.lazy_compute( + // That selector value is compared at every render. + fn (data) { data.counter / 10 }, + fn(data, computed) { Computed(..computed, double: d.counter * 2) } + ) +} + +pub fn update_data(data: Data, msg: Msg) { + case msg { + Increment -> #(Data(..data, counter: data.counter + 1), effect.none()) + Decrement -> #(Data(..data, counter: data.counter - 1), effect.none()) + } +} +``` + +And once your data is computed, all you have to do is to run through your view +function! + +```gleam +pub fn view(model: Model) { + use data, computed <- bright.view(model) + // You can use data & computed with correct, up to date data. + html.div([], []) +} +``` + +And you're good to go! Now, you don't have to think anymore to update your +derived data, everything is kept in-sync directly for you! + +## Using guards + +Sometimes, you also have to define side-effects that run after your computations +have run. Because you figure out the data is finally incorrect. Or because your +user have written a false URL in the address bar. Bright got you covered too! +Just use `bright.guard`, and let the side-effects flow automatically in your app, +only when you need it! + +```gleam +pub fn update(model: Model, msg: Msg) { + use model <- bright.update(model, update_data(_, msg)) + model + |> bright.compute(fn(data, computed) { Computed(..computed, double: d.counter * 2) }) + |> bright.lazy_compute( + fn (data) { data.counter / 10 }, + fn(data, computed) { Computed(..computed, double: d.counter * 2) } + ) + // bright.guard will run at every render, and let you the possibility to issue + // a side-effect. Bright will take care to gather them, and provide them to the + // runtime! + |> bright.guard(fn (data, computed) { + effect.from(fn (dispatch) { + io.println("That side-effect will run at every render!") + }) + }) + // bright.lazy_guard will issue the side-effect, if and only if the + // selector you pass as the first argument changed between two renders. + // In case the selector did not change, the old data is kept in memory for the + // next render. + |> bright.lazy_guard( + fn (data) { data.counter / 10 }, + fn (data, computed) { + effect.from(fn (dispatch) { + io.println("That side-effect will only run when the selector changes!") + }) + } + ) +} +``` + +## Combining multiple Bright + +Sometimes, you also need to combine multiple Bright in the same model. While you +can keep a `Bright` as model, you could want to combine them, to handle one `Bright` +by page for example. `bright.step` helps you to do this. + +```gleam +pub type Model { + Model( + counter_1: Bright(Data, Computed), + counter_2: Bright(Data, Computed), + ) +} + +pub type Msg { + First(counter: Counter) + Second(counter: Counter) +} + +pub type Counter { + Decrement + Increment +} + +/// Here, we define a new update function, that calls our previously defined +/// update function. It keeps the two Bright synchronized by running the full +/// updated cycle on each of them. +fn update_both_counters(model: Model, msg: Msg) { + use counter_1 <- bright.step(update(model.counter_1, msg.counter)) + use counter_2 <- bright.step(update(model.counter_2, msg.counter)) + bright.return(Model(..model, counter_1:, counter_2:)) +} +``` diff --git a/packages/tom/.gitignore b/packages/bright/e2e/sample/.gitignore similarity index 79% rename from packages/tom/.gitignore rename to packages/bright/e2e/sample/.gitignore index 170cca9..599be4e 100644 --- a/packages/tom/.gitignore +++ b/packages/bright/e2e/sample/.gitignore @@ -1,4 +1,4 @@ *.beam *.ez -build +/build erl_crash.dump diff --git a/packages/lustre/examples/99-full-stack-applications/README.md b/packages/bright/e2e/sample/README.md similarity index 50% rename from packages/lustre/examples/99-full-stack-applications/README.md rename to packages/bright/e2e/sample/README.md index a45e7a9..27a5d5c 100644 --- a/packages/lustre/examples/99-full-stack-applications/README.md +++ b/packages/bright/e2e/sample/README.md @@ -1,25 +1,24 @@ -# app +# sample -[![Package Version](https://img.shields.io/hexpm/v/app)](https://hex.pm/packages/app) -[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/app/) +[![Package Version](https://img.shields.io/hexpm/v/sample)](https://hex.pm/packages/sample) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/sample/) ```sh -gleam add app +gleam add sample@1 ``` ```gleam -import app +import sample pub fn main() { // TODO: An example of the project in use } ``` -Further documentation can be found at . +Further documentation can be found at . ## Development ```sh gleam run # Run the project gleam test # Run the tests -gleam shell # Run an Erlang shell ``` diff --git a/packages/lustre/examples/99-full-stack-applications/client/gleam.toml b/packages/bright/e2e/sample/gleam.toml similarity index 56% rename from packages/lustre/examples/99-full-stack-applications/client/gleam.toml rename to packages/bright/e2e/sample/gleam.toml index 9025873..0419f3b 100644 --- a/packages/lustre/examples/99-full-stack-applications/client/gleam.toml +++ b/packages/bright/e2e/sample/gleam.toml @@ -1,4 +1,4 @@ -name = "app" +name = "sample" version = "1.0.0" # Fill out these fields if you intend to generate HTML documentation or publish @@ -6,19 +6,19 @@ version = "1.0.0" # # description = "" # licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] # # For a full reference of all the available options, you can have a look at # https://gleam.run/writing-gleam/gleam-toml/. [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" -lustre = ">= 4.2.4 and < 5.0.0" -lustre_http = ">= 0.5.2 and < 1.0.0" -gleam_json = ">= 1.0.1 and < 2.0.0" -decipher = ">= 1.2.0 and < 2.0.0" +lustre = ">= 4.6.1 and < 5.0.0" +bright = {path = "../.."} +sketch = ">= 3.1.1 and < 4.0.0" +sketch_lustre = ">= 1.0.3 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" -lustre_dev_tools = ">= 1.3.2 and < 2.0.0" +lustre_dev_tools = ">= 1.6.0 and < 2.0.0" diff --git a/packages/bright/e2e/sample/index.html b/packages/bright/e2e/sample/index.html new file mode 100644 index 0000000..7d7baf8 --- /dev/null +++ b/packages/bright/e2e/sample/index.html @@ -0,0 +1,18 @@ + + + + + + + Bright 💡 + + + + + + + +
+
+ + diff --git a/packages/bright/e2e/sample/manifest.toml b/packages/bright/e2e/sample/manifest.toml new file mode 100644 index 0000000..37b2c4f --- /dev/null +++ b/packages/bright/e2e/sample/manifest.toml @@ -0,0 +1,58 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "bright", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre", "lustre_dev_tools"], source = "local", path = "../.." }, + { name = "conversation", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "conversation", source = "hex", outer_checksum = "908B46F60444442785A495197D482558AD8B849C3714A38FAA1940358CC8CCCD" }, + { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, + { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, + { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, + { name = "gleam_community_ansi", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "4CD513FC62523053E62ED7BAC2F36136EC17D6A8942728250A9A00A15E340E4B" }, + { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, + { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, + { name = "gleam_erlang", version = "0.30.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "760618870AE4A497B10C73548E6E44F43B76292A54F0207B3771CBB599C675B4" }, + { name = "gleam_http", version = "3.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "A9EE0722106FCCAB8AD3BF9D0A3EFF92BFE8561D59B83BAE96EB0BE1938D4E0F" }, + { name = "gleam_httpc", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "091CDD2BEC8092E82707BEA03FB5205A2BBBDE4A2F551E3C069E13B8BC0C428E" }, + { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" }, + { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, + { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, + { name = "gleam_package_interface", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "CF3BFC5D0997750D9550D8D73A90F4B8D71C6C081B20ED4E70FFBE1E99AFC3C2" }, + { name = "gleam_stdlib", version = "0.44.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "A6E55E309A6778206AAD4038D9C49E15DF71027A1DB13C6ADA06BFDB6CF1260E" }, + { name = "glearray", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "B99767A9BC63EF9CC8809F66C7276042E5EFEACAA5B25188B552D3691B91AC6D" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glint", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5F6720081150AED8023131B0F3A35F9B0D6426A96CE02BEC52AD7018DF70566A" }, + { name = "glisten", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "48EF7F6D1DCA877C2F49AF35CC33946C7129EEB05A114758A2CC569C708BFAF8" }, + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "lustre", version = "4.6.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "BDF833368F6C8F152F948D5B6A79866E9881CB80CB66C0685B3327E7DCBFB12F" }, + { name = "lustre_dev_tools", version = "1.6.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "5A1C7D20FA2C0D77D59F259EAE0E14BB3F5359CC1DE7C5ED6922B65FFCBD4C31" }, + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, + { name = "mist", version = "2.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "981F12FC8BA0656B40099EC876D6F2BEE7B95593610F342E9AB0DC4E663A932F" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, + { name = "plinth", version = "0.5.2", build_tools = ["gleam"], requirements = ["conversation", "gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "6F346577D02879D5D516C61EC7C41CD09642528072BA04E1DBD36B9415A82517" }, + { name = "ranger", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "B8F3AFF23A3A5B5D9526B8D18E7C43A7DFD3902B151B97EC65397FE29192B695" }, + { name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" }, + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, + { name = "sketch", version = "3.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "sketch", source = "hex", outer_checksum = "6CBFAAA92C37F1F44FC552FD9E9DAC34598BDEB5F873B6191C696DC67D85AD00" }, + { name = "sketch_lustre", version = "1.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre", "plinth", "sketch"], otp_app = "sketch_lustre", source = "hex", outer_checksum = "DD5437B10D4BB8AB45A19820B17883188B8568B6ED7885D7D073A983F4984E79" }, + { name = "snag", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "08E9EB87C413457DB1DD66CD704C6878DACC9C93B418600F63873D0CD224E756" }, + { name = "spinner", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "9EE43AA33BE2DA5731B8F3F170AAB59AF1A815AFA5BF615F12C1B91F3B03F157" }, + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, + { name = "tom", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "228E667239504B57AD05EC3C332C930391592F6C974D0EFECF32FFD0F3629A27" }, + { name = "wisp", version = "1.2.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "F71265D2F1DE11426535A2FA1DA3B11D2FFB783B116DF9496BC8C41983EBADB4" }, +] + +[requirements] +bright = { path = "../.." } +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 4.6.1 and < 5.0.0" } +lustre_dev_tools = { version = ">= 1.6.0 and < 2.0.0" } +sketch = { version = ">= 3.1.1 and < 4.0.0" } +sketch_lustre = { version = ">= 1.0.3 and < 2.0.0" } diff --git a/packages/bright/e2e/sample/src/icons.gleam b/packages/bright/e2e/sample/src/icons.gleam new file mode 100644 index 0000000..597150e --- /dev/null +++ b/packages/bright/e2e/sample/src/icons.gleam @@ -0,0 +1,28 @@ +import icons/book_open +import icons/check +import icons/copy +import icons/github +import icons/home +import sketch as s +import sketch/lustre/element/html as h +import sketch/size.{px} + +pub fn small(icon) { + s.class([s.width(px(24)), s.height(px(24))]) + |> h.div([], [icon]) +} + +pub fn tiny(icon) { + s.class([s.width(px(12)), s.height(px(12))]) + |> h.div([], [icon]) +} + +pub const book_open = book_open.icon + +pub const check = check.icon + +pub const copy = copy.icon + +pub const github = github.icon + +pub const home = home.icon diff --git a/packages/bright/e2e/sample/src/icons/book_open.gleam b/packages/bright/e2e/sample/src/icons/book_open.gleam new file mode 100644 index 0000000..a518c4d --- /dev/null +++ b/packages/bright/e2e/sample/src/icons/book_open.gleam @@ -0,0 +1,17 @@ +import lustre/attribute as a +import sketch/lustre/element/html + +const content = "" + +pub fn icon() { + html.svg_( + [ + a.style([#("max-width", "100%"), #("max-height", "100%")]), + a.attribute("xmlns", "http://www.w3.org/2000/svg"), + a.attribute("viewBox", "0 0 256 256"), + a.attribute("fill", "currentColor"), + a.attribute("dangerous-unescaped-html", content), + ], + [], + ) +} diff --git a/packages/bright/e2e/sample/src/icons/check.gleam b/packages/bright/e2e/sample/src/icons/check.gleam new file mode 100644 index 0000000..d8aadbf --- /dev/null +++ b/packages/bright/e2e/sample/src/icons/check.gleam @@ -0,0 +1,17 @@ +import lustre/attribute as a +import sketch/lustre/element/html + +const content = "" + +pub fn icon() { + html.svg_( + [ + a.style([#("max-width", "100%"), #("max-height", "100%")]), + a.attribute("xmlns", "http://www.w3.org/2000/svg"), + a.attribute("viewBox", "0 0 256 256"), + a.attribute("fill", "currentColor"), + a.attribute("dangerous-unescaped-html", content), + ], + [], + ) +} diff --git a/packages/bright/e2e/sample/src/icons/copy.gleam b/packages/bright/e2e/sample/src/icons/copy.gleam new file mode 100644 index 0000000..cfc9ffc --- /dev/null +++ b/packages/bright/e2e/sample/src/icons/copy.gleam @@ -0,0 +1,17 @@ +import lustre/attribute as a +import sketch/lustre/element/html + +const content = "" + +pub fn icon() { + html.svg_( + [ + a.style([#("max-width", "100%"), #("max-height", "100%")]), + a.attribute("xmlns", "http://www.w3.org/2000/svg"), + a.attribute("viewBox", "0 0 256 256"), + a.attribute("fill", "currentColor"), + a.attribute("dangerous-unescaped-html", content), + ], + [], + ) +} diff --git a/packages/bright/e2e/sample/src/icons/github.gleam b/packages/bright/e2e/sample/src/icons/github.gleam new file mode 100644 index 0000000..db6b326 --- /dev/null +++ b/packages/bright/e2e/sample/src/icons/github.gleam @@ -0,0 +1,17 @@ +import lustre/attribute as a +import sketch/lustre/element/html + +const content = "" + +pub fn icon() { + html.svg_( + [ + a.style([#("max-width", "100%"), #("max-height", "100%")]), + a.attribute("xmlns", "http://www.w3.org/2000/svg"), + a.attribute("viewBox", "0 0 256 256"), + a.attribute("fill", "currentColor"), + a.attribute("dangerous-unescaped-html", content), + ], + [], + ) +} diff --git a/packages/bright/e2e/sample/src/icons/home.gleam b/packages/bright/e2e/sample/src/icons/home.gleam new file mode 100644 index 0000000..2869405 --- /dev/null +++ b/packages/bright/e2e/sample/src/icons/home.gleam @@ -0,0 +1,17 @@ +import lustre/attribute as a +import sketch/lustre/element/html + +const content = "" + +pub fn icon() { + html.svg_( + [ + a.style([#("max-width", "100%"), #("max-height", "100%")]), + a.attribute("xmlns", "http://www.w3.org/2000/svg"), + a.attribute("viewBox", "0 0 256 256"), + a.attribute("fill", "currentColor"), + a.attribute("dangerous-unescaped-html", content), + ], + [], + ) +} diff --git a/packages/bright/e2e/sample/src/sample.ffi.mjs b/packages/bright/e2e/sample/src/sample.ffi.mjs new file mode 100644 index 0000000..27bfa91 --- /dev/null +++ b/packages/bright/e2e/sample/src/sample.ffi.mjs @@ -0,0 +1,3 @@ +export function dateNow() { + return Date.now() +} diff --git a/packages/bright/e2e/sample/src/sample.gleam b/packages/bright/e2e/sample/src/sample.gleam new file mode 100644 index 0000000..915c290 --- /dev/null +++ b/packages/bright/e2e/sample/src/sample.gleam @@ -0,0 +1,235 @@ +import bright.{type Bright} +import gleam/bool +import gleam/int +import gleam/io +import gleam/pair +import gleam/result +import gleam/string +import lustre +import lustre/effect +import lustre/event as e +import sketch +import sketch/lustre as sketch_ +import sketch/lustre/element +import sketch/lustre/element/html as h +import sketch/size.{px} +import styles + +@external(javascript, "./sample.ffi.mjs", "dateNow") +fn now() -> Int { + 0 +} + +pub type Data { + Data(counter: Int) +} + +pub type Computed { + Computed(double: Int, triple: Int, memoized: Int, last_lazy: Int) +} + +pub type Model { + Model( + node: String, + counter_1: Bright(Data, Computed), + counter_2: Bright(Data, Computed), + ) +} + +pub type Msg { + First(counter: Counter) + Second(counter: Counter) +} + +pub type Counter { + Decrement + Increment +} + +/// It's possible to switch between `update_both` and `update_one` +/// to see how it works actually. +pub fn main() { + let assert Ok(cache) = sketch.cache(strategy: sketch.Ephemeral) + use _ <- result.try(start(cache, update_one, "#single")) + use _ <- result.try(start(cache, update_both, "#double")) + Ok(Nil) +} + +fn start(cache, update, node) { + let view = sketch_.compose(sketch_.node(), view, cache) + lustre.application(init, update, view) + |> lustre.start(node, node) +} + +fn init(node: String) { + let data = Data(counter: 0) + let computed = Computed(double: 0, triple: 0, memoized: 0, last_lazy: 0) + let counter = bright.init(data, computed) + bright.return(Model(node:, counter_1: counter, counter_2: counter)) +} + +/// Here, update both fields in `Model` with the Counter message. +/// Both counters are synchronized, both exebright the full lifecycle +/// and both side-effects run as desired. +fn update_both(model: Model, msg: Msg) { + use counter_1 <- bright.step(update(model.counter_1, msg.counter)) + use counter_2 <- bright.step(update(model.counter_2, msg.counter)) + bright.return(Model(..model, counter_1:, counter_2:)) +} + +/// Here, update only one field, according to the main message. +/// The other message is not updated. +fn update_one(model: Model, msg: Msg) { + let #(data, msg_) = select_data_structure(model, msg) + use counter <- bright.step(update(data, msg_)) + case msg { + First(..) -> bright.return(Model(..model, counter_1: counter)) + Second(..) -> bright.return(Model(..model, counter_2: counter)) + } +} + +fn select_data_structure(model: Model, msg: Msg) { + case msg { + First(counter) -> #(model.counter_1, counter) + Second(counter) -> #(model.counter_2, counter) + } +} + +/// Execute the full lifecycle, with derived data, and lazy computations. +fn update(model: Bright(Data, Computed), msg: Counter) { + use model <- bright.update(model, update_data(_, msg)) + model + |> bright.compute(fn(d, c) { Computed(..c, double: d.counter * 2) }) + |> bright.compute(fn(d, c) { Computed(..c, triple: d.counter * 3) }) + |> bright.lazy_compute(fn(d) { d.counter / 10 }, compute_memoized) + |> bright.guard(warn_on_three) + |> bright.guard(warn_on_three_multiple) + |> bright.lazy_guard(fn(d) { d.counter / 10 }, warn) +} + +/// Raw update. +fn update_data(model: Data, msg: Counter) { + case msg { + Decrement -> Data(counter: model.counter - 1) + Increment -> Data(counter: model.counter + 1) + } + |> pair.new(effect.none()) +} + +fn view(model: Model) { + element.fragment([ + navbar(model), + styles.body([], [ + introduction(model.node), + explanations(model.node), + styles.container([], [ + counter(model.counter_1) |> element.map(First), + counter(model.counter_2) |> element.map(Second), + ]), + ]), + case model.node { + "#double" -> element.none() + _ -> styles.footer([], [h.text("Made with 💜 at Chou Corp.")]) + }, + ]) +} + +fn introduction(node) { + case node { + "#single" -> element.none() + _ -> + h.div(styles.intro(), [], [ + h.text("Bright is a Lustre's model & update management "), + h.text("package. While your model is the only mutable "), + h.text("place in your application, you can store almost "), + h.text("everything you want inside. Bright provides an "), + h.text("abstraction layer on top of Lustre's model, "), + h.text("and add the ability to derive some data from "), + h.text("your raw data, add some caching for extensive "), + h.text("computations, and protects you from some "), + h.text("invalid state that could come in sometimes."), + ]) + } +} + +fn explanations(node) { + case node { + "#single" -> + sketch.class([sketch.compose(styles.intro()), sketch.margin_top(px(60))]) + |> h.div([], [ + styles.title("Dissociated counters"), + h.text("That second example illustrates the ability to run two "), + h.text("Bright counters in the same application, dissociated with "), + h.text("each other. They both contains two computed, derived "), + h.text("data, and one lazy data, computed every time the result "), + h.text("of counter / 10 changes. But that time, when you change "), + h.text("one, the other will stay the same. You can see the data "), + h.text("and computations will not happen again. Open your "), + h.text("console, and watch the side-effects running!"), + ]) + _ -> + h.div(styles.intro(), [], [ + styles.title("Synchronized counters"), + h.text("That first example illustrates the ability to run two "), + h.text("Bright counters in the same application, synchronized "), + h.text("with each other. They both contains two computed, derived "), + h.text("data, and one lazy data, computed every time the result"), + h.text("of counter / 10 changes. Open your console, and watch "), + h.text("the side-effects running!"), + ]) + } +} + +fn navbar(model: Model) { + case model.node { + "#single" -> element.none() + _ -> styles.nav() + } +} + +fn counter(counter: Bright(Data, Computed)) { + use data, computed <- bright.view(counter) + styles.counter_wrapper([], [ + styles.counter([], [ + styles.counter_number(data.counter), + styles.buttons_wrapper([], [ + styles.button([e.on_click(Increment)], [h.text("Increase")]), + styles.button([e.on_click(Decrement)], [h.text("Decrease")]), + ]), + ]), + styles.counter_infos([], [ + styles.computed("computed.last_lazy: ", computed.last_lazy), + h.hr_([]), + styles.computed("computed.double: ", computed.double), + styles.computed("computed.triple: ", computed.triple), + ]), + ]) +} + +fn compute_memoized(data: Data, computed: Computed) { + let memoized = data.counter * 1000 + let last_lazy = now() + Computed(..computed, memoized:, last_lazy:) +} + +fn warn_on_three(data: Data, _: Computed) { + use <- bool.guard(when: data.counter != 3, return: effect.none()) + use _ <- effect.from + io.println("This message happened because the counter equals 3!") +} + +fn warn_on_three_multiple(data: Data, _: Computed) { + use <- bool.guard(when: data.counter % 3 != 0, return: effect.none()) + use _ <- effect.from + let counter = int.to_string(data.counter) + let msg = "This message happened because the counter is a multiple of 3!" + [msg, "(" <> counter <> ")"] + |> string.join(" ") + |> io.println +} + +fn warn(_, _) { + use _ <- effect.from + "This lazy message happened because the result of counter / 10 changed value!" + |> io.println +} diff --git a/packages/bright/e2e/sample/src/styles.gleam b/packages/bright/e2e/sample/src/styles.gleam new file mode 100644 index 0000000..95fa149 --- /dev/null +++ b/packages/bright/e2e/sample/src/styles.gleam @@ -0,0 +1,192 @@ +import gleam/int +import icons +import lustre/attribute as a +import sketch +import sketch/lustre/element/html as h +import sketch/media +import sketch/size.{px} + +pub fn intro() { + sketch.class([ + sketch.padding(px(40)), + sketch.padding_top(px(0)), + sketch.margin_bottom(px(20)), + sketch.first_of_type([sketch.padding_top(px(40))]), + ]) +} + +pub fn title(title) { + sketch.class([sketch.font_weight("bold")]) + |> h.h2([], [h.text(title)]) +} + +pub fn nav() { + sketch.class([ + sketch.font_size(size.rem(1.3)), + sketch.font_weight("bold"), + sketch.display("flex"), + sketch.justify_content("space-between"), + sketch.margin(px(18)), + sketch.gap(px(36)), + sketch.background("var(--navbar-background)"), + sketch.position("sticky"), + sketch.border_radius(px(10)), + sketch.top(px(18)), + sketch.border("1px solid var(--dark-background)"), + sketch.backdrop_filter("blur(8px)"), + ]) + |> h.nav([a.id("navbar")], [ + sketch.class([ + sketch.display("flex"), + sketch.align_items("center"), + sketch.padding_left(px(18)), + ]) + |> h.div([], [h.text("Bright")]), + h.div_([], []), + h.div( + sketch.class([ + sketch.display("flex"), + sketch.gap(px(24)), + sketch.padding(px(18)), + ]), + [], + [ + external_icon("https://hexdocs.pm/bright", icons.book_open()), + external_icon("https://github.com/ghivert/bright", icons.github()), + ], + ), + ]) +} + +fn external_icon(url, icon) { + sketch.class([ + sketch.color("#aaa"), + sketch.transition("all .3s"), + sketch.hover([sketch.color("var(--text-color)")]), + ]) + |> h.a([a.href(url)], [icons.small(icon)]) +} + +pub fn counter(attrs, children) { + sketch.class([ + sketch.display("flex"), + sketch.flex_direction("column"), + sketch.align_items("center"), + sketch.background("var(--darker-background)"), + sketch.color("var(--text-color)"), + sketch.height(px(220)), + sketch.width(px(220)), + sketch.border_radius(px(2)), + sketch.position("relative"), + sketch.z_index(100), + sketch.border_radius(px(10)), + ]) + |> h.div(attrs, children) +} + +pub fn button(attrs, children) { + sketch.class([ + sketch.appearance("none"), + sketch.border_radius(px(5)), + sketch.background("var(--dark-background)"), + sketch.display("flex"), + sketch.border("1px solid var(--border-color)"), + sketch.align_items("center"), + sketch.justify_content("center"), + sketch.padding(px(10)), + sketch.cursor("pointer"), + sketch.font_family("inherit"), + sketch.color("var(--text-color)"), + sketch.font_size_("inherit"), + sketch.text_transform("uppercase"), + sketch.font_weight("bold"), + sketch.hover([sketch.background("var(--button-hover)")]), + ]) + |> h.button(attrs, children) +} + +pub fn counter_number(counter) { + sketch.class([ + sketch.flex("1"), + sketch.display("flex"), + sketch.align_items("center"), + sketch.padding_top(px(20)), + sketch.font_weight("bold"), + sketch.font_size(size.rem(1.4)), + ]) + |> h.div([], [h.text(int.to_string(counter))]) +} + +pub fn buttons_wrapper(attrs, children) { + sketch.class([ + sketch.display("flex"), + sketch.flex_direction("column"), + sketch.padding(px(10)), + sketch.justify_content("space-evenly"), + sketch.width(size.percent(100)), + sketch.gap(px(10)), + ]) + |> h.div(attrs, children) +} + +pub fn counter_infos(attrs, children) { + sketch.class([ + sketch.font_family("Fira Code"), + sketch.background("var(--dark-background)"), + sketch.position("absolute"), + sketch.top(px(110)), + sketch.left(px(50)), + sketch.width(px(250)), + sketch.height(px(250)), + sketch.z_index(10), + sketch.display("flex"), + sketch.flex_direction("column"), + sketch.justify_content("end"), + sketch.padding(px(10)), + sketch.border_radius(px(10)), + sketch.media(media.max_width(px(400)), [sketch.width(px(200))]), + ]) + |> h.div(attrs, children) +} + +pub fn computed(title, content) { + h.div_([], [h.text(title), h.text(int.to_string(content))]) +} + +pub fn container(attrs, children) { + sketch.class([ + sketch.display("flex"), + sketch.gap(px(10)), + sketch.justify_content("center"), + sketch.media(media.max_width(px(700)), [ + sketch.flex_direction("column"), + sketch.align_items("center"), + ]), + ]) + |> h.div(attrs, children) +} + +pub fn counter_wrapper(attrs, children) { + sketch.class([ + sketch.position("relative"), + sketch.width(px(350)), + sketch.height(px(400)), + sketch.media(media.max_width(px(400)), [sketch.width(px(250))]), + ]) + |> h.div(attrs, children) +} + +pub fn footer(attrs, children) { + sketch.class([ + sketch.text_align("center"), + sketch.margin_top(px(60)), + sketch.margin_bottom(px(30)), + sketch.color("var(--text-grey)"), + ]) + |> h.div(attrs, children) +} + +pub fn body(attrs, children) { + sketch.class([sketch.max_width(px(1000)), sketch.margin_("auto")]) + |> h.div(attrs, children) +} diff --git a/packages/bright/e2e/sample/test/sample_test.gleam b/packages/bright/e2e/sample/test/sample_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/packages/bright/e2e/sample/test/sample_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/packages/bright/gleam.toml b/packages/bright/gleam.toml new file mode 100644 index 0000000..d2072ff --- /dev/null +++ b/packages/bright/gleam.toml @@ -0,0 +1,22 @@ +name = "bright" +version = "0.1.0" + +description = "Be bright. Derive data in your Lustre model." +licences = ["MIT"] + +[[links]] +title = "Sponsor" +href = "https://github.com/sponsors/ghivert" + +[repository] +type = "github" +user = "ghivert" +repo = "bright" + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" +lustre = ">= 4.6.1 and < 5.0.0" +lustre_dev_tools = ">= 1.6.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/packages/bright/manifest.toml b/packages/bright/manifest.toml new file mode 100644 index 0000000..d0bc494 --- /dev/null +++ b/packages/bright/manifest.toml @@ -0,0 +1,49 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, + { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, + { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, + { name = "gleam_community_ansi", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "4CD513FC62523053E62ED7BAC2F36136EC17D6A8942728250A9A00A15E340E4B" }, + { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, + { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, + { name = "gleam_erlang", version = "0.30.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "760618870AE4A497B10C73548E6E44F43B76292A54F0207B3771CBB599C675B4" }, + { name = "gleam_http", version = "3.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "A9EE0722106FCCAB8AD3BF9D0A3EFF92BFE8561D59B83BAE96EB0BE1938D4E0F" }, + { name = "gleam_httpc", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "091CDD2BEC8092E82707BEA03FB5205A2BBBDE4A2F551E3C069E13B8BC0C428E" }, + { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, + { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, + { name = "gleam_package_interface", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "CF3BFC5D0997750D9550D8D73A90F4B8D71C6C081B20ED4E70FFBE1E99AFC3C2" }, + { name = "gleam_stdlib", version = "0.43.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "69EF22E78FDCA9097CBE7DF91C05B2A8B5436826D9F66680D879182C0860A747" }, + { name = "glearray", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "B99767A9BC63EF9CC8809F66C7276042E5EFEACAA5B25188B552D3691B91AC6D" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glint", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5F6720081150AED8023131B0F3A35F9B0D6426A96CE02BEC52AD7018DF70566A" }, + { name = "glisten", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "48EF7F6D1DCA877C2F49AF35CC33946C7129EEB05A114758A2CC569C708BFAF8" }, + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "lustre", version = "4.6.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "486C3CFBD126939CAD2CA8B92A979A2DAADA5BABAA62BF2B163CD21E257BD4A1" }, + { name = "lustre_dev_tools", version = "1.6.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "5A1C7D20FA2C0D77D59F259EAE0E14BB3F5359CC1DE7C5ED6922B65FFCBD4C31" }, + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, + { name = "mist", version = "2.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "981F12FC8BA0656B40099EC876D6F2BEE7B95593610F342E9AB0DC4E663A932F" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" }, + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, + { name = "snag", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "08E9EB87C413457DB1DD66CD704C6878DACC9C93B418600F63873D0CD224E756" }, + { name = "spinner", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "9EE43AA33BE2DA5731B8F3F170AAB59AF1A815AFA5BF615F12C1B91F3B03F157" }, + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, + { name = "tom", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "228E667239504B57AD05EC3C332C930391592F6C974D0EFECF32FFD0F3629A27" }, + { name = "wisp", version = "1.2.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "F71265D2F1DE11426535A2FA1DA3B11D2FFB783B116DF9496BC8C41983EBADB4" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 4.6.1 and < 5.0.0" } +lustre_dev_tools = { version = ">= 1.6.0 and < 2.0.0" } diff --git a/packages/bright/src/bright.ffi.mjs b/packages/bright/src/bright.ffi.mjs new file mode 100644 index 0000000..599e83f --- /dev/null +++ b/packages/bright/src/bright.ffi.mjs @@ -0,0 +1,9 @@ +import * as gleam from "./gleam.mjs" + +export function coerce(a) { + return a +} + +export function areReferentiallyEqual(a, b) { + return a === b && gleam.isEqual(a, b) +} diff --git a/packages/bright/src/bright.gleam b/packages/bright/src/bright.gleam new file mode 100644 index 0000000..7cfd93c --- /dev/null +++ b/packages/bright/src/bright.gleam @@ -0,0 +1,254 @@ +import gleam/bool +import gleam/dynamic.{type Dynamic} +import gleam/function +import gleam/list +import gleam/pair +import lustre/effect.{type Effect} + +@external(erlang, "bright_ffi", "coerce") +@external(javascript, "./bright.ffi.mjs", "coerce") +fn coerce(a: a) -> b + +/// Optimization on JS, to ensure two data sharing the referential equality +/// will shortcut the comparison. Useful when performance are a thing in client +/// browser. +@external(javascript, "./bright.ffi.mjs", "areReferentiallyEqual") +fn are_referentially_equal(a: a, b: b) -> Bool { + dynamic.from(a) == dynamic.from(b) +} + +/// `Bright` holds raw data and computed data, and is used to compute caching. +/// `Bright` is instanciated using `init`, with initial data and computed data. +pub opaque type Bright(data, computed) { + Bright( + data: data, + computed: computed, + selections: List(Dynamic), + past_selections: List(Dynamic), + effects: List(Dynamic), + ) +} + +/// Creates the initial `Bright`. `data` & `computed` should be initialised with +/// their correct empty initial state. +pub fn init(data data: data, computed computed: computed) { + Bright(data:, computed:, selections: [], past_selections: [], effects: []) +} + +pub fn start( + bright: Bright(data, computed), + next: fn(Bright(data, computed)) -> Bright(data, computed), +) -> #(Bright(data, computed), Effect(msg)) { + let old_computations = bright.past_selections + let new_data = next(bright) + let all_effects = dynamic.from(new_data.effects) |> coerce |> list.reverse + panic_if_different_computations_count(old_computations, new_data.selections) + let past_selections = list.reverse(new_data.selections) + Bright(..new_data, past_selections:, selections: [], effects: []) + |> pair.new(effect.batch(all_effects)) +} + +/// Entrypoint for the update cycle. Use it a way to trigger the start of `Bright` +/// computations, and chain them with other `bright` calls. +/// +/// ```gleam +/// pub fn update(model: Bright(data, computed), msg: Msg) { +/// // Starts the update cycle, and returns #(Bright(data, computed), Effect(msg)). +/// use model <- bright.update(model, update_data(_, msg)) +/// bright.return(model) +/// } +/// ``` +pub fn update( + bright: Bright(data, computed), + update_: fn(data) -> #(data, Effect(msg)), + next: fn(Bright(data, computed)) -> Bright(data, computed), +) -> Bright(data, computed) { + let #(data, effects) = update_(bright.data) + let effects = [dynamic.from(effects), ..bright.effects] + Bright(..bright, data:, effects:) + |> next +} + +/// Derives data from the `data` state, and potentially the current `computed` +/// state. `compute` will run **at every render**, so be careful with computations +/// as they can block paint or actors. +/// +/// ```gleam +/// pub fn update(model: Bright(data, computed), msg: Msg) { +/// use model <- bright.update(model, update_data(_, msg)) +/// model +/// |> bright.compute(fn (d, c) { Computed(..c, field1: computation1(d)) }) +/// |> bright.compute(fn (d, c) { Computed(..c, field2: computation2(d)) }) +/// |> bright.compute(fn (d, c) { Computed(..c, field3: computation3(d)) }) +/// } +/// ``` +pub fn compute( + bright: Bright(data, computed), + compute_: fn(data, computed) -> computed, +) -> Bright(data, computed) { + compute_(bright.data, bright.computed) + |> fn(computed) { Bright(..bright, computed:) } +} + +/// Plugs in existing `data` and `computed` state, to issue some side-effects, +/// when your application needs to run side-effects depending on the current state. +/// +/// ```gleam +/// pub fn update(model: Bright(data, computed), msg: Msg) { +/// use model <- bright.update(model, update_data(_, msg)) +/// use d, c <- bright.guard(model) +/// use dispatch <- effect.from +/// case d.field == 10 { +/// True -> dispatch(my_msg) +/// False -> Nil +/// } +/// } +/// ``` +pub fn run( + bright: Bright(data, computed), + guard_: fn(data, computed) -> Effect(msg), +) -> Bright(data, computed) { + guard_(bright.data, bright.computed) + |> dynamic.from + |> list.prepend(bright.effects, _) + |> fn(effects) { Bright(..bright, effects:) } +} + +/// Derives data like [`compute`](#compute) lazily. `lazy_compute` accepts a +/// selector as second argument. Each time the selector returns a different data +/// than previous run, the computation will run. Otherwise, nothing happens. +/// +/// ```gleam +/// pub fn update(model: Bright(data, computed), msg: Msg) { +/// use model <- bright.update(model, update_data(_, msg)) +/// model +/// |> bright.lazy_compute(selector, fn (d, c) { Computed(..c, field1: computation1(d)) }) +/// |> bright.lazy_compute(selector, fn (d, c) { Computed(..c, field2: computation2(d)) }) +/// |> bright.lazy_compute(selector, fn (d, c) { Computed(..c, field3: computation3(d)) }) +/// } +/// +/// /// Use it with lazy_compute to recompute only when the field when +/// /// { old_data.field / 10 } != { data.field / 10 } +/// fn selector(d, _) { +/// d.field / 10 +/// } +/// ``` +pub fn lazy_compute( + bright: Bright(data, computed), + selector: fn(data) -> a, + compute_: fn(data, computed) -> computed, +) -> Bright(data, computed) { + lazy_wrap(bright, selector, compute, compute_) +} + +/// Plugs in existing `data` like [`guard`](#guard) lazily. `lazy_guard` accepts +/// a selector as second argument. Each time the selector returns a different data +/// than previous run, the computation will run. Otherwise, nothing happens. +/// +/// ```gleam +/// pub fn update(model: Bright(data, computed), msg: Msg) { +/// use model <- bright.update(model, update_data(_, msg)) +/// use d, c <- bright.lazy_guard(model, selector) +/// use dispatch <- effect.from +/// case d.field == 10 { +/// True -> dispatch(my_msg) +/// False -> Nil +/// } +/// } +/// +/// /// Use it with lazy_guard to recompute only when the field when +/// /// { old_data.field / 10 } != { data.field / 10 } +/// fn selector(d, _) { +/// d.field / 10 +/// } +/// ``` +pub fn lazy_run( + bright: Bright(data, computed), + selector: fn(data) -> a, + guard_: fn(data, computed) -> Effect(msg), +) -> Bright(data, computed) { + lazy_wrap(bright, selector, run, guard_) +} + +/// Injects `Bright(data, computed)` in the `view` function, like a middleware. +/// Used to extract `data` & `computed` states from `Bright`. +/// +/// ```gleam +/// pub fn view(model: Bright(data, computed)) { +/// use data, computed <- bright.view(model) +/// html.div([], [ +/// // Use data or computed here. +/// ]) +/// } +/// ``` +pub fn view( + bright: Bright(data, computed), + viewer: fn(data, computed) -> a, +) -> a { + viewer(bright.data, bright.computed) +} + +/// Allows to run multiple `update` on multiple `Bright` in the same update cycle. +/// Every call to step with compute a new `Bright`, and will let you chain the +/// steps. +/// +/// ```gleam +/// pub type Model { +/// Model( +/// fst_bright: Bright(data, computed), +/// snd_bright: Bright(data, computed), +/// ) +/// } +/// +/// fn update(model: Model, msg: Msg) { +/// use fst_bright <- bright.step(update_fst(model.fst_bright, msg)) +/// use snd_bright <- bright.step(update_snd(model.snd_bright, msg)) +/// bright.return(Model(fst_bright:, snd_bright:)) +/// } +/// ``` +pub fn step( + bright: #(Bright(data, computed), Effect(msg)), + next: fn(Bright(data, computed)) -> #(model, Effect(msg)), +) { + let #(bright, effs) = bright + let #(model, effs_) = next(bright) + #(model, effect.batch([effs, effs_])) +} + +/// Helper to write `bright` update cycle. Equivalent to `#(a, effect.none())`. +pub fn return(a) { + #(a, effect.none()) +} + +fn lazy_wrap( + bright: Bright(data, computed), + selector: fn(data) -> a, + setter: fn(Bright(data, computed), fn(data, computed) -> c) -> + Bright(data, computed), + compute_: fn(data, computed) -> c, +) -> Bright(data, computed) { + let selected_data = selector(bright.data) + let selections = [dynamic.from(selected_data), ..bright.selections] + let bright = Bright(..bright, selections:) + case bright.past_selections { + [] -> setter(bright, compute_) + [value, ..past_selections] -> { + Bright(..bright, past_selections:) + |> case are_referentially_equal(value, selected_data) { + True -> function.identity + False -> setter(_, compute_) + } + } + } +} + +fn panic_if_different_computations_count( + old_computations: List(c), + computations: List(d), +) -> Nil { + let count = list.length(old_computations) + use <- bool.guard(when: count == 0, return: Nil) + let is_same_count = count == list.length(computations) + use <- bool.guard(when: is_same_count, return: Nil) + panic as "Memoized computed should be consistent over time, otherwise memo can not work." +} diff --git a/packages/bright/src/bright_ffi.erl b/packages/bright/src/bright_ffi.erl new file mode 100644 index 0000000..4cc764f --- /dev/null +++ b/packages/bright/src/bright_ffi.erl @@ -0,0 +1,6 @@ +-module(bright_ffi). + +-export([coerce/1]). + +coerce(A) -> + A. diff --git a/packages/bright/test/bright_test.gleam b/packages/bright/test/bright_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/packages/bright/test/bright_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/packages/interfaces/.gitignore b/packages/interfaces/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/packages/interfaces/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/packages/lustre/examples/99-full-stack-applications/server/gleam.toml b/packages/interfaces/gleam.toml similarity index 69% rename from packages/lustre/examples/99-full-stack-applications/server/gleam.toml rename to packages/interfaces/gleam.toml index 4ea8472..663d43a 100644 --- a/packages/lustre/examples/99-full-stack-applications/server/gleam.toml +++ b/packages/interfaces/gleam.toml @@ -1,4 +1,4 @@ -name = "app" +name = "interfaces" version = "1.0.0" # Fill out these fields if you intend to generate HTML documentation or publish @@ -6,14 +6,16 @@ version = "1.0.0" # # description = "" # licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] # # For a full reference of all the available options, you can have a look at # https://gleam.run/writing-gleam/gleam-toml/. [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" +birl = ">= 1.7.1 and < 2.0.0" +gleam_json = ">= 2.1.0 and < 3.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/packages/interfaces/manifest.toml b/packages/interfaces/manifest.toml new file mode 100644 index 0000000..fbe77a1 --- /dev/null +++ b/packages/interfaces/manifest.toml @@ -0,0 +1,16 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, + { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "ranger", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "B8F3AFF23A3A5B5D9526B8D18E7C43A7DFD3902B151B97EC65397FE29192B695" }, +] + +[requirements] +birl = { version = ">= 1.7.1 and < 2.0.0" } +gleam_json = { version = ">= 2.1.0 and < 3.0.0" } +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/packages/interfaces/src/data/analytics.gleam b/packages/interfaces/src/data/analytics.gleam new file mode 100644 index 0000000..65a22ab --- /dev/null +++ b/packages/interfaces/src/data/analytics.gleam @@ -0,0 +1,88 @@ +import birl +import gleam/dynamic +import gleam/json +import gleam/option +import gleam/pair +import gleam/result + +pub type Analytics { + Analytics( + total_searches: Int, + total_signatures: Int, + total_indexed: Int, + timeseries: List(#(Int, birl.Time)), + ranked: List(Package), + popular: List(Package), + ) +} + +pub type Package { + Package( + name: String, + repository: String, + rank: Int, + popularity: option.Option(Int), + ) +} + +pub fn encode(analytics: Analytics) { + json.object([ + #("total_searches", json.int(analytics.total_searches)), + #("total_signatures", json.int(analytics.total_signatures)), + #("total_indexed", json.int(analytics.total_indexed)), + #("ranked", json.array(analytics.ranked, encode_package)), + #("popular", json.array(analytics.popular, encode_package)), + #("timeseries", { + json.array(analytics.timeseries, fn(row) { + json.object([ + #("count", json.int(row.0)), + #("date", json.string(birl.to_iso8601(row.1))), + ]) + }) + }), + ]) +} + +pub fn decode(dyn) { + dynamic.decode6( + Analytics, + dynamic.field("total_searches", dynamic.int), + dynamic.field("total_signatures", dynamic.int), + dynamic.field("total_indexed", dynamic.int), + dynamic.field("timeseries", { + dynamic.list(dynamic.decode2( + pair.new, + dynamic.field("count", dynamic.int), + dynamic.field("date", fn(dyn) { + dynamic.string(dyn) + |> result.then(fn(t) { + birl.parse(t) + |> result.replace_error([]) + }) + }), + )) + }), + dynamic.field("ranked", dynamic.list(decode_package)), + dynamic.field("popular", dynamic.list(decode_package)), + )(dyn) +} + +pub fn encode_package(package: Package) { + let Package(name:, repository:, rank:, popularity:) = package + json.object([ + #("name", json.string(name)), + #("repository", json.string(repository)), + #("rank", json.int(rank)), + #("popularity", json.nullable(popularity, json.int)), + ]) +} + +pub fn decode_package(dyn) { + dynamic.decode4( + Package, + dynamic.field("name", dynamic.string), + dynamic.field("repository", dynamic.string), + dynamic.field("rank", dynamic.int), + dynamic.field("popularity", dynamic.optional(dynamic.int)), + )(dyn) +} diff --git a/apps/frontend/src/data/implementations.gleam b/packages/interfaces/src/data/implementations.gleam similarity index 100% rename from apps/frontend/src/data/implementations.gleam rename to packages/interfaces/src/data/implementations.gleam diff --git a/apps/frontend/src/data/kind.gleam b/packages/interfaces/src/data/kind.gleam similarity index 67% rename from apps/frontend/src/data/kind.gleam rename to packages/interfaces/src/data/kind.gleam index 0c31fd2..4ddf899 100644 --- a/apps/frontend/src/data/kind.gleam +++ b/packages/interfaces/src/data/kind.gleam @@ -1,4 +1,5 @@ import gleam/dynamic +import gleam/json import gleam/result pub type Kind { @@ -8,7 +9,7 @@ pub type Kind { Constant } -pub fn decode_kind(dyn) { +pub fn decode(dyn) { use str <- result.try(dynamic.string(dyn)) case str { "function" -> Ok(Function) @@ -19,7 +20,17 @@ pub fn decode_kind(dyn) { } } -pub fn display_kind(kind) { +pub fn encode(kind) { + case kind { + Function -> "function" + TypeDefinition -> "type_definition" + TypeAlias -> "type_alias" + Constant -> "constant" + } + |> json.string +} + +pub fn display(kind) { case kind { Function -> "Function" TypeDefinition -> "Type" diff --git a/packages/interfaces/src/data/metadata.gleam b/packages/interfaces/src/data/metadata.gleam new file mode 100644 index 0000000..b165c33 --- /dev/null +++ b/packages/interfaces/src/data/metadata.gleam @@ -0,0 +1,53 @@ +import data/implementations.{type Implementations, Implementations} +import gleam/decoder_extra +import gleam/dynamic +import gleam/json +import gleam/option.{type Option} +import gleam/pair + +pub type Metadata { + Metadata( + deprecation: Option(String), + implementations: Option(Implementations), + ) +} + +pub fn decode(dyn) { + dynamic.decode2( + Metadata, + decoder_extra.completely_option("deprecation"), + dynamic.optional_field( + "implementations", + dynamic.decode3( + Implementations, + dynamic.field("gleam", dynamic.bool), + dynamic.field("uses_erlang_externals", dynamic.bool), + dynamic.field("uses_javascript_externals", dynamic.bool), + ), + ), + )(dyn) +} + +pub fn encode(metadata: Metadata) { + let Metadata(deprecation:, implementations:) = metadata + [] + |> append_optional(deprecation, fn(d) { #("deprecation", json.string(d)) }) + |> append_optional(implementations, encode_implementations) + |> json.object +} + +fn encode_implementations(i: Implementations) { + json.object([ + #("gleam", json.bool(i.gleam)), + #("uses_erlang_externals", json.bool(i.uses_erlang_externals)), + #("uses_javascript_externals", json.bool(i.uses_javascript_externals)), + ]) + |> pair.new("implementations", _) +} + +fn append_optional(elems, data, mapper) { + case data { + option.None -> elems + option.Some(data) -> [mapper(data), ..elems] + } +} diff --git a/apps/frontend/src/data/package.gleam b/packages/interfaces/src/data/package.gleam similarity index 50% rename from apps/frontend/src/data/package.gleam rename to packages/interfaces/src/data/package.gleam index 7c60a96..b229b82 100644 --- a/apps/frontend/src/data/package.gleam +++ b/packages/interfaces/src/data/package.gleam @@ -1,5 +1,4 @@ import gleam/dynamic -import gleam/io import gleam/json import gleam/option.{type Option} import gleam/result @@ -17,18 +16,23 @@ pub type Package { ) } -pub fn decoder(dyn) { +pub fn decode(dyn) { dynamic.decode8( Package, dynamic.field("name", dynamic.string), dynamic.field("repository", dynamic.optional(dynamic.string)), dynamic.field("documentation", dynamic.optional(dynamic.string)), - dynamic.field("hex-url", dynamic.optional(dynamic.string)), + dynamic.field("hex_url", dynamic.optional(dynamic.string)), dynamic.field("licenses", fn(dyn) { - use data <- result.try(dynamic.optional(dynamic.string)(dyn)) - option.unwrap(data, "[]") - |> json.decode(using: dynamic.list(dynamic.string)) - |> result.replace_error([dynamic.DecodeError("", "", [])]) + dynamic.any([ + dynamic.list(dynamic.string), + fn(dyn) { + use data <- result.try(dynamic.optional(dynamic.string)(dyn)) + option.unwrap(data, "[]") + |> json.decode(using: dynamic.list(dynamic.string)) + |> result.replace_error([dynamic.DecodeError("", "", [])]) + }, + ])(dyn) }), dynamic.field("description", dynamic.optional(dynamic.string)), dynamic.field("rank", dynamic.optional(dynamic.int)), @@ -41,3 +45,20 @@ pub fn decoder(dyn) { }), )(dyn) } + +pub fn encode(package: Package) { + json.object([ + #("name", json.string(package.name)), + #("repository", json.nullable(package.repository, json.string)), + #("documentation", json.nullable(package.documentation, json.string)), + #("hex_url", json.nullable(package.hex_url, json.string)), + #("licenses", json.array(package.licenses, json.string)), + #("description", json.nullable(package.description, json.string)), + #("rank", json.nullable(package.rank, json.int)), + #("popularity", { + json.object([#("github", json.int(package.popularity))]) + |> json.to_string + |> json.string + }), + ]) +} diff --git a/apps/frontend/src/data/signature.gleam b/packages/interfaces/src/data/signature.gleam similarity index 68% rename from apps/frontend/src/data/signature.gleam rename to packages/interfaces/src/data/signature.gleam index 7c8adda..10242c1 100644 --- a/apps/frontend/src/data/signature.gleam +++ b/packages/interfaces/src/data/signature.gleam @@ -1,5 +1,6 @@ import gleam/dynamic import gleam/int +import gleam/json import gleam/list import gleam/option.{type Option} import gleam/result @@ -46,7 +47,7 @@ pub type Signature { TypeDefinition(parameters: Int, constructors: List(TypeConstructor)) } -pub fn decode_signature(dyn) { +pub fn decode(dyn) { use res <- result.try(dynamic.field("kind", dynamic.string)(dyn)) case res { "constant" -> decode_constant(dyn) @@ -57,6 +58,35 @@ pub fn decode_signature(dyn) { } } +pub fn encode(signature: Signature) { + case signature { + Constant(type_:, ..) -> + json.object([ + #("kind", json.string("constant")), + #("type", encode_type(type_)), + ]) + TypeAlias(parameters:, alias:, ..) -> + json.object([ + #("kind", json.string("type-alias")), + #("parameters", json.int(parameters)), + #("alias", encode_type(alias)), + ]) + TypeDefinition(parameters:, constructors:) -> + json.object([ + #("kind", json.string("type-definition")), + #("parameters", json.int(parameters)), + #("constructors", json.array(constructors, encode_constructors)), + ]) + Function(name:, return:, parameters:, ..) -> + json.object([ + #("kind", json.string("function")), + #("name", json.string(name)), + #("return", encode_type(return)), + #("parameters", json.array(parameters, encode_parameter)), + ]) + } +} + fn decode_type(dyn) { use res <- result.try(dynamic.field("kind", dynamic.string)(dyn)) case res { @@ -68,6 +98,33 @@ fn decode_type(dyn) { } } +fn encode_type(type_: Type) { + case type_ { + Variable(id:, ..) -> + json.object([#("kind", json.string("variable")), #("id", json.int(id))]) + Fn(parameters:, return:, ..) -> + json.object([ + #("kind", json.string("fn")), + #("params", json.array(parameters, encode_type)), + #("return", encode_type(return)), + ]) + Named(name:, package:, module:, parameters:, ref:, ..) -> + json.object([ + #("kind", json.string("named")), + #("name", json.string(name)), + #("package", json.string(package)), + #("module", json.string(module)), + #("parameters", json.array(parameters, encode_type)), + #("ref", json.nullable(ref, json.string)), + ]) + Tuple(elements:, ..) -> + json.object([ + #("kind", json.string("tuple")), + #("elements", json.array(elements, encode_type)), + ]) + } +} + fn decode_variable(dyn) { dynamic.decode1(fn(a) { Variable(1, a) }, dynamic.field("id", dynamic.int))( dyn, @@ -137,6 +194,13 @@ fn decode_parameter(dyn) { )(dyn) } +pub fn encode_parameter(parameter: Parameter) { + json.object([ + #("label", json.nullable(parameter.label, json.string)), + #("type", encode_type(parameter.type_)), + ]) +} + fn decode_constant(dyn) { dynamic.decode1( fn(a: Type) { @@ -201,3 +265,11 @@ fn decode_constructors(dyn) { dynamic.field("parameters", dynamic.list(decode_parameter)), )(dyn) } + +fn encode_constructors(constructor: TypeConstructor) { + json.object([ + #("documentation", json.nullable(constructor.documentation, json.string)), + #("name", json.string(constructor.name)), + #("parameters", json.array(constructor.parameters, encode_parameter)), + ]) +} diff --git a/packages/interfaces/src/data/type_search.gleam b/packages/interfaces/src/data/type_search.gleam new file mode 100644 index 0000000..a30435d --- /dev/null +++ b/packages/interfaces/src/data/type_search.gleam @@ -0,0 +1,46 @@ +import data/kind.{type Kind} +import data/metadata.{type Metadata} +import data/signature.{type Signature} +import gleam/decoder_extra as decode_ +import gleam/dynamic +import gleam/json + +pub type TypeSearch { + TypeSearch( + type_name: String, + documentation: String, + signature_kind: Kind, + metadata: Metadata, + json_signature: Signature, + module_name: String, + package_name: String, + version: String, + ) +} + +pub fn decode(dyn) { + dynamic.decode8( + TypeSearch, + dynamic.field("type_name", dynamic.string), + dynamic.field("documentation", dynamic.string), + dynamic.field("signature_kind", kind.decode), + dynamic.field("metadata", decode_.json(metadata.decode)), + dynamic.field("json_signature", decode_.json(signature.decode)), + dynamic.field("module_name", dynamic.string), + dynamic.field("package_name", dynamic.string), + dynamic.field("version", dynamic.string), + )(dyn) +} + +pub fn encode(item: TypeSearch) { + json.object([ + #("type_name", json.string(item.type_name)), + #("documentation", json.string(item.documentation)), + #("signature_kind", kind.encode(item.signature_kind)), + #("metadata", metadata.encode(item.metadata)), + #("json_signature", signature.encode(item.json_signature)), + #("module_name", json.string(item.module_name)), + #("package_name", json.string(item.package_name)), + #("version", json.string(item.version)), + ]) +} diff --git a/packages/interfaces/src/gleam/coerce.gleam b/packages/interfaces/src/gleam/coerce.gleam new file mode 100644 index 0000000..c379fff --- /dev/null +++ b/packages/interfaces/src/gleam/coerce.gleam @@ -0,0 +1,3 @@ +@external(erlang, "interfaces_ffi", "coerce") +@external(javascript, "../interfaces.ffi.mjs", "coerce") +pub fn coerce(value: a) -> b diff --git a/packages/interfaces/src/gleam/decoder_extra.gleam b/packages/interfaces/src/gleam/decoder_extra.gleam new file mode 100644 index 0000000..6222325 --- /dev/null +++ b/packages/interfaces/src/gleam/decoder_extra.gleam @@ -0,0 +1,25 @@ +import gleam/dynamic.{type Dynamic} +import gleam/json +import gleam/option +import gleam/result + +pub fn completely_option(field: String) { + fn(dyn: Dynamic) { + dynamic.optional_field(field, dynamic.optional(dynamic.string))(dyn) + |> result.map(fn(res) { option.flatten(res) }) + } +} + +pub fn json(decoder: dynamic.Decoder(a)) { + dynamic.any([ + decoder, + fn(dyn) { + case dynamic.string(dyn) { + Error(e) -> Error(e) + Ok(content) -> + json.decode(content, decoder) + |> result.replace_error([]) + } + }, + ]) +} diff --git a/packages/interfaces/src/interfaces.ffi.mjs b/packages/interfaces/src/interfaces.ffi.mjs new file mode 100644 index 0000000..b6eec50 --- /dev/null +++ b/packages/interfaces/src/interfaces.ffi.mjs @@ -0,0 +1,3 @@ +export function coerce(a) { + return a +} diff --git a/packages/interfaces/src/interfaces_ffi.erl b/packages/interfaces/src/interfaces_ffi.erl new file mode 100644 index 0000000..e8ab1dc --- /dev/null +++ b/packages/interfaces/src/interfaces_ffi.erl @@ -0,0 +1,6 @@ +-module(interfaces_ffi). + +-export([coerce/1]). + +coerce(A) -> + A. diff --git a/packages/interfaces/test/interfaces_test.gleam b/packages/interfaces/test/interfaces_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/packages/interfaces/test/interfaces_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/packages/lustre/.github/FUNDING.yml b/packages/lustre/.github/FUNDING.yml deleted file mode 100644 index b873a5a..0000000 --- a/packages/lustre/.github/FUNDING.yml +++ /dev/null @@ -1,13 +0,0 @@ -# These are supported funding model platforms - -github: [hayleigh-dot-dev] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/packages/lustre/.github/workflows/release.yml b/packages/lustre/.github/workflows/release.yml deleted file mode 100644 index d6612fa..0000000 --- a/packages/lustre/.github/workflows/release.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: release - -on: - push: - tags: ["v*"] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3.1.0 - - uses: erlef/setup-beam@v1.16.0 - with: - otp-version: "26.0.2" - rebar3-version: "3" - gleam-version: "1.2.1" - - - run: cargo install tomlq - - run: | - if [ "v$(tomlq version -f gleam.toml)" == "${{ github.ref_name }}" ]; then - exit 0 - fi - echo "tag does not match version in gleam.toml, refusing to publish" - exit 1 - - run: gleam format --check src test - - run: gleam test - - run: gleam publish -y - env: - HEXPM_USER: ${{ secrets.HEX_USERNAME }} - HEXPM_PASS: ${{ secrets.HEX_PASSWORD }} - - - uses: softprops/action-gh-release@v1 diff --git a/packages/lustre/.gitignore b/packages/lustre/.gitignore deleted file mode 100644 index 992d05f..0000000 --- a/packages/lustre/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.beam -*.ez -build -erl_crash.dump - -.vscode - -node_modules \ No newline at end of file diff --git a/packages/lustre/LICENSE b/packages/lustre/LICENSE deleted file mode 100644 index b8320fd..0000000 --- a/packages/lustre/LICENSE +++ /dev/null @@ -1,18 +0,0 @@ -Copyright 2022 Hayleigh Thompson - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in the -Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/lustre/README.md b/packages/lustre/README.md deleted file mode 100644 index 29eabfc..0000000 --- a/packages/lustre/README.md +++ /dev/null @@ -1,199 +0,0 @@ -

Lustre

- -
- ✨ Make your frontend shine ✨ -
- -
- A framework for building Web apps in Gleam! -
- -
- - - - - -
- Built with ❤︎ by - Hayleigh Thompson and - - contributors - -
- ---- - -## Table of contents - -- [Features](#features) -- [Example](#example) -- [Philosophy](#philosophy) -- [Installation](#installation) -- [Where next](#where-next) -- [Support](#support) - -## Features - -- A **declarative**, functional API for constructing HTML. No templates, no macros, - just Gleam. - -- An Erlang and Elm-inspired architecture for **managing state**. - -- **Managed side effects** for predictable, testable code. - -- Universal components. **Write once, run anywhere**. Elm meets Phoenix LiveView. - -- A **batteries-included CLI** that makes scaffolding and building apps a breeze. - -- **Server-side rendering** for static HTML templating. - -## Example - -```gleam -import gleam/int -import lustre -import lustre/element.{text} -import lustre/element/html.{div, button, p} -import lustre/event.{on_click} - -pub fn main() { - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) - - Nil -} - -fn init(_flags) { - 0 -} - -type Msg { - Incr - Decr -} - -fn update(model, msg) { - case msg { - Incr -> model + 1 - Decr -> model - 1 - } -} - -fn view(model) { - let count = int.to_string(model) - - div([], [ - button([on_click(Incr)], [text(" + ")]), - p([], [text(count)]), - button([on_click(Decr)], [text(" - ")]) - ]) -} -``` - -## Philosophy - -Lustre is an _opinionated_ framework for building small-to-medium-sized Web -applications. Modern frontend development is hard and complex. Some of that -complexity is necessary, but a lot of it is accidental or comes from having far -too many options. Lustre has the same design philosophy as Gleam: where possible, -there should be only one way to do things. - -That means shipping with a single state management system out of the box, modelled -after Elm and Erlang/OTP. Open any Lustre application and you should feel -right at home. - -It also means we encourage simple approaches to constructing views over complex -ones. Lustre _does_ have a way to create encapsulated stateful components (something -we sorely missed in Elm) but it shouldn't be the default. Prefer simple functions -to stateful components. - -Where components _are_ necessary, lean into the fact that Lustre components can -run _anywhere_. Lustre gives you the tools to write components that can run inside -an existing Lustre application, export them as a standalone Web Component, or run -them on the server with a minimal runtime for patching the DOM. Lustre calls these -**universal components** and they're written with Gleam's multiple targets in mind. - -## Installation - -Lustre is published on [Hex](https://hex.pm/packages/lustre)! You can add it to -your Gleam projects from the command line: - -```sh -gleam add lustre -``` - -Lustre also has a companion package containing development tooling that you might -like to install: - -> **Note**: the lustre_dev_tools development server watches your filesystem for -> changes to your gleam code and can automatically reload the browser. For linux -> users this requires [inotify-tools]() be installed - -```sh -gleam add --dev lustre_dev_tools -``` - -If you're using a different build tool, like Rebar3 or Mix, you can add Lustre -to your `rebar.config` or `mix.exs` file respectively. - -```erlang -{deps, [ - {lustre, "4.0.0"} -]} -``` - -```elixir -defp deps do - [ - {:lustre, "~> 4.0"} - ] -end -``` - -## Where next - -To get up to speed with Lustre, check out the [quickstart guide](https://hexdocs.pm/lustre/guide/01-quickstart.html). -If you prefer to see some code, the [examples](https://github.com/lustre-labs/lustre/tree/main/examples) -directory contains a handful of small applications that demonstrate different -aspects of the framework. - -You can also read through the documentation and API reference on -[HexDocs](https://hexdocs.pm/lustre). - -## Support - -Lustre is mostly built by just me, [Hayleigh](https://github.com/hayleigh-dot-dev), -around two jobs. If you'd like to support my work, you can [sponsor me on GitHub](https://github.com/sponsors/hayleigh-dot-dev). - -Contributions are also very welcome! If you've spotted a bug, or would like to -suggest a feature, please open an issue or a pull request. diff --git a/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next.accepted b/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next.accepted deleted file mode 100644 index 4d6e333..0000000 --- a/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next.accepted +++ /dev/null @@ -1,5 +0,0 @@ ---- -version: 1.1.0 -title: Can compute a diff from one render to the next ---- -[[["0-0-0",{"content":"3"}]],[],[]] \ No newline at end of file diff --git a/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next_with_fragments.accepted b/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next_with_fragments.accepted deleted file mode 100644 index 10e6efc..0000000 --- a/packages/lustre/birdie_snapshots/can_compute_a_diff_from_one_render_to_the_next_with_fragments.accepted +++ /dev/null @@ -1,5 +0,0 @@ ---- -version: 1.1.0 -title: Can compute a diff from one render to the next with fragments ---- -[[["0-2-0",{"content":"3"}]],[],[]] \ No newline at end of file diff --git a/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state.accepted b/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state.accepted deleted file mode 100644 index 4545fcb..0000000 --- a/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state.accepted +++ /dev/null @@ -1,5 +0,0 @@ ---- -version: 1.0.1 -title: Can render an application's initial state. ---- -

0

\ No newline at end of file diff --git a/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state_when_using_fragments.accepted b/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state_when_using_fragments.accepted deleted file mode 100644 index d89dc7b..0000000 --- a/packages/lustre/birdie_snapshots/can_render_an_application's_initial_state_when_using_fragments.accepted +++ /dev/null @@ -1,5 +0,0 @@ ---- -version: 1.1.0 -title: Can render an application's initial state when using fragments ---- -

start fragment

middle fragment

0

order check, last element

\ No newline at end of file diff --git a/packages/lustre/birdie_snapshots/can_render_an_application's_state_after_some_updates.accepted b/packages/lustre/birdie_snapshots/can_render_an_application's_state_after_some_updates.accepted deleted file mode 100644 index 0922d44..0000000 --- a/packages/lustre/birdie_snapshots/can_render_an_application's_state_after_some_updates.accepted +++ /dev/null @@ -1,5 +0,0 @@ ---- -version: 1.0.1 -title: Can render an application's state after some updates. ---- -

3

\ No newline at end of file diff --git a/packages/lustre/birdie_snapshots/can_render_static_html.accepted b/packages/lustre/birdie_snapshots/can_render_static_html.accepted deleted file mode 100644 index 907c6a5..0000000 --- a/packages/lustre/birdie_snapshots/can_render_static_html.accepted +++ /dev/null @@ -1,5 +0,0 @@ ---- -version: 1.0.4 -title: Can render static HTML ---- -Hello, World!

Hello, World!

\ No newline at end of file diff --git a/packages/lustre/birdie_snapshots/can_render_static_html_with_unescaped_html.new b/packages/lustre/birdie_snapshots/can_render_static_html_with_unescaped_html.new deleted file mode 100644 index 9ffc59d..0000000 --- a/packages/lustre/birdie_snapshots/can_render_static_html_with_unescaped_html.new +++ /dev/null @@ -1,6 +0,0 @@ ---- -version: 1.1.2 -title: Can render static HTML with unescaped HTML ---- - -
hello!
\ No newline at end of file diff --git a/packages/lustre/birdie_snapshots/can_safely_escape_dangerous_symbols_in_attributes.accepted b/packages/lustre/birdie_snapshots/can_safely_escape_dangerous_symbols_in_attributes.accepted deleted file mode 100644 index cbb7f9d..0000000 --- a/packages/lustre/birdie_snapshots/can_safely_escape_dangerous_symbols_in_attributes.accepted +++ /dev/null @@ -1,5 +0,0 @@ ---- -version: 1.1.2 -title: Can safely escape dangerous symbols in attributes ---- -
\ No newline at end of file diff --git a/packages/lustre/examples/01-hello-world/README.md b/packages/lustre/examples/01-hello-world/README.md deleted file mode 100644 index 71df563..0000000 --- a/packages/lustre/examples/01-hello-world/README.md +++ /dev/null @@ -1,67 +0,0 @@ -![](./header.png) - -# 01 Hello World - -This hello world example is a tiny example of what you need to put together to -get a Lustre application running. In later examples we'll touch on server-side -rendering and Lustre Universal Components but for these first examples we'll -be looking at rendering on the client _only_. - -## Configuring the Gleam project - -It's important to remember to add `target = "javascript"` to your `gleam.toml`! -If you forget to do this you might end up confused when it looks like your project -is successfully building but you have no JavaScript output! - -## Creating a `lustre.element` application - -The simplest kind of Lustre application is the `element`. This sets up a static -application that does not have its own update loop and cannot dynamically render -any content. Instead, we provide a static Lustre `Element` to render once. - -### HTML attributes and inline styles - -In Lustre, HTML attributes are modelled as a `List` of attributes. This is a bit -different from many other frameworks that use an object or record for attributes. -Lustre takes the list-of-attributes approach for a couple of reasons: - -- Gleam doesn't have a way to construct an anonymous record: we'd have to have - an infinite number of types to cover every possible varation! - -- Working with lists makes it convenient to merge different sets of attributes - together (like an element that defines some local attributes and merges them - with any passed in as an argument). - -In a similar fashion, inline styles are lists of property/value tuples. In this -example we're setting inline styles for the `width` and `height` properties. - -### Why `element.text`? - -In frameworks like React, it's enough to just return a `String` if you want to -render some text. Gleam's type system works a little differently though, a string -literal isn't compatible with Lustre's `Element` type on its own, so we need to -wrap any text to render in `element.text`. - -You won't see us do it in any of the examples we share, but it's common for folks -to import `text` and any html elements they're using unqualified to cut down on -some of the noise: - -```gleam -import lustre/element.{text} -import lustre/element/html.{div, p} -... -``` - -## Seeing the result - -Lustre has a companion package containing development tooling called -[lustre_dev_tools](https://hexdocs.pm/lustre_dev_tools/). It's already included -in this and all the other example. You can run `gleam run -m lustre/dev start` -in any of these examples to start a development server and head over to -`localhost:1234` to see what it produces. - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). diff --git a/packages/lustre/examples/01-hello-world/gleam.toml b/packages/lustre/examples/01-hello-world/gleam.toml deleted file mode 100644 index 6b9e4db..0000000 --- a/packages/lustre/examples/01-hello-world/gleam.toml +++ /dev/null @@ -1,13 +0,0 @@ -name = "app" -version = "1.0.0" -target = "javascript" - -[dependencies] -gleam_json = "1.0.1" -gleam_stdlib = "~> 0.36" -lustre = "~> 4.0" -lustre_ui = "~> 0.4" - -[dev-dependencies] -gleeunit = "~> 1.0" -lustre_dev_tools = "~> 1.0" diff --git a/packages/lustre/examples/01-hello-world/header.png b/packages/lustre/examples/01-hello-world/header.png deleted file mode 100644 index dd76b40..0000000 Binary files a/packages/lustre/examples/01-hello-world/header.png and /dev/null differ diff --git a/packages/lustre/examples/01-hello-world/index.html b/packages/lustre/examples/01-hello-world/index.html deleted file mode 100644 index 36ddf10..0000000 --- a/packages/lustre/examples/01-hello-world/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - 🚧 app - - - - - - -
- - diff --git a/packages/lustre/examples/01-hello-world/manifest.toml b/packages/lustre/examples/01-hello-world/manifest.toml deleted file mode 100644 index ee55acd..0000000 --- a/packages/lustre/examples/01-hello-world/manifest.toml +++ /dev/null @@ -1,49 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" }, - { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, - { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, -] - -[requirements] -gleam_json = { version = "1.0.1" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { version = "~> 4.0" } -lustre_dev_tools = { version = "~> 1.0" } -lustre_ui = { version = "~> 0.4" } diff --git a/packages/lustre/examples/01-hello-world/src/app.gleam b/packages/lustre/examples/01-hello-world/src/app.gleam deleted file mode 100644 index 57b9492..0000000 --- a/packages/lustre/examples/01-hello-world/src/app.gleam +++ /dev/null @@ -1,20 +0,0 @@ -import lustre -import lustre/attribute -import lustre/element -import lustre/element/html -import lustre/ui - -pub fn main() { - let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")] - let app = - lustre.element(ui.centre( - [attribute.style(styles)], - html.div([], [ - html.h1([], [element.text("Hello, world.")]), - html.h2([], [element.text("Welcome to Lustre.")]), - ]), - )) - let assert Ok(_) = lustre.start(app, "#app", Nil) - - Nil -} diff --git a/packages/lustre/examples/02-interactivity/README.md b/packages/lustre/examples/02-interactivity/README.md deleted file mode 100644 index 37ebde7..0000000 --- a/packages/lustre/examples/02-interactivity/README.md +++ /dev/null @@ -1,149 +0,0 @@ -![](./header.png) - -# 02 Interactivity - -In this example we show the basic structure of all Lustre applications with a -classic counter example. - -## The Model-View-Update architecture - -All Lustre applications are built around the Model-View-Update (MVU) architecture. -This is a pattern that's been popularised by the Elm programming language and -has since been adopted by many other frameworks and languages. - -MVU applications are built around three main concepts: - -- A `Model` and a function to initialise it. -- A `Msg` type and a function to update the model based on messages. -- A `View` function to render the model as a Lustre `Element`. - -These three pieces come together to form a self-contained update loop. You produce -an initial model, render it as HTML, and convert any user interactions into -messages to handle in the update function. - -```text - +--------+ - | | - | update | - | | - +--------+ - ^ | - | | - Msg | | Model - | | - | v -+------+ +------------------------+ -| | Model | | -| init |------------------------>| Lustre Runtime | -| | | | -+------+ +------------------------+ - ^ | - | | - Msg | | Model - | | - | v - +--------+ - | | - | view | - | | - +--------+ -``` - -### Model - -The model represents the entire state of your application. For most Lustre -applications this will be a record, but for this example we're aliasing `Int` to -our `Model` type to keep things simple. - -We also need to write an `init` function that returns the initial state of our -application. It takes one argument, known as "flags" which is provided when the -application is first started. - -```gleam -fn init(initial_count: Int) -> Model { - case initial_count < 0 { - True -> 0 - False -> initial_count - } -} -``` - -Our `init` function takes a starting count, but ensures it cannot be below `0`. - -### Update - -In many other frameworks, it's common to update state directly in an event handler. -MVU applications take a different approach: instead of state updates being scattered -around your codebase, they are handled in a single `update` function. - -To achieve this, we define a `Msg` type that represents all the different kinds of -messages our application can receive. If you're familiar with Erlang this approach -to state management will be familiar to you. If you're coming from a JavaScript -background, this approach is most-similar to state management solutions like Redux -or Vuex. - -```gleam -pub opaque type Msg { - Incr - Decr -} -``` - -This approach means it is easy to quickly get an idea of all the ways your app -can change state, and makes it easy to add new state changes over time. By pattern -matching on an incoming message in our `update` function, we can lean on Gleam's -_exhaustiveness checking_ to ensure we handle all possible messages. - -### View - -Because state management is handled in our `update` function, our `view` becomes -a simple function that takes a model and returns some HTML in the form of a -Lustre `Element`. - -```gleam -fn view(model: Model) -> Element(Msg) { - ... -} -``` - -In Lustre we call _all_ functions that return an `Element` "view functions": there's -nothing special about the `view` that takes your model. - -Folks coming from frameworks like React might notice the absence of components -with local encapsulated state. Lustre _does_ have components like this, but unlike -other frameworks these are a fairly advanced use of the library and are typically -used for larger pieces of UI like an entire form or a table. We'll cover how -components fit into Lustre in later examples, but for now resist the urge to think -in terms of "components" and "state" and try to think of your UI as a composition -of _view functions_. - -## Creating a dynamic Lustre application - -In the previous example we used the `lustre.element` function to construct a -static Lustre app. To introduce the basic MVU loop, we can use `lustre.simple` -instead. From now on we'll see that all the different ways to construct a Lustre -application all take the same three `init`, `update`, and `view` functions. - -Starting a Lustre application with `lustre.start` requires three things: - -- A configured `Application` (that's what we used `lustre.element` for). - -- A [CSS selector](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors) - to locate the DOM node to mount the application on to. As in other frameworks, - it's common to use an element with the id "app": for that you'd write the - selector as `#app`. - -- Some initial data to pass to the application's `init` function. Because applications - constructed with `lustre.element` are not dynamic there's nothing meaningful - to pass in here, so we just use `Nil`. - -Starting an application could fail for a number of reasons, so this function -returns a `Result`. The `Ok` value is a function you can use to send messages to -your running application from the outside world: we'll see more of that in later -examples! - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). diff --git a/packages/lustre/examples/02-interactivity/gleam.toml b/packages/lustre/examples/02-interactivity/gleam.toml deleted file mode 100644 index 6b9e4db..0000000 --- a/packages/lustre/examples/02-interactivity/gleam.toml +++ /dev/null @@ -1,13 +0,0 @@ -name = "app" -version = "1.0.0" -target = "javascript" - -[dependencies] -gleam_json = "1.0.1" -gleam_stdlib = "~> 0.36" -lustre = "~> 4.0" -lustre_ui = "~> 0.4" - -[dev-dependencies] -gleeunit = "~> 1.0" -lustre_dev_tools = "~> 1.0" diff --git a/packages/lustre/examples/02-interactivity/header.png b/packages/lustre/examples/02-interactivity/header.png deleted file mode 100644 index 6cca8a2..0000000 Binary files a/packages/lustre/examples/02-interactivity/header.png and /dev/null differ diff --git a/packages/lustre/examples/02-interactivity/index.html b/packages/lustre/examples/02-interactivity/index.html deleted file mode 100644 index 36ddf10..0000000 --- a/packages/lustre/examples/02-interactivity/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - 🚧 app - - - - - - -
- - diff --git a/packages/lustre/examples/02-interactivity/manifest.toml b/packages/lustre/examples/02-interactivity/manifest.toml deleted file mode 100644 index ee55acd..0000000 --- a/packages/lustre/examples/02-interactivity/manifest.toml +++ /dev/null @@ -1,49 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" }, - { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, - { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, -] - -[requirements] -gleam_json = { version = "1.0.1" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { version = "~> 4.0" } -lustre_dev_tools = { version = "~> 1.0" } -lustre_ui = { version = "~> 0.4" } diff --git a/packages/lustre/examples/02-interactivity/src/app.gleam b/packages/lustre/examples/02-interactivity/src/app.gleam deleted file mode 100644 index 6e69048..0000000 --- a/packages/lustre/examples/02-interactivity/src/app.gleam +++ /dev/null @@ -1,60 +0,0 @@ -import gleam/int -import lustre -import lustre/attribute -import lustre/element.{type Element} -import lustre/element/html -import lustre/event -import lustre/ui - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", 0) - - Nil -} - -// MODEL ----------------------------------------------------------------------- - -type Model = - Int - -fn init(initial_count: Int) -> Model { - case initial_count < 0 { - True -> 0 - False -> initial_count - } -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - Incr - Decr -} - -fn update(model: Model, msg: Msg) -> Model { - case msg { - Incr -> model + 1 - Decr -> model - 1 - } -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")] - let count = int.to_string(model) - - ui.centre( - [attribute.style(styles)], - ui.stack([], [ - ui.button([event.on_click(Incr)], [element.text("+")]), - html.p([attribute.style([#("text-align", "center")])], [ - element.text(count), - ]), - ui.button([event.on_click(Decr)], [element.text("-")]), - ]), - ) -} diff --git a/packages/lustre/examples/03-controlled-inputs/README.md b/packages/lustre/examples/03-controlled-inputs/README.md deleted file mode 100644 index 95ca620..0000000 --- a/packages/lustre/examples/03-controlled-inputs/README.md +++ /dev/null @@ -1,70 +0,0 @@ -![](./header.png) - -# 03 Controlled Inputs - -The most common way to handle inputs and other state-holding elements is in a -_controlled_ way. This means your app's model is the source of truth for that -element's state, and you update that state based on user input or other events. - -This example shows what that means in practice. For any controlled input we need -two things: - -- A field in our model (or a function to derive a value from the model) to use - as the input's `value` attribute. - -- A message variant to handle input events and update the model. - -```gleam -ui.input([ - // Input's value is fixed to the model's `value` field - attribute.value(model.value), - // Whenever the input changes, we send a `UserUpdatedMessage` message with the - // new value - event.on_input(UserUpdatedMessage) -]) -``` - -## Why is this beneficial? - -Central to Lustre's architecture is the idea that your model is the single source -of truth for your application's UI. This opens up the door to things like serialising -program state to load in the future, time-travel debugging, and rehydrating your -app's state from a server. - -It also gives you tighter control of when and how to update your UI in response -to user input. In this example, we only update the model when the new input -value is less than 10 characters long. - -```gleam -case msg { - UserUpdatedMessage(value) -> { - let length = string.length(value) - - case length <= model.max { - True -> Model(..model, value: value, length: length) - False -> model - } - } - - ... -``` - -## A note on message naming - -In our [state management guide](https://hexdocs.pm/lustre/guide/02-state-management.html) -we touch on the idea of "messages not actions." We think the best way to name your -messages is following a "Subject Verb Object" pattern: `UserUpdatedMessage` not -`SetMessage` and so on. - -This approach to message naming can feel a cumbersome at first, especially for -small examples like this. One of Lustre's super powers is that as your app grows -in size, your `Msg` type becomes a very helpful overview of all the different -events your app can handle. When they take the form of `Subject Verb Object` it -gives you an immediate sense of the different things that speak to your app: how -much is coming from your backend, how much is user input, and so on. - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). diff --git a/packages/lustre/examples/03-controlled-inputs/gleam.toml b/packages/lustre/examples/03-controlled-inputs/gleam.toml deleted file mode 100644 index 6b9e4db..0000000 --- a/packages/lustre/examples/03-controlled-inputs/gleam.toml +++ /dev/null @@ -1,13 +0,0 @@ -name = "app" -version = "1.0.0" -target = "javascript" - -[dependencies] -gleam_json = "1.0.1" -gleam_stdlib = "~> 0.36" -lustre = "~> 4.0" -lustre_ui = "~> 0.4" - -[dev-dependencies] -gleeunit = "~> 1.0" -lustre_dev_tools = "~> 1.0" diff --git a/packages/lustre/examples/03-controlled-inputs/header.png b/packages/lustre/examples/03-controlled-inputs/header.png deleted file mode 100644 index 336fb18..0000000 Binary files a/packages/lustre/examples/03-controlled-inputs/header.png and /dev/null differ diff --git a/packages/lustre/examples/03-controlled-inputs/index.html b/packages/lustre/examples/03-controlled-inputs/index.html deleted file mode 100644 index 36ddf10..0000000 --- a/packages/lustre/examples/03-controlled-inputs/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - 🚧 app - - - - - - -
- - diff --git a/packages/lustre/examples/03-controlled-inputs/manifest.toml b/packages/lustre/examples/03-controlled-inputs/manifest.toml deleted file mode 100644 index ee55acd..0000000 --- a/packages/lustre/examples/03-controlled-inputs/manifest.toml +++ /dev/null @@ -1,49 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" }, - { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, - { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, -] - -[requirements] -gleam_json = { version = "1.0.1" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { version = "~> 4.0" } -lustre_dev_tools = { version = "~> 1.0" } -lustre_ui = { version = "~> 0.4" } diff --git a/packages/lustre/examples/03-controlled-inputs/src/app.gleam b/packages/lustre/examples/03-controlled-inputs/src/app.gleam deleted file mode 100644 index d44e1ae..0000000 --- a/packages/lustre/examples/03-controlled-inputs/src/app.gleam +++ /dev/null @@ -1,71 +0,0 @@ -import gleam/int -import gleam/string -import lustre -import lustre/attribute -import lustre/element.{type Element} -import lustre/event -import lustre/ui -import lustre/ui/layout/aside - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) -} - -// MODEL ----------------------------------------------------------------------- - -type Model { - Model(value: String, length: Int, max: Int) -} - -fn init(_flags) -> Model { - Model(value: "", length: 0, max: 10) -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - UserUpdatedMessage(value: String) - UserResetMessage -} - -fn update(model: Model, msg: Msg) -> Model { - case msg { - UserUpdatedMessage(value) -> { - let length = string.length(value) - - case length <= model.max { - True -> Model(..model, value: value, length: length) - False -> model - } - } - UserResetMessage -> Model(..model, value: "", length: 0) - } -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")] - let length = int.to_string(model.length) - let max = int.to_string(model.max) - - ui.centre( - [attribute.style(styles)], - ui.aside( - [aside.content_first(), aside.align_centre()], - ui.field( - [], - [element.text("Write a message:")], - ui.input([ - attribute.value(model.value), - event.on_input(UserUpdatedMessage), - ]), - [element.text(length <> "/" <> max)], - ), - ui.button([event.on_click(UserResetMessage)], [element.text("Reset")]), - ), - ) -} diff --git a/packages/lustre/examples/04-custom-event-handlers/README.md b/packages/lustre/examples/04-custom-event-handlers/README.md deleted file mode 100644 index 434725a..0000000 --- a/packages/lustre/examples/04-custom-event-handlers/README.md +++ /dev/null @@ -1,76 +0,0 @@ -![](./header.png) - -# 04 Custom Event Handlers - -While Lustre's built-in event handlers can cover most of your basic needs, in practice you will often need to provide more advanced functionality. For this, we can reach for the `event.on("eventname", handler)` function to generate attributes that can provide custom event handling. - -But first, let's take a look under the hood to see what event handlers actually _do_. - -## Decoding Dynamic Data - -Lustre is a type-safe framework, but the DOM allows HTML elements to generate events containing values of any arbitrary type and structure. In Gleam, such data is referred to as _dynamic_, and is handled by the `gleam/dynamic` library. `gleam/dynamic` is used for decoding everything from unpredictable JSON input to Lustre's DOM events. - -If you peek at [the gleam\dynamic documentation](https://hexdocs.pm/gleam_stdlib/0.17.1/gleam/dynamic/#module-types), you'll quickly see it exports four types: - -```gleam - pub external type Dynamic - // data for which we don't know the type - - pub type DecodeError { ... } - // the error returned when unexpected data is encountered - - pub type DecodeErrors = List(DecodeError) - - pub type Decoder(t) = fn(Dynamic) -> Result(t, DecodeErrors) - // any function that accepts dynamic data and returns a Result(t, DecodeErrors) -``` - -In Lustre, all DOM event values are converted to `Dynamic` values before being passed to their respective handlers. Event handlers accept those `Dynamic` values and return a `Result` of either `Ok(Msg)`, or `DecodeErrors` - the `DecodeError` list. - -Therefore, Lustre event handlers are simply an implementation of the `Decoder` function type. - -## Writing A Custom Input Handler - -In javascript, input event handlers often look something like this: - -```js -function onInput(event) { - const input = event.target.value; - // do your stuff! -} -``` - -This is very convenient! But it's not type-safe. From the function's perspective, there is no guarantee that _`event`_ is an object with a property named _`target`_ which itself has a property named _`value`_. In a more complex app, we might even pass it a numeric or boolean value on accident. The failure to handle such error conditions leads to many `Uncaught TypeError` crashes. - -Here's how we can extract the event's dynamic value in a type-safe way in Lustre: - -```gleam - let on_input = fn(event: dynamic.Dynamic) -> Result(Msg, dynamic.DecodeErrors) { - use target <- result.try(dynamic.field("target", dynamic.dynamic)(event)) - use value <- result.try(dynamic.field("value", dynamic.string)(target)) - // do your stuff! - Ok(GotInput(value)) - } -``` - -First we extract the `target` field from our `event`, which is expected to be of the type `dynamic.dynamic`. Because the target is itself dynamic, we can again use the dynamic library to extract its `value` field, which is expected to be of type `dynamic.string`. If either of those expectations are not met, the function will return an error, and nothing more will happen. - -This is such a common use case that Lustre's `event` module has a helper function for it. Here is a far less verbose version that provides the exact same type-safe guarantees: - -```gleam - let on_input = fn(event) { - use value <- result.try(event.value(event)) - // do your stuff! - Ok(GotInput(value)) - } -``` - -## Make it Loud - -In this [example code](./src/app.gleam#L63), we define a custom input handler called `make_it_loud`, which calls `string.uppercase` to make sure all our input is LOUD. Then in our [view function](./src/app.gleam#L79), instead of calling `event.on_input(GotInput)` like we did in the last example, we can just call `event.on("input", make_it_loud)`. - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). diff --git a/packages/lustre/examples/04-custom-event-handlers/gleam.toml b/packages/lustre/examples/04-custom-event-handlers/gleam.toml deleted file mode 100644 index 6b9e4db..0000000 --- a/packages/lustre/examples/04-custom-event-handlers/gleam.toml +++ /dev/null @@ -1,13 +0,0 @@ -name = "app" -version = "1.0.0" -target = "javascript" - -[dependencies] -gleam_json = "1.0.1" -gleam_stdlib = "~> 0.36" -lustre = "~> 4.0" -lustre_ui = "~> 0.4" - -[dev-dependencies] -gleeunit = "~> 1.0" -lustre_dev_tools = "~> 1.0" diff --git a/packages/lustre/examples/04-custom-event-handlers/header.png b/packages/lustre/examples/04-custom-event-handlers/header.png deleted file mode 100644 index e75e2b5..0000000 Binary files a/packages/lustre/examples/04-custom-event-handlers/header.png and /dev/null differ diff --git a/packages/lustre/examples/04-custom-event-handlers/index.html b/packages/lustre/examples/04-custom-event-handlers/index.html deleted file mode 100644 index 36ddf10..0000000 --- a/packages/lustre/examples/04-custom-event-handlers/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - 🚧 app - - - - - - -
- - diff --git a/packages/lustre/examples/04-custom-event-handlers/manifest.toml b/packages/lustre/examples/04-custom-event-handlers/manifest.toml deleted file mode 100644 index ee55acd..0000000 --- a/packages/lustre/examples/04-custom-event-handlers/manifest.toml +++ /dev/null @@ -1,49 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" }, - { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, - { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, -] - -[requirements] -gleam_json = { version = "1.0.1" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { version = "~> 4.0" } -lustre_dev_tools = { version = "~> 1.0" } -lustre_ui = { version = "~> 0.4" } diff --git a/packages/lustre/examples/04-custom-event-handlers/src/app.gleam b/packages/lustre/examples/04-custom-event-handlers/src/app.gleam deleted file mode 100644 index 725b04b..0000000 --- a/packages/lustre/examples/04-custom-event-handlers/src/app.gleam +++ /dev/null @@ -1,77 +0,0 @@ -import gleam/dynamic -import gleam/int -import gleam/result -import gleam/string -import lustre -import lustre/attribute -import lustre/element.{type Element} -import lustre/event -import lustre/ui -import lustre/ui/layout/aside - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) -} - -// MODEL ----------------------------------------------------------------------- - -type Model { - Model(value: String, length: Int, max: Int) -} - -fn init(_flags) -> Model { - Model(value: "", length: 0, max: 10) -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - UserUpdatedMessage(value: String) - UserResetMessage -} - -fn update(model: Model, msg: Msg) -> Model { - case msg { - UserUpdatedMessage(value) -> { - let length = string.length(value) - case length <= model.max { - True -> Model(..model, value: value, length: length) - False -> model - } - } - UserResetMessage -> Model(..model, value: "", length: 0) - } -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")] - let length = int.to_string(model.length) - let max = int.to_string(model.max) - let make_it_loud = fn(event) -> Result(Msg, List(dynamic.DecodeError)) { - use target <- result.try(dynamic.field("target", dynamic.dynamic)(event)) - use value <- result.try(dynamic.field("value", dynamic.string)(target)) - - let loud = string.uppercase(value) - - Ok(UserUpdatedMessage(loud)) - } - - ui.centre( - [attribute.style(styles)], - ui.aside( - [aside.content_first(), aside.align_centre()], - ui.field( - [], - [element.text("Write a LOUD message:")], - ui.input([attribute.value(model.value), event.on("input", make_it_loud)]), - [element.text(length <> "/" <> max)], - ), - ui.button([event.on_click(UserResetMessage)], [element.text("Reset")]), - ), - ) -} diff --git a/packages/lustre/examples/05-http-requests/README.md b/packages/lustre/examples/05-http-requests/README.md deleted file mode 100644 index 5fb95eb..0000000 --- a/packages/lustre/examples/05-http-requests/README.md +++ /dev/null @@ -1,137 +0,0 @@ -![](./header.png) - -# 05 HTTP Requests - -In the previous examples, we've seen Lustre applications constructed with the -[`lustre.simple`](https://hexdocs.pm/lustre/lustre.html#simple) constructor. -These kinds of applications are great for introducing the Model-View-Update (MVU) -pattern, but for most real-world applications we'll need a way to talk to the -outside world. - -Lustre's runtime includes _managed effects_, which allow us to perform side effects -like HTTP requests and communicate the results back to our application's `update` -function. To learn more about Lustre's effect system and why it's useful, check -out the [side effects guide](https://hexdocs.pm/lustre/guide/03-side-effects.html), -or the docs for the [`lustre/effect` module](https://hexdocs.pm/lustre/lustre/effect.html). - -This example is a practical look at what effects mean in Lustre, and we'll look -at how to send HTTP requests in a Lustre application: a pretty important thing to -know! - -## Moving on from `lustre.simple` - -From this example onwards, we will use a new application constructor: -[`lustre.application`](https://hexdocs.pm/lustre/lustre.html#application). Full Lustre -applications have the ability to communicate to the runtime. Let's compare the type -of both the `simple` and `application` functions: - -```gleam -pub fn simple( - init: fn(flags) -> model, - update: fn(model, msg) -> model, - view: fn(model) -> Element(msg), -) -> App(flags, model, msg) - -pub fn application( - init: fn(flags) -> #(model, Effect(msg)), - update: fn(model, msg) -> #(model, Effect(msg)), - view: fn(model) -> Element(msg), -) -> App(flags, model, msg) -``` - -All that's changed is the return type of our `init` and `update` functions. Instead -of returning just a new model, they now return a tuple containing both a model and -any side effects we want the runtime to perform. - -You'll notice that running a Lustre app with side effects _changes the signature_ -of our [`init`](src/app.gleam#L43) and [`update`](src/app.gleam#L54) functions. -Instead of returning just a model, we return a tuple containing both a model an -an `Effect(Msg)` value. The effect value specifies any further updates we might -want the Lustre runtime to execute before the next invocation of the `view` -function. - -> **Note**: notice how the type of `view` remains the same. In Lustre, your `view` -> is always a [_pure function_](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md) -> that takes a model and returns the UI to be rendered: we never perform side effects -> in the `view` function itself. - -## HTTP requests as side effects - -The community library [`lustre_http`](https://hexdocs.pm/lustre_http/) gives us -a way to model HTTP requests as Lustre `Effect`s. Crucially, when we call -`lustre_http.get` we are _not_ performing the request! We're constructing a -description of the side effect that we can hand off to the Lustre runtime to -perform. - -```gleam -fn get_quote() -> Effect(Msg) { - let url = "https://api.quotable.io/random" - let decoder = - dynamic.decode2( - Quote, - dynamic.field("author", dynamic.string), - dynamic.field("content", dynamic.string), - ) - - lustre_http.get(url, lustre_http.expect_json(decoder, ApiUpdatedQuote)) -} -``` - -To construct HTTP requests, we need a few different things: - -- The `url` to send the request to. - -- A description of what we _expect_ the result to be. There are a few options: - `expect_anything`, `expect_text`, `expect_json`. In this example we say we're - expecting a JSON response and provide a decoder. - -- Along with what we expect the response to be, we also need to provide a way - to turn that response into a `Msg` value that our `update` function can handle. - -The same applies for post requests too, but there you also need to provide the -JSON body of the request. - -## Tying it together - -We now have a function that can create an `Effect` for us, but we need to hand it -to the runtime to be executed. The only way we can do that is by returning it from -our `update` (or `init`) function! We attach an event listener on a button, and -when the user clicks that button we'll return the `Effect` we want to perform as -the second element of a tuple: - -```gleam -fn view(model: Model) -> Element(Msg) { - ui.centre([], - ui.button([event.on_click(UserClickedRefresh)], [ - element.text("New quote"), - ]), - ) -} - -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - case msg { - UserClickedRefresh -> #(model, get_quote()) - ... - } -} -``` - -Of course, we need to handle responses from the quote API in our `update` function -too. When there are no side effects we want the runtime to perform for us, we need -to call `effect.none()`: - -```gleam -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - case msg { - ... - ApiUpdatedQuote(Ok(quote)) -> #(Model(quote: Some(quote)), effect.none()) - ApiUpdatedQuote(Error(_)) -> #(model, effect.none()) - } -} -``` - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). diff --git a/packages/lustre/examples/05-http-requests/gleam.toml b/packages/lustre/examples/05-http-requests/gleam.toml deleted file mode 100644 index 518d327..0000000 --- a/packages/lustre/examples/05-http-requests/gleam.toml +++ /dev/null @@ -1,14 +0,0 @@ -name = "app" -version = "1.0.0" -target = "javascript" - -[dependencies] -gleam_json = "1.0.1" -gleam_stdlib = "~> 0.36" -lustre = "~> 4.0" -lustre_ui = "~> 0.4" -lustre_http = "~> 0.5.2" - -[dev-dependencies] -gleeunit = "~> 1.0" -lustre_dev_tools = "~> 1.0" diff --git a/packages/lustre/examples/05-http-requests/header.png b/packages/lustre/examples/05-http-requests/header.png deleted file mode 100644 index f08fb17..0000000 Binary files a/packages/lustre/examples/05-http-requests/header.png and /dev/null differ diff --git a/packages/lustre/examples/05-http-requests/index.html b/packages/lustre/examples/05-http-requests/index.html deleted file mode 100644 index 36ddf10..0000000 --- a/packages/lustre/examples/05-http-requests/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - 🚧 app - - - - - - -
- - diff --git a/packages/lustre/examples/05-http-requests/manifest.toml b/packages/lustre/examples/05-http-requests/manifest.toml deleted file mode 100644 index 68abb49..0000000 --- a/packages/lustre/examples/05-http-requests/manifest.toml +++ /dev/null @@ -1,53 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, - { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" }, - { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" }, - { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, - { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, -] - -[requirements] -gleam_json = { version = "1.0.1" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { version = "~> 4.0" } -lustre_dev_tools = { version = "~> 1.0" } -lustre_http = { version = "~> 0.5.2" } -lustre_ui = { version = "~> 0.4" } diff --git a/packages/lustre/examples/05-http-requests/src/app.gleam b/packages/lustre/examples/05-http-requests/src/app.gleam deleted file mode 100644 index acd5fcc..0000000 --- a/packages/lustre/examples/05-http-requests/src/app.gleam +++ /dev/null @@ -1,93 +0,0 @@ -import gleam/dynamic -import gleam/option.{type Option, None, Some} -import lustre -import lustre/attribute -import lustre/effect.{type Effect} -import lustre/element.{type Element} -import lustre/element/html -import lustre/event - -// Lustre_http is a community package that provides a simple API for making -// HTTP requests from your update function. You can find the docs for the package -// here: https://hexdocs.pm/lustre_http/index.html -import lustre/ui -import lustre/ui/layout/aside -import lustre_http.{type HttpError} - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - let app = lustre.application(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) -} - -// MODEL ----------------------------------------------------------------------- - -type Model { - Model(quote: Option(Quote)) -} - -type Quote { - Quote(author: String, content: String) -} - -fn init(_flags) -> #(Model, Effect(Msg)) { - #(Model(quote: None), effect.none()) -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - UserClickedRefresh - ApiUpdatedQuote(Result(Quote, HttpError)) -} - -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - case msg { - UserClickedRefresh -> #(model, get_quote()) - ApiUpdatedQuote(Ok(quote)) -> #(Model(quote: Some(quote)), effect.none()) - ApiUpdatedQuote(Error(_)) -> #(model, effect.none()) - } -} - -fn get_quote() -> Effect(Msg) { - let url = "https://api.quotable.io/random" - let decoder = - dynamic.decode2( - Quote, - dynamic.field("author", dynamic.string), - dynamic.field("content", dynamic.string), - ) - - lustre_http.get(url, lustre_http.expect_json(decoder, ApiUpdatedQuote)) -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")] - - ui.centre( - [attribute.style(styles)], - ui.aside( - [aside.min_width(70), attribute.style([#("width", "60ch")])], - view_quote(model.quote), - ui.button([event.on_click(UserClickedRefresh)], [ - element.text("New quote"), - ]), - ), - ) -} - -fn view_quote(quote: Option(Quote)) -> Element(msg) { - case quote { - Some(quote) -> - ui.stack([], [ - element.text(quote.author <> " once said..."), - html.p([attribute.style([#("font-style", "italic")])], [ - element.text(quote.content), - ]), - ]) - None -> html.p([], [element.text("Click the button to get a quote!")]) - } -} diff --git a/packages/lustre/examples/06-custom-effects/README.md b/packages/lustre/examples/06-custom-effects/README.md deleted file mode 100644 index 0af61bd..0000000 --- a/packages/lustre/examples/06-custom-effects/README.md +++ /dev/null @@ -1,88 +0,0 @@ -![](./header.png) - -# 06 Custom Effects - -In the last example, we showed how to use effects provided by `lustre_http`. In -this example we'll see how to create effects of our own using Lustre's -[`effect.from`](https://hexdocs.pm/lustre/lustre/effect.html#from) -function. - -Since we use effects to communicate with _external_ systems (like the browser or -the Erlang VM) you'll find creating custom effects usually involves Gleam's -[external functions](https://tour.gleam.run/everything/#advanced-features-externals). -So be sure to read up on that! - -> Gleam externals are part of its "foreign function interface", which is why -> you'll typically see files with `ffi` in the name - like -> [`app.ffi.mjs`](./src/app.ffi.mjs). - -## Accessing Browser Storage - -In this example, the external system we want to interact with is the browser's -[local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). -This way, we can write a message into the text input and it will still be there -when we refresh the page. Handy! - -The `view`, `update` and `init` functions should look pretty familiar by now, so -let's focus on the functions that generate our custom effects. - -```rust -fn read_localstorage(key: String) -> Effect(Msg) { - effect.from(fn(dispatch) { - do_read_localstorage(key) - |> CacheUpdatedMessage - |> dispatch - }) -} -``` - -We use `effect.from` by passing it a custom function that describes the effect -we want the Lustre runtime to perform. Our custom function will receive a -`dispatch` function as its only parameter - we can use that to send new messages -back to our `update` function if we want to. - -In this case, we read from local storage, pipe its value into the -`CacheUpdatedMessage` constructor, and pipe that to the `dispatch` function so -our `update` messsage can handle it. - -```rust -fn write_localstorage(key: String, value: String) -> Effect(msg) { - effect.from(fn(_) { - do_write_localstorage(key, value) - }) -} -``` - -Here, our effect is simpler. We tell the gleam compiler we don't need to use the -`dispatch` function by replacing it with a [discard -pattern](https://tour.gleam.run/everything/#basics-discard-patterns). Then we -write to local storage, and no more work needs to be done. - -You may be wondering, why bother using an effect if we aren't also going to update -our program with the result? Why not just fire off `do_write_localstorage` directly -from the `update` function or a custom event handler? - -Using effects has many benefits! It lets us implement our `update` and `view` -functions as [pure functions](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md). -This makes them easier to test, allows for time-travel debugging, and even opens -the door to easily porting them to [server components](https://hexdocs.pm/lustre/lustre/server_component.html). - -## Another note on message naming - -In our [controlled inputs -example](https://github.com/lustre-labs/lustre/tree/main/examples/03-controlled-inputs) -we touched on the idea of naming messages in a "Subject Verb Object" pattern. -This example neatly shows the benefits of taking such an approach once different -"things" start talking to your application. - -It would be easy to have a single `SetMessage` variant that both the user input -and local storage lookup use to update the model, but doing so might encourage -us to conceal the fact that the local storage lookup can fail and makes it -harder to see what things our app deals with. - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord -server](https://discord.gg/Fm8Pwmy). You could also open an issue on the [Lustre -GitHub repository](https://github.com/lustre-labs/lustre/issues). diff --git a/packages/lustre/examples/06-custom-effects/gleam.toml b/packages/lustre/examples/06-custom-effects/gleam.toml deleted file mode 100644 index 1066817..0000000 --- a/packages/lustre/examples/06-custom-effects/gleam.toml +++ /dev/null @@ -1,14 +0,0 @@ -name = "app" -version = "1.0.0" -target = "javascript" - -[dependencies] -gleam_json = "1.0.1" -gleam_stdlib = "~> 0.36" -lustre = "~> 4.0" -lustre_ui = "~> 0.4" -lustre_http = "~> 0.5" - -[dev-dependencies] -gleeunit = "~> 1.0" -lustre_dev_tools = "~> 1.0" diff --git a/packages/lustre/examples/06-custom-effects/header.png b/packages/lustre/examples/06-custom-effects/header.png deleted file mode 100644 index 1dbbdb0..0000000 Binary files a/packages/lustre/examples/06-custom-effects/header.png and /dev/null differ diff --git a/packages/lustre/examples/06-custom-effects/index.html b/packages/lustre/examples/06-custom-effects/index.html deleted file mode 100644 index 36ddf10..0000000 --- a/packages/lustre/examples/06-custom-effects/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - 🚧 app - - - - - - -
- - diff --git a/packages/lustre/examples/06-custom-effects/manifest.toml b/packages/lustre/examples/06-custom-effects/manifest.toml deleted file mode 100644 index 69c2af4..0000000 --- a/packages/lustre/examples/06-custom-effects/manifest.toml +++ /dev/null @@ -1,53 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, - { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" }, - { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" }, - { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, - { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, -] - -[requirements] -gleam_json = { version = "1.0.1" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { version = "~> 4.0" } -lustre_dev_tools = { version = "~> 1.0" } -lustre_http = { version = "~> 0.5" } -lustre_ui = { version = "~> 0.4" } diff --git a/packages/lustre/examples/06-custom-effects/src/app.ffi.mjs b/packages/lustre/examples/06-custom-effects/src/app.ffi.mjs deleted file mode 100644 index c401b99..0000000 --- a/packages/lustre/examples/06-custom-effects/src/app.ffi.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { Ok, Error } from "./gleam.mjs"; - -export function read_localstorage(key) { - const value = window.localStorage.getItem(key); - - return value ? new Ok(value) : new Error(undefined); -} - -export function write_localstorage(key, value) { - window.localStorage.setItem(key, value); -} diff --git a/packages/lustre/examples/06-custom-effects/src/app.gleam b/packages/lustre/examples/06-custom-effects/src/app.gleam deleted file mode 100644 index a6c668e..0000000 --- a/packages/lustre/examples/06-custom-effects/src/app.gleam +++ /dev/null @@ -1,84 +0,0 @@ -import gleam/option.{type Option, None, Some} -import lustre -import lustre/attribute -import lustre/effect.{type Effect} -import lustre/element.{type Element} -import lustre/event -import lustre/ui - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - let app = lustre.application(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) -} - -// MODEL ----------------------------------------------------------------------- - -type Model { - Model(message: Option(String)) -} - -fn init(_flags) -> #(Model, Effect(Msg)) { - #(Model(message: None), read_localstorage("message")) -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - UserUpdatedMessage(String) - CacheUpdatedMessage(Result(String, Nil)) -} - -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - case msg { - UserUpdatedMessage(input) -> #( - Model(message: Some(input)), - write_localstorage("message", input), - ) - CacheUpdatedMessage(Ok(message)) -> #( - Model(message: Some(message)), - effect.none(), - ) - CacheUpdatedMessage(Error(_)) -> #(model, effect.none()) - } -} - -fn read_localstorage(key: String) -> Effect(Msg) { - effect.from(fn(dispatch) { - do_read_localstorage(key) - |> CacheUpdatedMessage - |> dispatch - }) -} - -@external(javascript, "./app.ffi.mjs", "read_localstorage") -fn do_read_localstorage(_key: String) -> Result(String, Nil) { - Error(Nil) -} - -fn write_localstorage(key: String, value: String) -> Effect(msg) { - effect.from(fn(_) { do_write_localstorage(key, value) }) -} - -@external(javascript, "./app.ffi.mjs", "write_localstorage") -fn do_write_localstorage(_key: String, _value: String) -> Nil { - Nil -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [#("width", "100vw"), #("height", "100vh")] - let message = option.unwrap(model.message, "") - - ui.centre( - [attribute.style(styles)], - ui.field( - [], - [], - ui.input([attribute.value(message), event.on_input(UserUpdatedMessage)]), - [element.text("Type a message and refresh the page")], - ), - ) -} diff --git a/packages/lustre/examples/07-routing/README.md b/packages/lustre/examples/07-routing/README.md deleted file mode 100644 index 217b674..0000000 --- a/packages/lustre/examples/07-routing/README.md +++ /dev/null @@ -1,65 +0,0 @@ -![](./header.png) - -# 07 Routing - -In this example, we demonstrate basic routing using the community library [modem](https://hexdocs.pm/modem/). Modem's quickstart docs should be all you should need to get up to speed, so that's the best place to start. - -Of course, it's not much fun routing without something to route _to_. This example lets users create new pages on the fly - a guest book for your next house party! Hospitality is very important, and guests will be sure to feel welcome when they see their name in the navigation with a special greeting page just for them. - -## Using Modem - -Modem uses [custom side effects](../06-custom-effects/) and external Javascript to translate browser click and navigation events into `update` messages for the Lustre runtime. All we need to use it is a [route change handler function](./src/app.gleam#L59) that we can pass to `modem.init` in our app's `init` function. - -> _Note:_ See [`modem.advanced`](https://hexdocs.pm/modem/modem.html#advanced) to configure more options. - -Inside our `on_route_change` function, we match URI path segment patterns to our routes: - -```gleam - fn on_route_change(uri: Uri) -> Msg { - case uri.path_segments(uri.path) { - ["welcome", guest] -> OnRouteChange(WelcomeGuest(guest)) - _ -> OnRouteChange(Home) - } - } -``` - -In our `update` function, we assign the matched route to `model.current_route`: - -```gleam -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - case msg { - OnRouteChange(route) -> #( - Model(..model, current_route: route), - effect.none(), - ) - ... - } -``` - -And in our `view` function, we use `model.current_route` to determine what to display to the user: - -```gleam -fn view(model: Model) -> Element(Msg) { - let page = case model.current_route { - Home -> render_home(model) - WelcomeGuest(name) -> render_welcome(model, name) - } - ... -} -``` - -## Views: They're just functions! - -Lustre doesn't provide a traditional HTML or JSX-style templating engine, and this is by design. - -Since the `view` portion of this example is a bit more involved than our previous ones have been, it should start to give you more of a feel for how views in Lustre are _just functions_. The layout is a function. Each page view is a function. The nav is a function, and each individual nav _item_ is a function too. - -This means we can build up our entire UI using Gleam's functional syntax, benefiting from features like exhaustive pattern matching based on our routes. - -Since our views are [pure functions](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md), we know they'll always render reliably. - -## Getting help - -If you're having trouble with Lustre or are not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). diff --git a/packages/lustre/examples/07-routing/gleam.toml b/packages/lustre/examples/07-routing/gleam.toml deleted file mode 100644 index 58f37ee..0000000 --- a/packages/lustre/examples/07-routing/gleam.toml +++ /dev/null @@ -1,15 +0,0 @@ -name = "app" -version = "1.0.0" -target = "javascript" - -[dependencies] -gleam_json = "1.0.1" -gleam_stdlib = "~> 0.36" -lustre = "~> 4.0" -lustre_ui = "~> 0.4" -lustre_http = "~> 0.5.2" -modem = "~> 1.0" - -[dev-dependencies] -gleeunit = "~> 1.0" -lustre_dev_tools = "~> 1.1" diff --git a/packages/lustre/examples/07-routing/header.png b/packages/lustre/examples/07-routing/header.png deleted file mode 100644 index f903c89..0000000 Binary files a/packages/lustre/examples/07-routing/header.png and /dev/null differ diff --git a/packages/lustre/examples/07-routing/index.html b/packages/lustre/examples/07-routing/index.html deleted file mode 100644 index 36ddf10..0000000 --- a/packages/lustre/examples/07-routing/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - 🚧 app - - - - - - -
- - diff --git a/packages/lustre/examples/07-routing/manifest.toml b/packages/lustre/examples/07-routing/manifest.toml deleted file mode 100644 index a02e147..0000000 --- a/packages/lustre/examples/07-routing/manifest.toml +++ /dev/null @@ -1,55 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, - { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "9ABD71D63F4B8F362CB824DED2C4CA64895DEFACD8F22B0FF055BF15241B1AE2" }, - { name = "lustre_dev_tools", version = "1.3.3", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "67B4E62DAD9B8323487AAA697A6F3FA72348B6DEA6674D65D4F7A1407CF377ED" }, - { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "modem", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "4C6E448089B09A57C179455D44526A717E4E217D4000B91201617FD2D9F18E68" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, - { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, -] - -[requirements] -gleam_json = { version = "1.0.1" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { version = "~> 4.0" } -lustre_dev_tools = { version = "~> 1.1" } -lustre_http = { version = "~> 0.5.2" } -lustre_ui = { version = "~> 0.4" } -modem = { version = "~> 1.0" } diff --git a/packages/lustre/examples/07-routing/src/app.gleam b/packages/lustre/examples/07-routing/src/app.gleam deleted file mode 100644 index 5b65d51..0000000 --- a/packages/lustre/examples/07-routing/src/app.gleam +++ /dev/null @@ -1,169 +0,0 @@ -import gleam/dynamic -import gleam/list -import gleam/result -import gleam/string -import gleam/uri.{type Uri} -import lustre -import lustre/attribute -import lustre/effect.{type Effect} -import lustre/element.{type Element} -import lustre/element/html -import lustre/event -import lustre/ui -import lustre/ui/layout/cluster -import lustre/ui/util/cn -import modem - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - let app = lustre.application(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) -} - -// MODEL ----------------------------------------------------------------------- - -type Model { - Model(current_route: Route, guests: List(Guest), new_guest_name: String) -} - -type Route { - Home - WelcomeGuest(value: String) -} - -type Guest { - Guest(slug: String, name: String) -} - -fn init(_flags) -> #(Model, Effect(Msg)) { - #( - Model( - current_route: Home, - guests: [ - Guest(slug: "chihiro", name: "Chihiro"), - Guest(slug: "totoro", name: "Totoro"), - ], - new_guest_name: "", - ), - modem.init(on_route_change), - ) -} - -fn on_route_change(uri: Uri) -> Msg { - case uri.path_segments(uri.path) { - ["welcome", guest] -> OnRouteChange(WelcomeGuest(guest)) - _ -> OnRouteChange(Home) - } -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - OnRouteChange(Route) - UserUpdatedNewGuestName(String) - UserAddedNewGuest(Guest) -} - -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - case msg { - OnRouteChange(route) -> #( - Model(..model, current_route: route), - effect.none(), - ) - UserUpdatedNewGuestName(name) -> #( - Model(..model, new_guest_name: name), - effect.none(), - ) - UserAddedNewGuest(guest) -> #( - Model( - ..model, - guests: list.append(model.guests, [guest]), - new_guest_name: "", - ), - effect.none(), - ) - } -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [#("margin", "15vh")] - - let page = case model.current_route { - Home -> view_home(model) - WelcomeGuest(name) -> view_welcome(model, name) - } - - ui.stack([attribute.style(styles)], [view_nav(model), page]) -} - -fn view_home(model: Model) { - let new_guest_input = fn(event) { - use key_code <- result.try(dynamic.field("key", dynamic.string)(event)) - case key_code { - "Enter" -> { - let guest_slug = - model.new_guest_name - |> string.replace(" ", "-") - |> string.lowercase - Ok( - UserAddedNewGuest(Guest(name: model.new_guest_name, slug: guest_slug)), - ) - } - _ -> { - use value <- result.try(event.value(event)) - Ok(UserUpdatedNewGuestName(value)) - } - } - } - - view_body([ - view_title("Welcome to the Party 🏡"), - html.p([], [element.text("Please sign the guest book:")]), - ui.input([ - event.on("keyup", new_guest_input), - attribute.value(model.new_guest_name), - ]), - ]) -} - -fn view_welcome(model: Model, slug) -> Element(a) { - let guest = - model.guests - |> list.find(fn(guest: Guest) { guest.slug == slug }) - - let title = case guest { - Ok(guest) -> view_title("Hello, " <> guest.name <> "! 🎉") - _ -> view_title("Sorry ... didn't quite catch that.") - } - - view_body([title]) -} - -fn view_nav(model: Model) -> Element(a) { - let item_styles = [#("text-decoration", "underline")] - - let view_nav_item = fn(path, text) { - html.a([attribute.href("/" <> path), attribute.style(item_styles)], [ - element.text(text), - ]) - } - - let guest_nav_items = - model.guests - |> list.map(fn(guest: Guest) { - view_nav_item("welcome/" <> guest.slug, guest.name) - }) - - cluster.of(html.nav, [], [view_nav_item("", "Home"), ..guest_nav_items]) -} - -fn view_body(children) { - ui.centre([cn.mt_xl()], ui.stack([], children)) -} - -fn view_title(text) { - html.h1([cn.text_xl()], [element.text(text)]) -} diff --git a/packages/lustre/examples/99-full-stack-applications/client/index.html b/packages/lustre/examples/99-full-stack-applications/client/index.html deleted file mode 100644 index da137b1..0000000 --- a/packages/lustre/examples/99-full-stack-applications/client/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - 🚧 app - - - - - -
- - diff --git a/packages/lustre/examples/99-full-stack-applications/client/manifest.toml b/packages/lustre/examples/99-full-stack-applications/client/manifest.toml deleted file mode 100644 index 4d627ad..0000000 --- a/packages/lustre/examples/99-full-stack-applications/client/manifest.toml +++ /dev/null @@ -1,54 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "B1FA529E7BE3FF12CADF32814AB8EC7294E74CEDEE8CC734505707B929A98985" }, - { name = "decipher", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_json", "gleam_stdlib", "stoiridh_version"], otp_app = "decipher", source = "hex", outer_checksum = "9F1B5C6FF0D798046E4E0EF87D09DD729324CB72BD7F0D4152B797324D51223E" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, - { name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "lustre", version = "4.2.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "09B94E1380CBC400DCD594B36A845E5CB2E143DF89E95460B2CA59E44499CAC9" }, - { name = "lustre_dev_tools", version = "1.3.2", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "CC8F46BCE51C1349862C5F6BA0075B0C68096B866ED1C520B60358FAAB398B60" }, - { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" }, - { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "stoiridh_version", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "stoiridh_version", source = "hex", outer_checksum = "298ABEA44DF37764A34C2E9190A84BF2770BC59DD9397C6DC7708040E5A0142B" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, - { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, -] - -[requirements] -decipher = { version = ">= 1.2.0 and < 2.0.0"} -gleam_json = { version = ">= 1.0.1 and < 2.0.0" } -gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } -lustre = { version = ">= 4.2.4 and < 5.0.0" } -lustre_dev_tools = { version = ">= 1.3.2 and < 2.0.0" } -lustre_http = { version = ">= 0.5.2 and < 1.0.0" } diff --git a/packages/lustre/examples/99-full-stack-applications/client/src/app.gleam b/packages/lustre/examples/99-full-stack-applications/client/src/app.gleam deleted file mode 100644 index aae4530..0000000 --- a/packages/lustre/examples/99-full-stack-applications/client/src/app.gleam +++ /dev/null @@ -1,127 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import decipher -import gleam/dynamic.{dynamic} -import gleam/int -import gleam/list -import gleam/result -import lustre -import lustre/attribute -import lustre/effect.{type Effect} -import lustre/element.{type Element} -import lustre/element/html -import lustre/event - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - let app = lustre.application(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) - - Nil -} - -// MODEL ----------------------------------------------------------------------- - -type Model = - List(#(String, Int)) - -fn init(_) -> #(Model, Effect(Msg)) { - let model = [] - let effect = effect.none() - - #(model, effect) -} - -// UPDATE ---------------------------------------------------------------------- - -type Msg { - ServerSavedList(Result(Nil, String)) - UserAddedProduct(name: String) - UserSavedList - UserUpdatedQuantity(name: String, amount: Int) -} - -fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - case msg { - ServerSavedList(_) -> #(model, effect.none()) - UserAddedProduct(name) -> #([#(name, 1), ..model], effect.none()) - UserSavedList -> #(model, effect.none()) - UserUpdatedQuantity(name, quantity) -> { - let model = - list.map(model, fn(item) { - case item.0 == name { - True -> #(name, quantity) - False -> item - } - }) - - #(model, effect.none()) - } - } -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [ - #("max-width", "30ch"), - #("margin", "0 auto"), - #("display", "flex"), - #("flex-direction", "column"), - #("gap", "1em"), - ] - - html.div([attribute.style(styles)], [ - view_grocery_list(model), - view_new_item(), - html.div([], [html.button([], [html.text("Sync")])]), - ]) -} - -fn view_new_item() -> Element(Msg) { - let handle_click = fn(event) { - let path = ["target", "previousElementSibling", "value"] - - event - |> decipher.at(path, dynamic.string) - |> result.map(UserAddedProduct) - } - - html.div([], [ - html.input([]), - html.button([event.on("click", handle_click)], [html.text("Add")]), - ]) -} - -fn view_grocery_list(model: Model) -> Element(Msg) { - let styles = [#("display", "flex"), #("flex-direction", "column-reverse")] - - element.keyed(html.div([attribute.style(styles)], _), { - use #(name, quantity) <- list.map(model) - let item = view_grocery_item(name, quantity) - - #(name, item) - }) -} - -fn view_grocery_item(name: String, quantity: Int) -> Element(Msg) { - let handle_input = fn(e) { - event.value(e) - |> result.nil_error - |> result.then(int.parse) - |> result.map(UserUpdatedQuantity(name, _)) - |> result.replace_error([]) - } - - html.div([attribute.style([#("display", "flex"), #("gap", "1em")])], [ - html.span([attribute.style([#("flex", "1")])], [html.text(name)]), - html.input([ - attribute.style([#("width", "4em")]), - attribute.type_("number"), - attribute.value(int.to_string(quantity)), - attribute.min("0"), - event.on("input", handle_input), - ]), - ]) -} diff --git a/packages/lustre/examples/99-full-stack-applications/server/manifest.toml b/packages/lustre/examples/99-full-stack-applications/server/manifest.toml deleted file mode 100644 index 2a7bc47..0000000 --- a/packages/lustre/examples/99-full-stack-applications/server/manifest.toml +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, -] - -[requirements] -gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/packages/lustre/examples/99-full-stack-applications/server/src/app.gleam b/packages/lustre/examples/99-full-stack-applications/server/src/app.gleam deleted file mode 100644 index 452ccb7..0000000 --- a/packages/lustre/examples/99-full-stack-applications/server/src/app.gleam +++ /dev/null @@ -1,5 +0,0 @@ -import gleam/io - -pub fn main() { - io.println("Hello from app!") -} diff --git a/packages/lustre/examples/99-full-stack-applications/shared/manifest.toml b/packages/lustre/examples/99-full-stack-applications/shared/manifest.toml deleted file mode 100644 index 2a7bc47..0000000 --- a/packages/lustre/examples/99-full-stack-applications/shared/manifest.toml +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, -] - -[requirements] -gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/packages/lustre/examples/99-full-stack-applications/shared/src/shared.gleam b/packages/lustre/examples/99-full-stack-applications/shared/src/shared.gleam deleted file mode 100644 index 4b3811b..0000000 --- a/packages/lustre/examples/99-full-stack-applications/shared/src/shared.gleam +++ /dev/null @@ -1,5 +0,0 @@ -import gleam/io - -pub fn main() { - io.println("Hello from shared!") -} diff --git a/packages/lustre/examples/99-server-components/README.md b/packages/lustre/examples/99-server-components/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/packages/lustre/examples/99-server-components/gleam.toml b/packages/lustre/examples/99-server-components/gleam.toml deleted file mode 100644 index 1d640d2..0000000 --- a/packages/lustre/examples/99-server-components/gleam.toml +++ /dev/null @@ -1,27 +0,0 @@ -name = "app" -version = "1.0.0" - -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -# description = "" -# licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] -# -# For a full reference of all the available options, you can have a look at -# https://gleam.run/writing-gleam/gleam-toml/. - -[dependencies] -gleam_stdlib = "~> 0.36" -lustre = { path = "../../" } -mist = "~> 1.0" -gleam_erlang = "~> 0.24" -gleam_otp = "~> 0.10" -gleam_http = "~> 3.6" -lustre_ui = "~> 0.4" -gleam_json = "~> 1.0" -simplifile = "~> 1.5" - -[dev-dependencies] -gleeunit = "~> 1.0" diff --git a/packages/lustre/examples/99-server-components/manifest.toml b/packages/lustre/examples/99-server-components/manifest.toml deleted file mode 100644 index aea8226..0000000 --- a/packages/lustre/examples/99-server-components/manifest.toml +++ /dev/null @@ -1,37 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "lustre", version = "4.2.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], source = "local", path = "../.." }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, -] - -[requirements] -gleam_erlang = { version = "~> 0.24" } -gleam_http = { version = "~> 3.6" } -gleam_json = { version = "~> 1.0" } -gleam_otp = { version = "~> 0.10" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { path = "../../" } -lustre_ui = { version = "~> 0.4" } -mist = { version = "~> 1.0" } -simplifile = { version = "~> 1.5" } diff --git a/packages/lustre/examples/99-server-components/src/app.gleam b/packages/lustre/examples/99-server-components/src/app.gleam deleted file mode 100644 index bc3227f..0000000 --- a/packages/lustre/examples/99-server-components/src/app.gleam +++ /dev/null @@ -1,168 +0,0 @@ -import counter -import gleam/bytes_builder -import gleam/erlang -import gleam/erlang/process.{type Selector, type Subject} -import gleam/http/request.{type Request} -import gleam/http/response.{type Response} -import gleam/json -import gleam/option.{type Option, None, Some} -import gleam/otp/actor -import gleam/result -import lustre -import lustre/attribute -import lustre/element.{element} -import lustre/element/html.{html} -import lustre/server_component -import mist.{ - type Connection, type ResponseData, type WebsocketConnection, - type WebsocketMessage, -} - -pub fn main() { - let assert Ok(_) = - fn(req: Request(Connection)) -> Response(ResponseData) { - case request.path_segments(req) { - // Set up the websocket connection to the client. This is how we send - // DOM updates to the browser and receive events from the client. - ["counter"] -> - mist.websocket( - request: req, - on_init: socket_init, - on_close: socket_close, - handler: socket_update, - ) - - // We need to serve the server component runtime. There's also a minified - // version of this script for production. - ["lustre-server-component.mjs"] -> { - let assert Ok(priv) = erlang.priv_directory("lustre") - let path = priv <> "/static/lustre-server-component.mjs" - - mist.send_file(path, offset: 0, limit: None) - |> result.map(fn(script) { - response.new(200) - |> response.prepend_header("content-type", "application/javascript") - |> response.set_body(script) - }) - |> result.lazy_unwrap(fn() { - response.new(404) - |> response.set_body(mist.Bytes(bytes_builder.new())) - }) - } - - // For all other requests we'll just serve some HTML that renders the - // server component. - _ -> - response.new(200) - |> response.prepend_header("content-type", "text/html") - |> response.set_body( - html([], [ - html.head([], [ - html.link([ - attribute.rel("stylesheet"), - attribute.href( - "https://cdn.jsdelivr.net/gh/lustre-labs/ui/priv/styles.css", - ), - ]), - html.script( - [ - attribute.type_("module"), - attribute.src("/lustre-server-component.mjs"), - ], - "", - ), - ]), - html.body([], [ - element( - "lustre-server-component", - [server_component.route("/counter")], - [html.p([], [html.text("This is a slot")])], - ), - ]), - ]) - |> element.to_document_string_builder - |> bytes_builder.from_string_builder - |> mist.Bytes, - ) - } - } - |> mist.new - |> mist.port(3000) - |> mist.start_http - - process.sleep_forever() -} - -// - -type Counter = - Subject(lustre.Action(counter.Msg, lustre.ServerComponent)) - -fn socket_init( - conn: WebsocketConnection, -) -> #(Counter, Option(Selector(lustre.Patch(counter.Msg)))) { - let self = process.new_subject() - let app = counter.app() - let assert Ok(counter) = lustre.start_actor(app, 0) - - process.send( - counter, - server_component.subscribe( - // server components can have many connected clients, so we need a way to - // identify this client. - "ws", - // this callback is called whenever the server component has a new patch - // to send to the client. here we json encode that patch and send it to - // via the websocket connection. - // - // a more involved version would have us sending the patch to this socket's - // subject, and then it could be handled (perhaps with some other work) in - // the `mist.Custom` branch of `socket_update` below. - process.send(self, _), - ), - ) - - #( - // we store the server component's `Subject` as this socket's state so we - // can shut it down when the socket is closed. - counter, - Some(process.selecting(process.new_selector(), self, fn(a) { a })), - ) -} - -fn socket_update( - counter: Counter, - conn: WebsocketConnection, - msg: WebsocketMessage(lustre.Patch(counter.Msg)), -) { - case msg { - mist.Text(json) -> { - // we attempt to decode the incoming text as an action to send to our - // server component runtime. - let action = json.decode(json, server_component.decode_action) - - case action { - Ok(action) -> process.send(counter, action) - Error(_) -> Nil - } - - actor.continue(counter) - } - - mist.Binary(_) -> actor.continue(counter) - mist.Custom(patch) -> { - let assert Ok(_) = - patch - |> server_component.encode_patch - |> json.to_string - |> mist.send_text_frame(conn, _) - - actor.continue(counter) - } - mist.Closed | mist.Shutdown -> actor.Stop(process.Normal) - } -} - -fn socket_close(counter: Counter) { - process.send(counter, lustre.shutdown()) -} diff --git a/packages/lustre/examples/99-server-components/src/counter.gleam b/packages/lustre/examples/99-server-components/src/counter.gleam deleted file mode 100644 index 0c9f115..0000000 --- a/packages/lustre/examples/99-server-components/src/counter.gleam +++ /dev/null @@ -1,58 +0,0 @@ -import gleam/int -import lustre -import lustre/attribute -import lustre/element.{type Element} -import lustre/element/html -import lustre/event -import lustre/ui - -// MAIN ------------------------------------------------------------------------ - -pub fn app() { - lustre.simple(init, update, view) -} - -// MODEL ----------------------------------------------------------------------- - -type Model = - Int - -fn init(initial_count: Int) -> Model { - case initial_count < 0 { - True -> 0 - False -> initial_count - } -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - Incr - Decr -} - -fn update(model: Model, msg: Msg) -> Model { - case msg { - Incr -> model + 1 - Decr -> model - 1 - } -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")] - let count = int.to_string(model) - - ui.centre( - [attribute.style(styles)], - ui.stack([], [ - ui.button([event.on_click(Incr)], [element.text("+")]), - html.slot([]), - html.p([attribute.style([#("text-align", "center")])], [ - element.text(count), - ]), - ui.button([event.on_click(Decr)], [element.text("-")]), - ]), - ) -} diff --git a/packages/lustre/examples/README.md b/packages/lustre/examples/README.md deleted file mode 100644 index c3a6009..0000000 --- a/packages/lustre/examples/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Lustre examples - -Each of the examples in this directory is a self-contained, complete Gleam app -that demonstrates a particular feature or concept of the library. For newcomers, -we recommend looking through them in order, as each example tends to build on -the previous ones. Feel free to jump in to any example that interests you, though! - -> **Note**: these examples all use [`lustre/ui`](https://github.com/lustre-labs/ui) -> to show off something a little more visually interesting than unstyled HTML. None -> of the ideas in these examples are specific to `lustre/ui`, though, and you should -> know that you can follow along with any of these examples using only the standard -> `lustre/element/html` module. - -## Examples - -- [`01-hello-world`](./01-hello-world) is a simple example to just get something - on the screen. - -- [`02-interactivity`](./02-interactivity) introduces the core Model-View-Update - loop that underpins every Lustre application. - -- [`03-controlled-inputs`](./03-controlled-inputs) demonstrates the most common - way to handle `` elements in Lustre. - -- [`04-custom-event-handlers`](./04-custom-event-handlers) shows you how to - write your own event handlers and decoders, instead of relying on the ones - provided by `lustre/event`. - -- [`05-http-requests`](./05-http-requests) demonstrates how side effects are - handled in Lustre, using the third-party [`lustre_http`](https://hexdocs.pm/lustre_http/) - package. - -- [`06-custom-effects`](./06-custom-effects) builds on the previous example and - shows you how to write your own side effects for Lustre to perform. - -- [`07-routing`](./07-routing) shows you how to use [`modem`](https://hexdocs.pm/modem/) - to set up routing and navigating between pages in a Lustre app. - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). - -While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org) -is also a great resource for learning about the Model-View-Update architecture -and the kinds of patterns that Lustre is built around. diff --git a/packages/lustre/gleam.toml b/packages/lustre/gleam.toml deleted file mode 100644 index 8996318..0000000 --- a/packages/lustre/gleam.toml +++ /dev/null @@ -1,49 +0,0 @@ -name = "lustre" -version = "4.3.0" -gleam = ">= 1.0.0" - -description = "An Elm-inspired framework for building HTML templates, single page applications, and server-rendered components in Gleam!" -repository = { type = "github", user = "lustre-labs", repo = "lustre" } -licences = ["MIT"] - -links = [ - { title = "Sponsor", href = "https://github.com/sponsors/hayleigh-dot-dev" }, -] - -internal_modules = ["lustre/internals", "lustre/internals/*"] - -[documentation] -pages = [ - { title = "Examples directory", path = "reference/examples.html", source = "./pages/reference/examples.md" }, - { title = " ", path = "#", source = "" }, - { title = "Quickstart guide", path = "guide/01-quickstart.html", source = "./pages/guide/01-quickstart.md" }, - { title = "Managing state", path = "guide/02-state-management.html", source = "./pages/guide/02-state-management.md" }, - { title = "Side effects", path = "guide/03-side-effects.html", source = "./pages/guide/03-side-effects.md" }, - { title = "Server-side rendering", path = "guide/04-server-side-rendering.html", source = "./pages/guide/04-server-side-rendering.md" }, - # { title = "Full-stack apps", path = "guide/05-full-stack-apps.html", source = "./pages/guide/05-full-stack-apps.md" }, - # { title = "Components", path = "#", source = "" }, - # { title = "Server components", path = "#", source = "" }, - # { title = " ", path = "#", source = "" }, - # { title = "SPA deployments", path = "#", source = "" }, - # { title = "Full-stack deployments", path = "#", source = "" }, - # { title = " ", path = "#", source = "" }, - # { title = "Using with Wisp", path = "#", source = "" }, - # { title = "Using with Glen", path = "#", source = "" }, - # { title = "Using with Mist", path = "#", source = "" }, - # { title = " ", path = "#", source = "" }, - { title = "For Elm developers", path = "cheatsheets/elm", source = "./pages/reference/for-elm-devs.md" }, - { title = "For React developers", path = "cheatsheets/react", source = "./pages/reference/for-react-devs.md" }, - { title = "For LiveView developers", path = "cheatsheets/liveview", source = "./pages/reference/for-liveview-devs.md" }, -] - -[dependencies] -gleam_erlang = "~> 0.24" -gleam_json = "~> 1.0 or ~> 2.0" -gleam_otp = "~> 0.9" -gleam_stdlib = "~> 0.36" - -[dev-dependencies] -birdie = "~> 1.0" -gleeunit = "~> 1.0" -shellout = "~> 1.6" -simplifile = "~> 1.4" diff --git a/packages/lustre/manifest.toml b/packages/lustre/manifest.toml deleted file mode 100644 index cc9d928..0000000 --- a/packages/lustre/manifest.toml +++ /dev/null @@ -1,33 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birdie", version = "1.1.5", build_tools = ["gleam"], requirements = ["argv", "filepath", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "justin", "rank", "simplifile", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "E1B6CB7B9EDE8F4C67F7E68C9FB45FBAA54881545F85D315D2B179560CC63F60" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "glance", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "8F3314D27773B7C3B9FB58D8C02C634290422CE531988C0394FA0DF8676B964D" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glexer", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "BD477AD657C2B637FEF75F2405FAEFFA533F277A74EF1A5E17B55B1178C228FB" }, - { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, - { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, - { name = "shellout", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "E2FCD18957F0E9F67E1F497FC9FF57393392F8A9BAEAEA4779541DE7A68DD7E0" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "thoas", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1" }, - { name = "trie_again", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "5B19176F52B1BD98831B57FDC97BD1F88C8A403D6D8C63471407E78598E27184" }, -] - -[requirements] -birdie = { version = "~> 1.0" } -gleam_erlang = { version = "~> 0.24" } -gleam_json = { version = "~> 1.0 or ~> 2.0" } -gleam_otp = { version = "~> 0.9" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -shellout = { version = "~> 1.6" } -simplifile = { version = "~> 1.4" } diff --git a/packages/lustre/package-lock.json b/packages/lustre/package-lock.json deleted file mode 100644 index c11246c..0000000 --- a/packages/lustre/package-lock.json +++ /dev/null @@ -1,3170 +0,0 @@ -{ - "name": "lustre-client-test", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "lustre-client-test", - "version": "0.1.0", - "license": "MIT", - "devDependencies": { - "esbuild": "^0.20.2", - "linkedom": "^0.16.11", - "npm-run-all": "^4.1.5", - "vitest": "^1.5.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.4.tgz", - "integrity": "sha512-GkhjAaQ8oUTOKE4g4gsZ0u8K/IHU1+2WQSgS1TwTcYvL+sjbaQjNHFXbOJ6kgqGHIO1DfUhI/Sphi9GkRT9K+Q==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.4.tgz", - "integrity": "sha512-Bvm6D+NPbGMQOcxvS1zUl8H7DWlywSXsphAeOnVeiZLQ+0J6Is8T7SrjGTH29KtYkiY9vld8ZnpV3G2EPbom+w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.4.tgz", - "integrity": "sha512-i5d64MlnYBO9EkCOGe5vPR/EeDwjnKOGGdd7zKFhU5y8haKhQZTN2DgVtpODDMxUr4t2K90wTUJg7ilgND6bXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.4.tgz", - "integrity": "sha512-WZupV1+CdUYehaZqjaFTClJI72fjJEgTXdf4NbW69I9XyvdmztUExBtcI2yIIU6hJtYvtwS6pkTkHJz+k08mAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.4.tgz", - "integrity": "sha512-ADm/xt86JUnmAfA9mBqFcRp//RVRt1ohGOYF6yL+IFCYqOBNwy5lbEK05xTsEoJq+/tJzg8ICUtS82WinJRuIw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.4.tgz", - "integrity": "sha512-tJfJaXPiFAG+Jn3cutp7mCs1ePltuAgRqdDZrzb1aeE3TktWWJ+g7xK9SNlaSUFw6IU4QgOxAY4rA+wZUT5Wfg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.4.tgz", - "integrity": "sha512-7dy1BzQkgYlUTapDTvK997cgi0Orh5Iu7JlZVBy1MBURk7/HSbHkzRnXZa19ozy+wwD8/SlpJnOOckuNZtJR9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.4.tgz", - "integrity": "sha512-zsFwdUw5XLD1gQe0aoU2HVceI6NEW7q7m05wA46eUAyrkeNYExObfRFQcvA6zw8lfRc5BHtan3tBpo+kqEOxmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.4.tgz", - "integrity": "sha512-p8C3NnxXooRdNrdv6dBmRTddEapfESEUflpICDNKXpHvTjRRq1J82CbU5G3XfebIZyI3B0s074JHMWD36qOW6w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.4.tgz", - "integrity": "sha512-Lh/8ckoar4s4Id2foY7jNgitTOUQczwMWNYi+Mjt0eQ9LKhr6sK477REqQkmy8YHY3Ca3A2JJVdXnfb3Rrwkng==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.4.tgz", - "integrity": "sha512-1xwwn9ZCQYuqGmulGsTZoKrrn0z2fAur2ujE60QgyDpHmBbXbxLaQiEvzJWDrscRq43c8DnuHx3QorhMTZgisQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.4.tgz", - "integrity": "sha512-LuOGGKAJ7dfRtxVnO1i3qWc6N9sh0Em/8aZ3CezixSTM+E9Oq3OvTsvC4sm6wWjzpsIlOCnZjdluINKESflJLA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.4.tgz", - "integrity": "sha512-ch86i7KkJKkLybDP2AtySFTRi5fM3KXp0PnHocHuJMdZwu7BuyIKi35BE9guMlmTpwwBTB3ljHj9IQXnTCD0vA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.4.tgz", - "integrity": "sha512-Ma4PwyLfOWZWayfEsNQzTDBVW8PZ6TUUN1uFTBQbF2Chv/+sjenE86lpiEwj2FiviSmSZ4Ap4MaAfl1ciF4aSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.4.tgz", - "integrity": "sha512-9m/ZDrQsdo/c06uOlP3W9G2ENRVzgzbSXmXHT4hwVaDQhYcRpi9bgBT0FTG9OhESxwK0WjQxYOSfv40cU+T69w==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.4.tgz", - "integrity": "sha512-YunpoOAyGLDseanENHmbFvQSfVL5BxW3k7hhy0eN4rb3gS/ct75dVD0EXOWIqFT/nE8XYW6LP6vz6ctKRi0k9A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/@vitest/expect": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.2.tgz", - "integrity": "sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==", - "dev": true, - "dependencies": { - "@vitest/spy": "1.5.2", - "@vitest/utils": "1.5.2", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.2.tgz", - "integrity": "sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==", - "dev": true, - "dependencies": { - "@vitest/utils": "1.5.2", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.2.tgz", - "integrity": "sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==", - "dev": true, - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.2.tgz", - "integrity": "sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==", - "dev": true, - "dependencies": { - "tinyspy": "^2.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.2.tgz", - "integrity": "sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==", - "dev": true, - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/execa/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/execa/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/execa/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/execa/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/html-escaper": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", - "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", - "dev": true - }, - "node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dev": true, - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", - "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", - "dev": true - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/linkedom": { - "version": "0.16.11", - "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.16.11.tgz", - "integrity": "sha512-WgaTVbj7itjyXTsCvgerpneERXShcnNJF5VIV+/4SLtyRLN+HppPre/WDHRofAr2IpEuujSNgJbCBd5lMl6lRw==", - "dev": true, - "dependencies": { - "css-select": "^5.1.0", - "cssom": "^0.5.0", - "html-escaper": "^3.0.3", - "htmlparser2": "^9.1.0", - "uhyphen": "^0.2.0" - } - }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "node_modules/memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mlly": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", - "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" - }, - "bin": { - "npm-run-all": "bin/npm-run-all/index.js", - "run-p": "bin/run-p/index.js", - "run-s": "bin/run-s/index.js" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/pidtree": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", - "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", - "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", - "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", - "dev": true, - "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.6.1", - "pathe": "^1.1.2" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-is": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.0.tgz", - "integrity": "sha512-wRiUsea88TjKDc4FBEn+sLvIDesp6brMbGWnJGjew2waAc9evdhja/2LvePc898HJbHw0L+MTWy7NhpnELAvLQ==", - "dev": true - }, - "node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", - "dev": true, - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rollup": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.4.tgz", - "integrity": "sha512-kuaTJSUbz+Wsb2ATGvEknkI12XV40vIiHmLuFlejoo7HtDok/O5eDDD0UpCVY5bBX5U5RYo8wWP83H7ZsqVEnA==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.16.4", - "@rollup/rollup-android-arm64": "4.16.4", - "@rollup/rollup-darwin-arm64": "4.16.4", - "@rollup/rollup-darwin-x64": "4.16.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.16.4", - "@rollup/rollup-linux-arm-musleabihf": "4.16.4", - "@rollup/rollup-linux-arm64-gnu": "4.16.4", - "@rollup/rollup-linux-arm64-musl": "4.16.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.16.4", - "@rollup/rollup-linux-riscv64-gnu": "4.16.4", - "@rollup/rollup-linux-s390x-gnu": "4.16.4", - "@rollup/rollup-linux-x64-gnu": "4.16.4", - "@rollup/rollup-linux-x64-musl": "4.16.4", - "@rollup/rollup-win32-arm64-msvc": "4.16.4", - "@rollup/rollup-win32-ia32-msvc": "4.16.4", - "@rollup/rollup-win32-x64-msvc": "4.16.4", - "fsevents": "~2.3.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", - "dev": true - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "node_modules/std-env": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", - "dev": true - }, - "node_modules/string.prototype.padend": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", - "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", - "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tinybench": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", - "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", - "dev": true - }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true - }, - "node_modules/uhyphen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", - "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", - "dev": true - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vite": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz", - "integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==", - "dev": true, - "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.2.tgz", - "integrity": "sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.2.tgz", - "integrity": "sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==", - "dev": true, - "dependencies": { - "@vitest/expect": "1.5.2", - "@vitest/runner": "1.5.2", - "@vitest/snapshot": "1.5.2", - "@vitest/spy": "1.5.2", - "@vitest/utils": "1.5.2", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.5.2", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.5.2", - "@vitest/ui": "1.5.2", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/packages/lustre/package.json b/packages/lustre/package.json deleted file mode 100644 index a333d7a..0000000 --- a/packages/lustre/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "lustre-client-test", - "version": "0.1.0", - "description": "testing for client scripts, and using vitest experimental bench", - "scripts": { - "bench": "run-s build:bench bench:vitest", - "bench:vitest": "vitest bench --config ./vitest.config.js", - "build": "run-p build:test:**", - "build:bench": "run-s build:test:vdom", - "build:test:02": "cd examples/02-interactivity && gleam build", - "build:test:vdom": "cd test-apps/vdom-test-templates && gleam build", - "run:vitest": "vitest --config ./vitest.config.js", - "test": "run-s build \"run:vitest -- --run\"", - "test:02": "run-s build:test:02 \"run:vitest -- --run 02-interactivity.test\"", - "test:vdom": "run-s build:test:vdom \"run:vitest -- --run vdom.ffi \"", - "watch:test": "run-p \"watch:init:**\"", - "watch:init:build": "run-p build:test:**", - "watch:init:vitest": "run-s run:vitest" - }, - "author": "Jacob Scearcy", - "license": "MIT", - "devDependencies": { - "esbuild": "^0.20.2", - "linkedom": "^0.16.11", - "npm-run-all": "^4.1.5", - "vitest": "^1.5.0" - }, - "dependencies": { - "@chouqueth/gleam": "^1.3.2" - } -} diff --git a/packages/lustre/pages/guide/01-quickstart.md b/packages/lustre/pages/guide/01-quickstart.md deleted file mode 100644 index 6713967..0000000 --- a/packages/lustre/pages/guide/01-quickstart.md +++ /dev/null @@ -1,422 +0,0 @@ -# 01 Quickstart guide - -Welcome to the Lustre quickstart guide! This document should get you up to speed -with the core ideas that underpin every Lustre application as well as how to get -something on the screen. - -## What is a SPA? - -Lustre can be used to create HTML in many different contexts, but it is primarily -designed to be used to build Single-Page Applications – or SPAs. SPAs are a type -of Web application that render content primarily in the browser (rather than on -the server) and, crucially, do not require a full page load when navigating -between pages or loading new content. - -To help build these kinds of applications, Lustre comes with an opinionated -runtime. Some of Lustre's core features include: - -- **Declarative rendering**: User interfaces are constructed using a declarative - API that describes HTML as a function of your application's state. This is in - contrast to more traditional imperative approaches to direct DOM mutation like - jQuery. - -- **State management**: If UIs are a function of state, then orchestrating state - changes is crucial! Lustre provides a simple message-based state management - system modelled after OTP [gen_servers](https://www.erlang.org/doc/design_principles/gen_server_concepts), - Gleam's [actors](https://hexdocs.pm/gleam_otp/gleam/otp/actor.html), and the - [Elm Architecture](https://guide.elm-lang.org/architecture/). - -- **Managed side effects**: Managing asynchronous operations like HTTP requests - and timers can be tricky when JavaScript is single-threaded. Lustre provides a - runtime to manage these side effects and let them communicate with your application - using the same messages as your update loop. - -## Your first Lustre program - -To get started, let's create a new Gleam application and add Lustre as a dependency. - -```sh -gleam new app && cd app && gleam add lustre -``` - -By default, Gleam builds projects for the Erlang target unless told otherwise. We -can change this by adding a `target` field to the `gleam.toml` file generated in -the root of the project. - -```diff - name = "app" -+ target = "javascript" - version = "1.0.0" - - ... -``` - -The simplest type of Lustre application is constructed with the `element` function. -This produces an application that renders a static piece of content without the -typical update loop. - -We can start by importing `lustre` and `lustre/element` and just rendering some -text: - -```gleam -import lustre -import lustre/element - -pub fn main() { - let app = lustre.element(element.text("Hello, world!")) - let assert Ok(_) = lustre.start(app, "#app", Nil) - - Nil -} -``` - -Lustre has some official development tooling published in the -[`lustre_dev_tools`](https://hexdocs.pm/lustre_dev_tools/) package. Most projects -will probably want to add those too! - -> **Note**: the lustre_dev_tools development server watches your filesystem for -> changes to your gleam code and can automatically reload the browser. For linux -> users this requires [inotify-tools](https://github.com/inotify-tools/inotify-tools) -> be installed. If you do not or cannot install this, the development server will -> still run but it will not watch your files for changes. - -> **Note**: currently one of lustre_dev_tools' dependencies is not compatible with -> the most recent version of `gleam_json`, making it impossible to install. To fix -> this, add `gleam_json = "1.0.1"` as a dependency in your `gleam.toml` file. - -```sh -gleam add --dev lustre_dev_tools -``` - -It's important to make sure the development tooling is added as a `--dev` -dependency. This ensures they're never included in production builds of your app. - -To start a development server, we can run: - -```sh -gleam run -m lustre/dev start -``` - -The first time you run this command might take a little while, but subsequent runs -should be much faster! - -> **Note**: Lustre uses esbuild under the hood, and attempts to download the [right -> binary for your platform](https://esbuild.github.io/getting-started/#download-a-build). -> If you're not connected to the internet, on an unsupported platform, or don't -> want Lustre to download the binary you can grab or build it yourself and place it -> in `build/.lustre/bin/esbuild`. - -Once the server is up and running you should be able to visit http://localhost:1234 -and be greeted with your "Hello, world!" message. - -We mentioned Lustre has a declarative API for constructing HTML. Let's see what -that looks like by building something slightly more complex. - -```gleam -import lustre -import lustre/attribute -import lustre/element -import lustre/element/html - -pub fn main() { - let app = - lustre.element( - html.div([], [ - html.h1([], [element.text("Hello, world!")]), - html.figure([], [ - html.img([attribute.src("https://cataas.com/cat")]), - html.figcaption([], [element.text("A cat!")]) - ]) - ]) - ) - let assert Ok(_) = lustre.start(app, "#app", Nil) - - Nil -} -``` - -Here we _describe_ the structure of the HTML we want to render, and leave the -busywork to Lustre's runtime: that's what makes it declarative! - -"**Where are the templates?**" we hear you cry. Lustre doesn't have a separate -templating syntax like JSX or HEEx for a few reasons (lack of metaprogramming -built into Gleam, for one). Some folks might find this a bit odd at first, but -we encourage you to give it a try. Realising that your UI is _just functions_ -can be a bit of a lightbulb moment as you build more complex applications. - -## Adding interactivity - -Rendering static HTML is great, but we said at the beginning Lustre was designed -primarily for building SPAs – and SPAs are interactive! To do that we'll need -to move on from `lustre.element` to the first of Lustre's application constructors -that includes an update loop: `lustre.simple`. - -```gleam -import gleam/int -import lustre -import lustre/element -import lustre/element/html -import lustre/event - -pub fn main() { - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) - - Nil -} -``` - -There are three main building blocks to every interactive Lustre application: - -- A `Model` that represents your application's state and an `init` function - to create it. - -- A `Msg` type that represents all the different ways the outside world can - communicate with your application and an `update` function that modifies - your model in response to those messages. - -- A `view` function that renders your model to HTML. - -We'll build a simple counter application to demonstrate these concepts. Our -model can be an `Int` and our `init` function will initialise it to `0`: - -```gleam -pub type Model = Int - -fn init(_flags) -> Model { - 0 -} -``` - -> **Note**: The `init` function always takes a single argument! These are the "flags" -> or start arguments you can pass in when your application is started with -> `lustre.start`. For the time being, we can ignore them, but they're useful for -> passing in configuration or other data when your application starts. - -The main update loop in a Lustre application revolves around messages passed in -from the outside world. For our counter application, we'll have two messages to -increment and decrement the counter: - -```gleam -pub type Msg { - Increment - Decrement -} - -pub fn update(model: Model, msg: Msg) -> Model { - case msg { - Increment -> model + 1 - Decrement -> model - 1 - } -} -``` - -Each time a message is produced from an event listener, Lustre will call your -`update` function with the current model and the incoming message. The result -will be the new application state that is then passed to the `view` function: - -```gleam -pub fn view(model: Model) -> element.Element(Msg) { - let count = int.to_string(model) - - html.div([], [ - html.button([event.on_click(Increment)], [ - element.text("+") - ]), - element.text(count), - html.button([event.on_click(Decrement)], [ - element.text("-") - ]) - ]) -} -``` - -The above snippet attaches two click event listeners that produce an `Increment` -or `Decrement` message when clicked. The Lustre runtime is responsible for -attaching these event listeners and calling your `update` function with the -resulting message. - -> **Note**: notice that the return type of `view` is `element.Element(Msg)`. The -> type parameter `Msg` tells us the kinds of messages this element might produce -> from events: type safety to the rescue! - -This forms the core of every Lustre application: - -- A model produces some view. -- The view can produce messages in response to user interaction. -- Those messages are passed to the update function to produce a new model. -- ... and the cycle continues. - -## Talking to the outside world - -This "closed loop" of messages and updates works well if all we need is an -interactive document, but many applications will also need to talk to the outside -world – whether that's fetching data from an API, setting up a WebSocket connection, -or even just setting a timer. - -Lustre manages these side effects through an abstraction called an `Effect`. In -essence, effects are any functions that talk with the outside world and might -want to send messages back to your application. Lustre lets you write your own -effects, but for now we'll use a community package called -[`lustre_http`](https://hexdocs.pm/lustre_http/index.html) to fetch a new cat image -every time the counter is incremented. - -Because this is a separate package, make sure to add it to your project first. -While we're here, we'll also add `gleam_json` so we can decode the response from -the cat API: - -```sh -$ gleam add lustre_http -``` - -Now we are introducing side effects, we need to graduate from `lustre.simple` to -the more powerful `lustre.application` constructor. - -```gleam -import gleam/dynamic -import gleam/int -import gleam/list -import lustre -import lustre/attribute -import lustre/effect -import lustre/element -import lustre/element/html -import lustre/event -import lustre_http - -pub fn main() { - let app = lustre.application(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) - - Nil -} -``` - -If you edited your previous counter app, you'll notice the program no longer -compiles. Specifically, the type of our `init` and `update` functions are wrong -for the new `lustre.application` constructor! - -In order to tell Lustre about what effects it should perform, these functions now -need to return a _tuple_ of the new model and any effects. We can amend our `init` -function like so: - -```gleam -pub type Model { - Model(count: Int, cats: List(String)) -} - -fn init(_flags) -> #(Model, effect.Effect(Msg)) { - #(Model(0, []), effect.none()) -} -``` - -The `effect.none` function is a way of saying "no effects" – we don't need to do -anything when the application starts. We've also changed our `Model` type from a -simple type alias to a Gleam [record](https://tour.gleam.run/data-types/records/) -that holds both the current count and a list of cat image URLs. - -In our `update` function, we want to fetch a new cat image every time the counter -is incremented. To do this we need two things: - -- An `Effect` to describe the request the runtime should perform. -- A variant of our `Msg` to handle the response. - -The `lustre_http` package has the effect side of things handled, so we just need -to modify our `Msg` type to include a new variant for the response: - -```gleam -pub type Msg { - UserIncrementedCount - UserDecrementedCount - ApiReturnedCat(Result(String, lustre_http.HttpError)) -} -``` - -> **Note**: Concerned your message type is too verbose? Read our thoughts on why -> this is a good thing in our [state management guide](./02-state-management.html). - -Finally, we can modify our `update` function to also fetch a cat image when the -counter is incremented and handle the response: - -```gleam -pub fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { - case msg { - UserIncrementedCount -> #(Model(..model, count: model.count + 1), get_cat()) - UserDecrementedCount -> #(Model(..model, count: model.count - 1), effect.none()) - ApiReturnedCat(Ok(cat)) -> #(Model(..model, cats: [cat, ..model.cats]), effect.none()) - ApiReturnedCat(Error(_)) -> #(model, effect.none()) - } -} - -fn get_cat() -> effect.Effect(Msg) { - let decoder = dynamic.field("_id", dynamic.string) - let expect = lustre_http.expect_json(decoder, ApiReturnedCat) - - lustre_http.get("https://cataas.com/cat?json=true", expect) -} -``` - -> **Note**: The `get_cat` function returns an `Effect` that tells the runtime how -> to fetch a cat image. It's important to know that the `get_cat` function doesn't -> perform the request directly! This is why we need to add the `ApiReturnedCat` message -> variant: the runtime needs to know what to do with the response when it arrives. - -This model of managed effects can feel cumbersome at first, but it comes with some -benefits. Forcing side effects to produce a message means our message type naturally -describes all the ways the world can communicate with our application; as an app -grows being able to get this kind of overview is invaluable! It also means we can -test our update loop in isolation from the runtime and side effects: we could write -tests that verify a particular sequence of messages produces an expected model -without needing to mock out HTTP requests or timers. - -Before we forget, let's also update our `view` function to actually display the -cat images we're fetching: - -```gleam -pub fn view(model: Model) -> element.Element(Msg) { - let count = int.to_string(model.count) - - html.div([], [ - html.button([event.on_click(UserIncrementedCount)], [ - element.text("+") - ]), - element.text(count), - html.button([event.on_click(UserDecrementedCount)], [ - element.text("-") - ]), - html.div( - [], - list.map(model.cats, fn(cat) { - html.img([attribute.src("https://cataas.com/cat/" <> cat)]) - }), - ), - ]) -} -``` - -## Where to go from here - -Believe it or not, you've already seen about 80% of what Lustre has to offer! From -these core concepts, you can build rich interactive applications that are predictable -and maintainable. Where to go from here depends on what you want to build, and -how you like to learn: - -- There are a number of [examples](https://github.com/lustre-labs/lustre/tree/main/examples) - if the Lustre repository that gradually introduce more complex applications - and ideas. - -- The [rest of this guide](./02-state-management.html) also continues to teach - Lustre's high-level concepts and best-practices. - -- Of course, if you want to dive in and start making things straight away, the - [API documentation](https://hexdocs.pm/lustre/lustre.html) is always handy to keep open. - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). - -While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org) -is also a great resource for learning about the Model-View-Update architecture -and the kinds of patterns that Lustre is built around. diff --git a/packages/lustre/pages/guide/02-state-management.md b/packages/lustre/pages/guide/02-state-management.md deleted file mode 100644 index b29ac5c..0000000 --- a/packages/lustre/pages/guide/02-state-management.md +++ /dev/null @@ -1,244 +0,0 @@ -# 02 State management - -We saw in the quickstart guide that all Lustre applications are built around the -Model-View-Update (MVU) architecture. This means that the state of the application -is stored in a single, immutable data structure called the model, and updated as -messages are dispatched to the runtime. - -The MVU architecture is an example of _unidirectional data flow_: - -- Your model describes the entire state of your application at a given point in - time. - -- The UI is a [pure](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md) - function of that model: if the model doesn't change, the UI doesn't change. - -- Events from the outside world – user interaction, HTTP responses, ... – send - messages to an update function that constructs a new model. - -- The UI re-renders based on the new state. - -```text - +--------+ - | | - | update | - | | - +--------+ - ^ | - | | - Msg | | Model - | | - | v -+------+ +------------------------+ -| | Model | | -| init |------------------------>| Lustre Runtime | -| | | | -+------+ +------------------------+ - ^ | - | | - Msg | | Model - | | - | v - +--------+ - | | - | view | - | | - +--------+ -``` - -This is in contrast to _bidirectional_ approaches to state management, where the -UI can modify state directly. For some developers this can be a difficult idea -to get used to, but it brings a number of benefits: - -- A **single source of truth** makes it easier to reason about the state of your - application. State management is lifted _out_ of UI code, letting it focus just - on presentation and making it easier to test and refactor. - -- Message-driven **declarative state updates** give you a holistic view of how - your application can change over time. Tracking incoming messages gives you a - history of state updates and can be serialised and logged for debugging or - testing purposes. - -- State updates are **pure**. We will learn more about this in the [next guide](./03-side-effects.html), - but for now it is enough to know that this means testing your state changes is - much easier because mocking messages is simpler than mocking side effects! - -The rest of this guide contains some learned-wisdom and best practices for managing -state in Lustre applications. - -## The best model is not always a record - -It is overwhelmingly common to see the model of a Lustre application as a single -record. This is a sensible place to start, but there are other options! Gleam's -custom types allow us to model our data as disjoint variants. Using these as your -application's model can be particularly useful when you have different states that -do not need to persist across navigations: - -```gleam -type Model { - LoggedIn(LoggedInModel) - Public(PublicModel) -} - -type LoggedInModel { - ... -} - -type PublicModel { - ... -} -``` - -Here, we have a model that represents our application as either having a logged in -user or just one of the public routes. This pushes us towards the great practice of -[making impossible states impossible](https://github.com/stereobooster/pragmatic-types/blob/master/posts/making-impossible-states-impossible.md). -Now, we can write separate update and view functions that only handle the states -they care about. - -Another option is to use a _type alias_ to represent some state using existing -Gleam types. It's important to remember that your model represents _application_ -state and not necessarily _page_ state. This can manifest as simple as aliasing -Gleam's `Result` type or maybe a `Dict` representing loaded posts. - -## Messages not actions - -Lustre is not the first frontend framework to use the MVU architecture or to -focus on dispatching messages to update state. State management libraries like -Redux and Zustand follow a very similar pattern. The devil is in the details -though, and these libraries often talk in terms of _actions_ but you'll see -Elm and Lustre prefer the term _message_. - -Actions frame incoming events as _things to do_: "add a new todo", "make an HTTP -request", etc. This can work well in the beginning, but as your application grows -and the number of things you can do grows, naming messages as actions can become -problematic. - -In particular, it encourages you to recursively call your `update` function with -different messages when you want to compose behaviour. Gleam is a functional -programming language: we should use functions to update our state, not message -dispatching! Communicating through messages is a way for the _outside world_ to -talk to our application, not for our applications to talk to themselves. - -A recursive update function makes it difficult to see the consequences of any one -message as you need to trace through the recursive calls in your head to understand -which messages are being dispatched and in what order. - -Instead, we recommend you name your messages according to a **Subject Verb Object** -pattern. This frames messages based on who (or what) sent them, what state or -"thing" they're working on, and what they did or want to do. Imagine a password -reset form, the user can type in a new password and submit it and our app waits -for a response. As a first-pass we might end up with something like this: - -```gleam -type Msg { - SetPassword(String) - ResetPassword - PasswordReset(Result(Nil, String)) -} -``` - -This is quite muddled, and is compounded as we add more messages to our app -(especially if they also relate to the password!). It's hard to tell from looking -at our messages what our app might _really_ be doing: we'd have to dig into our -`update` function and possibly our `view` to work out what our intent was. One -super power of the MVU pattern is that we can look at our messages to get a -holistic view of what our app can handle. Things become much clearer if we refactor -this example to the Subject Verb Object naming pattern: - -```gleam -type Msg { - UserUpdatedPassword(String) - UserRequestedPasswordReset - BackendResetPassword(Result(Nil, String)) -} -``` - -It's now immediately obvious at a glance: - -1. Where these messages are coming from (user interaction, the network, ...) -2. What sort of event or intention they represent - -As our apps grow in size, we'll be thankful for this clarity! - -## View functions not components - -Although Lustre does have a way to create encapsulated stateful components (something -we sorely missed in Elm) it shouldn't be the default. The word "component" is a bit -overloaded in the frontend world, so for clarity Lustre considers _components_ -as stateful nested Model-View-Update applications and calls stateless functions -that return `Element`s _view functions_. - -The best Lustre code bases take the lessons learned from similar languages like -Elm, Erlang, and Elixir and keep the number of components low and the number of -simple view functions much higher. If you're coming from a typical frontend -framework the idea of eschewing stateful components might seem quite strange, but -there are some tangible benefits to this approach: - -- **Favouring view functions forces us to be intentional with state.** - - Frameworks often make it easy to add state to components, which in turn makes - it easy to add state without really thinking about whether we need it or whether - we're taking the best approach. - - View functions on the other hand _only_ have arguments, and adding a new argument - is a much more deliberate act. This gives us a chance to consider whether we're - modelling things the right way or whether we're trying to do too much. - -- **Components are bad for code organisation.** - - It can be tempting to use components as a way to organise code. You might see - this commonly in React and Vue codebases: you have a folder for components, a - folder for hooks, and so on. Using components as a means of organisation often - leads to us drawing weird boundaries around our code and spreading out things - that should be together. - - By sticking to view functions we're much more likely to keep code grouped based - on _what it does_ rather than what it _is_ and this approach is much more idiomatic - to Gleam on the whole, and also an approach favoured by Elm and Elixir alike. - -- **Avoiding components makes your code easier to test.** - - When we reach for components too soon or too frequently, we often end up needing - to pull in a complete E2E testing framework to make sure our code is behaving - correctly, or we might end up exposing our components' internals for testing: - defeating the purpose of encapsulation in the first place! - - By sticking to plain view functions and functions to transform data before - rendering, we end up with a codebase that is much easier to test with Gleam's - available testing tools. - -- **Overusing components makes refactoring more challenging.** - - Imagine you have a table component with tabs to switch between different views. - If some time in the future you decide to pull the tabs out so they can be - rendered elsewhere on the page you'll discover that the tabs' state was tightly - coupled to the table. Now we are forced to refactor the table component so the - tab state can be passed in as an attribute. We'll also need to refactor the - _parent_ to contain the state of the tabs so it can be passed down to both - components. - - By avoiding components this sort of refactoring becomes simpler: we were already - managing the state further up the component tree so moving things around is - much less painful. - -- **Creating components is more boilerplate.** - - Components share the same shape as any other Lustre application. That means for - any component you want to create, you also need to define an `init`, `update`, - and `view` function, a `Model` type, and a `Msg` type. If you find yourself - thinking "wow, this is a lot of boilerplate just to do X" then listen to your - gut! - -## Related examples - -If you'd like to see some of the ideas in action, we have a number of examples -that demonstrate how to use Lustre in practice: - -- [`02-interactivity`](https://github.com/lustre-labs/lustre/tree/main/examples/02-interactivity) -- [`03-controlled-inputs`](https://github.com/lustre-labs/lustre/tree/main/examples/03-controlled-inputs) - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). diff --git a/packages/lustre/pages/guide/03-side-effects.md b/packages/lustre/pages/guide/03-side-effects.md deleted file mode 100644 index f873724..0000000 --- a/packages/lustre/pages/guide/03-side-effects.md +++ /dev/null @@ -1,268 +0,0 @@ -# 03 Side effects - -Lustre's implementation of the Model-View-Update architecture includes one -additional piece of the puzzle: managed side effects. If we take the MVU diagram -from the previous guide and upgrade it to include managed effects, it looks like -this: - -```text - +--------+ - | | - | update | - | | - +--------+ - ^ | - | | - Msg | | #(Model, Effect(msg)) - | | - | v -+------+ +------------------------+ -| | #(Model, Effect(msg)) | | -| init |------------------------>| Lustre Runtime | -| | | | -+------+ +------------------------+ - ^ | - | | - Msg | | Model - | | - | v - +--------+ - | | - | view | - | | - +--------+ -``` - -Well what does managed effects mean, exactly? In Lustre, we expect your `init`, -`update`, and `view` functions to be [_pure_](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md). -That means they shouldn't perform side effects like making an HTTP request or writing -to local storage: we should be able to run your functions 100 times with the same -input and get the same output every time! - -Of course, in real applications performing HTTP requests and writing to local -storage turn out to be quite useful things to do. If we shouldn't perform side -effects in our code how do we do them then? Lustre has an [`Effect`](https://hexdocs.pm/lustre/lustre/effect.html) -type that _tells the runtime what side effects to perform_. So we say "Hey, I -want to make an HTTP request to this URL and when you get the response, dispatch -this message to me". The runtime takes care of performing the side effect and -turning the result into something our `update` function understands. - -## Why managed effects? - -This can feel like a lot of ceremony to go through just to make an HTTP request. -The natural question is: why not just let us make these requests ourselves? - -Managed effects have a number of benefits that come from _separating our programs -from the outside world_: - -1. **Predictability**: by keeping side effects out of our `update` function, we - can be confident that our application's state is only ever changed in one - place. This makes it easier to reason about our code and track down bugs. - -2. **Testability**: because our application code is pure, we can test it without - needing to mock out HTTP services or browser APIs. We can test our `update` - function, for example, by passing in a sequence of messages: no network mocks - required! - -3. **Reusability**: Lustre applications can run in a variety of environments and - contexts. The more we push platform-specific code into managed effects, the - easier time we'll have running our application as a [server component](https://hexdocs.pm/lustre/lustre/server_component.html) - or as a static site. - -## Packages for common effects - -The community has started to build packages that cover common side effects. For -many applications it's enough to drop these packages in and start using them -without needing to write any custom effects. - -> **Note**: _all_ of these packages are community maintained and unrelated to the -> core Lustre organisation. If you run into issues please open an issue on the -> package's repository! - -- [`lustre_http`](https://hexdocs.pm/lustre_http/) lets you make HTTP requests - and describe what responses to expect from them. - -- [`lustre_websocket`](https://hexdocs.pm/lustre_websocket/) handles WebSocket - connections and messages. - -- [`modem`](https://hexdocs.pm/modem/) and [`lustre_routed`](https://hexdocs.pm/lustre_routed/) - are two packages that help you manage navigation and routing. - -- [`lustre_animation`](https://hexdocs.pm/lustre_animation/) is a simple package - for interpolating between values over time. - -## Running effects - -We know that effects need to be performed by the runtime, but how does the runtime -know when we want it to run an effect? If you have been using the `lustre.simple` -application constructor until now, it is time to upgrade to -[`lustre.application`](https://hexdocs.pm/lustre/lustre.html#application)! - -Full Lustre applications differ from simple applications in one important way by -returning a tuple of `#(Model, Effect(Msg))` from your `init` and `update` -functions: - -```gleam -pub fn simple( - init: fn(flags) -> model, - update: fn(model, msg) -> model, - view: fn(model) -> Element(msg), -) -> App(flags, model, msg) - -pub fn application( - init: fn(flags) -> #(model, Effect(msg)), - update: fn(model, msg) -> #(model, Effect(msg)), - view: fn(model) -> Element(msg), -) -> App(flags, model, msg) -``` - -We can, for example, launch an HTTP request on application start by using `lustre_http.get` -in our `init` function: - -```gleam -fn init(_flags) { - let model = Model(...) - let get_ip = lustre_http.get( - "https://api.ipify.org", - ApiReturnedIpAddress - ) - - #(model, get_ip) -} -``` - -> **Note**: to tell the runtime we _don't_ want to perform any side effects this -> time, we can use [`effect.none()`](https://hexdocs.pm/lustre/lustre/effect.html#none). - -## Writing your own effects - -When you need to do something one of the existing packages doesn't cover, you need -to write your own effect. You can do that by passing a callback to -[`effect.from`](https://hexdocs.pm/lustre/lustre/effect.html#from). Custom effects -are called with an argument – commonly called `dispatch` – that you can use to -send messages back to your application's `update` function. - -Below is an example of a custom effect that reads a value from local storage: - -```js -// ffi.mjs -import { Ok, Error } from "./gleam.mjs"; - -export function read(key) { - const value = window.localStorage.getItem(key); - return value ? new Ok(value) : new Error(undefined); -} -``` - -```gleam -fn read(key: String, to_msg: fn(Result(String, Nil)) -> msg) -> Effect(msg) { - effect.from(fn(dispatch) { - do_read(key) - |> to_msg - |> dispatch - }) -} - -@external(javascript, "ffi.mjs", "read") -fn do_read(key: String) -> Result(String, Nil) { - Error(Nil) -} -``` - -> **Note**: we provide a default implementation of the `do_read` function that -> always fails. Where possible it's good to provide an implementation for all of -> Gleam's targets. This makes it much easier to run your code as a -> [server component](https://hexdocs.pm/lustre/lustre/server_component.html) in -> the future. - -### Effects that touch the DOM - -Lustre runs all your side effects after your `update` function returns but _before_ -your `view` function is called. A common bug folks run into is trying to interact -with a particular element in the DOM before it's had a chance to render. As a -rule of thumb, you should _always_ wrap custom effects that interact with the DOM -in a [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) -call to ensure the DOM has had a chance to update first. - -### Effects without dispatch - -So far, we have seen side effects that are expected to _return something_ to our -program. If we fire an HTTP request, it wouldn't be much use if we couldn't get -the response back! Sometimes folks wrongly assume effects _must_ use the `dispatch` -function they're given, but this isn't true! - -It's also totally valid to write effects that don't dispatch any messages. Earlier -we saw an example of how to read from local storage, we might also want an effect -to _write_ to local storage and there's not much to dispatch in that case! - -```js -// ffi.mjs -export function write(key, value) { - window.localStorage.setItem(key, value); -} -``` - -```gleam -// app.gleam -fn write(key: String, value: String) -> Effect(msg) { - effect.from(fn(_) { - do_write(key, value) - }) -} - -@external(javascript, "ffi.mjs", "write") -fn do_write(key: String, value: String) -> Nil { - Nil -} -``` - -### Effects with multiple dispatch - -Similar to effects that don't dispatch any messages, some folks skip over the fact -effects can dispatch _multiple_ messages. Packages like [`lustre_websocket`](https://hexdocs.pm/lustre_websocket/) -and [`modem`](https://hexdocs.pm/modem/) set up effects that will dispatch many -messages over the lifetime of your program. - -Once you have a reference to that `dispatch` function, you're free to call it as -many times as you want! - -```js -// ffi.mjs -export function every(interval, cb) { - window.setInterval(cb, interval); -} -``` - -```gleam -// app.gleam -fn every(interval: Int, tick: msg) -> Effect(msg) { - effect.from(fn(dispatch) { - do_every(interval, fn() { - dispatch(tick) - }) - }) -} - -@external(javascript, "ffi.mjs", "every") -fn do_every(interval: Int, cb: fn() -> Nil) -> Nil { - Nil -} -``` - -Here we set up an effect that will continuously dispatch a `tick` message at a -fixed interval. - -## Related examples - -If you'd like to see some of the ideas in action, we have a number of examples -that demonstrate how Lustre's effects system works in practice: - -- [`05-http-requests`](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests) -- [`06-custom-effects`](https://github.com/lustre-labs/lustre/tree/main/examples/06-custom-effects) -- [`07-routing`](https://github.com/lustre-labs/lustre/tree/main/examples/07-routing) - -## Getting help - -If you're having trouble with Lustre or not sure what the right way to do -something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). -You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). diff --git a/packages/lustre/pages/guide/04-server-side-rendering.md b/packages/lustre/pages/guide/04-server-side-rendering.md deleted file mode 100644 index 27a2d89..0000000 --- a/packages/lustre/pages/guide/04-server-side-rendering.md +++ /dev/null @@ -1,216 +0,0 @@ -# 04 Server-side rendering - -Up until now, we have focused on Lustre's ability as a framework for building -Single Page Applications (SPAs). While Lustre's development and feature set is -primarily focused on SPA development, that doesn't mean it can't be used on the -backend as well! In this guide we'll set up a small [mist](https://hexdocs.pm/mist/) -server that renders some static HTML using Lustre. - -## Setting up the project - -We'll start by adding the dependencies we need and scaffolding the HTTP server. -Besides Lustre and Mist, we also need `gleam_erlang` (to keep our application -alive) and `gleam_http` (for types and functions to work with HTTP requests and -responses): - -```sh -gleam new app && cd app && gleam add gleam_erlang gleam_http lustre mist -``` - -Besides imports for `mist` and `gleam_http` modules, we also need to import some -modules to render HTML with Lustre. Importantly, we _don't_ need anything from the -main `lustre` module: we're not building an application with a runtime! - -```gleam -import gleam/bytes_builder -import gleam/erlang/process -import gleam/http/request.{type Request} -import gleam/http/response.{type Response} -import lustre/element -import lustre/element/html.{html} -import mist.{type Connection, type ResponseData} -``` - -We'll modify Mist's example and write a simple request handler that responds to -requests to `/greet/:name` with a greeting message: - -```gleam -pub fn main() { - let empty_body = mist.Bytes(bytes_builder.new()) - let not_found = response.set_body(response.new(404), empty_body) - - let assert Ok(_) = - fn(req: Request(Connection)) -> Response(ResponseData) { - case request.path_segments(req) { - ["greet", name] -> greet(name) - _ -> not_found - } - } - |> mist.new - |> mist.port(3000) - |> mist.start_http - - process.sleep_forever() -} -``` - -Let's take a peek inside that `greet` function: - -```gleam -fn greet(name: String) -> Response(ResponseData) { - let res = response.new(200) - let html = - html([], [ - html.head([], [html.title([], "Greetings!")]), - html.body([], [ - html.h1([], [html.text("Hey there, " <> name <> "!")]) - ]) - ]) - - response.set_body(res, - html - |> element.to_document_string - |> bytes_builder.from_string - |> mist.Bytes - ) -} -``` - -The `lustre/element` module has functions for rendering Lustre elements to a -string (or string builder); the `to_document_string` function helpfully prepends -the `` declaration to the output. - -It's important to realise that `element.to_string` and `element.to_document_string` -can render _any_ Lustre element! This means you could take the `view` function -from your client-side SPA and render it server-side, too. - -## Hydration - -If we know we can render our apps server-side, the next logical question is how -do we handle _hydration_? Hydration is the process of taking the static HTML -generated by the server and turning it into a fully interactive client application, -ideally doing as little work as possible. - -Most frameworks today support hydration or some equivalent, for example by -serialising the state of each component into the HTML and then picking up where -the server left off. Lustre doesn't have a built-in hydration mechanism, but -because of the way it works, it's easy to implement one yourself! - -We've said many times now that in Lustre, your `view` is just a -[pure function](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md) -of your model. We should produce the same HTML every time we call `view` with the -same model, no matter how many times we call it. - -Let's use that to our advantage! We know our app's `init` function is responsible -for producing the initial model, so all we need is a way to make sure the initial -model on the client is the same as what the server used to render the page. - -```gleam -pub fn view(model: Int) -> Element(Msg) { - let count = int.to_string(model) - - html.div([], [ - html.button([event.on_click(Decr)], [html.text("-")]), - html.button([event.on_click(Incr)], [html.text("+")]), - html.p([], [html.text("Count: " <> count)]) - ]) -} -``` - -We've seen the counter example a thousand times over now, but it's a good example -to show off how simple hydration can be. The `view` function produces some HTML -with events attached, but we already know Lustre can render _any_ element to a -string so that shouldn't be a problem. - -Let's imagine our HTTP server responds with the following HTML: - -```gleam -import app/counter -import gleam/bytes_builder -import gleam/http/response.{type Response} -import gleam/json -import lustre/attribute -import lustre/element.{type Element} -import lustre/element/html.{html} -import mist.{type ResponseData} - -fn app() -> Response(ResponseData) { - let res = response.new(200) - - let model = 5 - let html = - html([], [ - html.head([], [ - html.script([attribute.type_("module"), attribute.src("...")], ""), - html.script([attribute.type_("application/json"), attribute.id("model")], - json.int(model) - |> json.to_string - ) - ]), - html.body([], [ - html.div([attribute.id("app")], [ - counter.view(model) - ]) - ]) - ]) - - response.set_body(res, - html - |> element.to_document_string - |> bytes_builder.from_string - |> mist.Bytes - ) -} -``` - -We've rendered the shell of our application, as well as the counter using `5` as -the initial model. Importantly, we've included a ` - - - -
- - diff --git a/packages/lustre/test-apps/options-list/manifest.toml b/packages/lustre/test-apps/options-list/manifest.toml deleted file mode 100644 index 676354e..0000000 --- a/packages/lustre/test-apps/options-list/manifest.toml +++ /dev/null @@ -1,46 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" }, - { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "filespy", version = "0.4.0", build_tools = ["gleam"], requirements = ["fs", "gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "filespy", source = "hex", outer_checksum = "950469A2FA50265EB84530637D3E9597C2CA676A2EEABC98C69A83C77316709C" }, - { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, - { name = "glearray", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "908154F695D330E06A37FAB2C04119E8F315D643206F8F32B6A6C14A8709FFF4" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "BB0F14643CC51C069A5DC6E9082EAFCD9967AFD1C9CC408803D1A40A3FD43B54" }, - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" }, - { name = "lustre", version = "4.1.7", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], source = "local", path = "../.." }, - { name = "lustre_dev_tools", version = "1.2.1", build_tools = ["gleam"], requirements = ["argv", "filepath", "filespy", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "930BBE8C4E92A16857C31B7B12616651433E1643304696FB93B69D659CE3ADC2" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" }, - { name = "mist", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7765E53DCC9ACCACF217B8E0CA3DE7E848C783BFAE5118B75011E81C2C80385C" }, - { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, - { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, -] - -[requirements] -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { path = "../../" } -lustre_dev_tools = { version = "~> 1.0" } -lustre_ui = { version = "~> 0.4" } diff --git a/packages/lustre/test-apps/options-list/src/app.gleam b/packages/lustre/test-apps/options-list/src/app.gleam deleted file mode 100644 index 3328e7d..0000000 --- a/packages/lustre/test-apps/options-list/src/app.gleam +++ /dev/null @@ -1,49 +0,0 @@ -import lustre -import lustre/attribute -import lustre/element.{type Element} -import lustre/element/html -import lustre/event -import lustre/ui - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "#app", Nil) -} - -// MODEL ----------------------------------------------------------------------- - -type Model = - String - -fn init(_flags) -> Model { - "a" -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - Select(String) -} - -fn update(_model: Model, msg: Msg) -> Model { - case msg { - Select(tag) -> tag - } -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")] - - ui.centre( - [attribute.style(styles)], - html.select([event.on_input(Select)], [ - html.option([attribute.value("a"), attribute.selected("a" == model)], "a"), - html.option([attribute.value("b"), attribute.selected("b" == model)], "b"), - html.option([attribute.value("c"), attribute.selected("c" == model)], "c"), - ]), - ) -} diff --git a/packages/lustre/test-apps/server-component-change-children/README.md b/packages/lustre/test-apps/server-component-change-children/README.md deleted file mode 100644 index a6acb10..0000000 --- a/packages/lustre/test-apps/server-component-change-children/README.md +++ /dev/null @@ -1,5 +0,0 @@ -This example makes sure that patches that add or remove different children in a -server component are correctly applied on the client. At one point we realised -patches were being sent in reverse order and that meant the client ended up -incorrectly reusing newly-created children from the _current patch_ when diffing -new nodes. diff --git a/packages/lustre/test-apps/server-component-change-children/gleam.toml b/packages/lustre/test-apps/server-component-change-children/gleam.toml deleted file mode 100644 index eb98040..0000000 --- a/packages/lustre/test-apps/server-component-change-children/gleam.toml +++ /dev/null @@ -1,27 +0,0 @@ -name = "app" -version = "1.0.0" - -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -# description = "" -# licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] -# -# For a full reference of all the available options, you can have a look at -# https://gleam.run/writing-gleam/gleam-toml/. - -[dependencies] -gleam_stdlib = "~> 0.36" -lustre = { path = "../../" } -mist = "~> 0.17" -gleam_erlang = "~> 0.24" -gleam_otp = "~> 0.10" -gleam_http = "~> 3.6" -lustre_ui = "~> 0.4" -gleam_json = "~> 1.0" -simplifile = "~> 1.5" - -[dev-dependencies] -gleeunit = "~> 1.0" diff --git a/packages/lustre/test-apps/server-component-change-children/manifest.toml b/packages/lustre/test-apps/server-component-change-children/manifest.toml deleted file mode 100644 index 32f8931..0000000 --- a/packages/lustre/test-apps/server-component-change-children/manifest.toml +++ /dev/null @@ -1,31 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glisten", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "73BC09C8487C2FFC0963BFAB33ED2F0D636FDFA43B966E65C1251CBAB8458099" }, - { name = "lustre", version = "4.1.8", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], source = "local", path = "../.." }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "mist", version = "0.17.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten"], otp_app = "mist", source = "hex", outer_checksum = "DA8ACEE52C1E4892A75181B3166A4876D8CBC69D555E4770250BC84C80F75524" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "thoas", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1" }, -] - -[requirements] -gleam_erlang = { version = "~> 0.24" } -gleam_http = { version = "~> 3.6" } -gleam_json = { version = "~> 1.0" } -gleam_otp = { version = "~> 0.10" } -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { path = "../../" } -lustre_ui = { version = "~> 0.4" } -mist = { version = "~> 0.17" } -simplifile = { version = "~> 1.5" } diff --git a/packages/lustre/test-apps/server-component-change-children/src/app.gleam b/packages/lustre/test-apps/server-component-change-children/src/app.gleam deleted file mode 100644 index 82a1658..0000000 --- a/packages/lustre/test-apps/server-component-change-children/src/app.gleam +++ /dev/null @@ -1,167 +0,0 @@ -import counter -import gleam/bytes_builder -import gleam/erlang -import gleam/erlang/process.{type Selector, type Subject} -import gleam/http/request.{type Request} -import gleam/http/response.{type Response} -import gleam/json -import gleam/option.{type Option, None} -import gleam/otp/actor -import gleam/result -import lustre -import lustre/attribute -import lustre/element -import lustre/element/html.{html} -import lustre/server_component -import mist.{ - type Connection, type ResponseData, type WebsocketConnection, - type WebsocketMessage, -} - -pub fn main() { - let assert Ok(_) = - fn(req: Request(Connection)) -> Response(ResponseData) { - case request.path_segments(req) { - // Set up the websocket connection to the client. This is how we send - // DOM updates to the browser and receive events from the client. - ["counter"] -> - mist.websocket( - request: req, - on_init: socket_init, - on_close: socket_close, - handler: socket_update, - ) - - // We need to serve the server component runtime. There's also a minified - // version of this script for production. - ["lustre-server-component.mjs"] -> { - let assert Ok(priv) = erlang.priv_directory("lustre") - let path = priv <> "/static/lustre-server-component.mjs" - - mist.send_file(path, offset: 0, limit: None) - |> result.map(fn(script) { - response.new(200) - |> response.prepend_header("content-type", "application/javascript") - |> response.set_body(script) - }) - |> result.lazy_unwrap(fn() { - response.new(404) - |> response.set_body(mist.Bytes(bytes_builder.new())) - }) - } - - // For all other requests we'll just serve some HTML that renders the - // server component. - _ -> - response.new(200) - |> response.prepend_header("content-type", "text/html") - |> response.set_body( - html([], [ - html.head([], [ - html.link([ - attribute.rel("stylesheet"), - attribute.href( - "https://cdn.jsdelivr.net/gh/lustre-labs/ui/priv/styles.css", - ), - ]), - html.script( - [ - attribute.type_("module"), - attribute.src("/lustre-server-component.mjs"), - ], - "", - ), - ]), - html.body([], [ - server_component.component([server_component.route("/counter")]), - ]), - ]) - |> element.to_document_string_builder - |> bytes_builder.from_string_builder - |> mist.Bytes, - ) - } - } - |> mist.new - |> mist.port(3000) - |> mist.start_http - - process.sleep_forever() -} - -// - -type Counter = - Subject(lustre.Action(counter.Msg, lustre.ServerComponent)) - -fn socket_init( - conn: WebsocketConnection, -) -> #(Counter, Option(Selector(lustre.Patch(counter.Msg)))) { - let app = counter.app() - let assert Ok(counter) = lustre.start_actor(app, 0) - - process.send( - counter, - server_component.subscribe( - // server components can have many connected clients, so we need a way to - // identify this client. - "ws", - // this callback is called whenever the server component has a new patch - // to send to the client. here we json encode that patch and send it to - // via the websocket connection. - // - // a more involved version would have us sending the patch to this socket's - // subject, and then it could be handled (perhaps with some other work) in - // the `mist.Custom` branch of `socket_update` below. - fn(patch) { - let assert Ok(_) = - patch - |> server_component.encode_patch - |> json.to_string - |> mist.send_text_frame(conn, _) - - Nil - }, - ), - ) - - #( - // we store the server component's `Subject` as this socket's state so we - // can shut it down when the socket is closed. - counter, - // the `None` here means we aren't planning on receiving any messages from - // elsewhere and dont need a `Selector` to handle them. - None, - ) -} - -import gleam/io - -fn socket_update( - counter: Counter, - _conn: WebsocketConnection, - msg: WebsocketMessage(lustre.Patch(counter.Msg)), -) { - case msg { - mist.Text(json) -> { - // we attempt to decode the incoming text as an action to send to our - // server component runtime. - let action = json.decode(json, server_component.decode_action) - - case action { - Ok(action) -> process.send(counter, action) - Error(_) -> Nil - } - - actor.continue(counter) - } - - mist.Binary(_) -> actor.continue(counter) - mist.Custom(_) -> actor.continue(counter) - mist.Closed | mist.Shutdown -> actor.Stop(process.Normal) - } -} - -fn socket_close(counter: Counter) { - process.send(counter, lustre.shutdown()) -} diff --git a/packages/lustre/test-apps/server-component-change-children/src/counter.gleam b/packages/lustre/test-apps/server-component-change-children/src/counter.gleam deleted file mode 100644 index 06131fc..0000000 --- a/packages/lustre/test-apps/server-component-change-children/src/counter.gleam +++ /dev/null @@ -1,88 +0,0 @@ -import gleam/list -import lustre -import lustre/attribute -import lustre/element.{type Element} -import lustre/element/html -import lustre/event -import lustre/ui - -// MAIN ------------------------------------------------------------------------ - -pub fn app() { - lustre.simple(init, update, view) -} - -// MODEL ----------------------------------------------------------------------- - -pub type Model = - List(#(String, String, String, String)) - -fn init(_) -> Model { - [] -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - Incr - Decr -} - -fn update(_: Model, msg: Msg) -> Model { - case msg { - Incr -> [ - #("1", "1", "1", "1"), - #("2", "2", "2", "2"), - #("3", "3", "3", "3"), - #("4", "4", "4", "4"), - #("5", "5", "5", "5"), - ] - - Decr -> [ - #("3", "3", "3", "3"), - #("2", "2", "2", "2"), - #("1", "1", "1", "1"), - ] - } -} - -// VIEW ------------------------------------------------------------------------ - -fn view(model: Model) -> Element(Msg) { - let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")] - - ui.centre( - [attribute.style(styles)], - ui.stack([], [ - ui.button([event.on_click(Incr)], [element.text("ascending")]), - ui.button([event.on_click(Decr)], [element.text("descending")]), - html.div([], [ - ui.stack([], [ - html.table([], [ - html.thead([], [ - html.tr([attribute.style([])], [ - html.th([], [html.text("Part No")]), - html.th([], [html.text("Customer")]), - html.th([], [html.text("Job No")]), - html.th([], [html.text("Due Date")]), - ]), - ]), - { - // let rows = - html.tbody([], { - list.map(model, fn(tuple) { - html.tr([], [ - html.td([], [html.text(tuple.0)]), - html.td([], [html.text(tuple.1)]), - html.td([], [html.text(tuple.2)]), - html.td([], [html.text(tuple.3)]), - ]) - }) - }) - }, - ]), - ]), - ]), - ]), - ) -} diff --git a/packages/lustre/test-apps/svg/README.md b/packages/lustre/test-apps/svg/README.md deleted file mode 100644 index ebe238b..0000000 --- a/packages/lustre/test-apps/svg/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This example exists because setting some SVG attributes like `viewBox` was causing -a runtime error. These attributes were mirrored as DOM properties but they were -marked as **read-only**. diff --git a/packages/lustre/test-apps/svg/gleam.toml b/packages/lustre/test-apps/svg/gleam.toml deleted file mode 100644 index 4a824db..0000000 --- a/packages/lustre/test-apps/svg/gleam.toml +++ /dev/null @@ -1,12 +0,0 @@ -name = "app" -version = "1.0.0" -target = "javascript" - -[dependencies] -gleam_stdlib = "~> 0.36" -lustre = { path = "../../" } -lustre_ui = "~> 0.4" - -[dev-dependencies] -gleeunit = "~> 1.0" -lustre_dev_tools = "~> 1.0" diff --git a/packages/lustre/test-apps/svg/manifest.toml b/packages/lustre/test-apps/svg/manifest.toml deleted file mode 100644 index 74a1b75..0000000 --- a/packages/lustre/test-apps/svg/manifest.toml +++ /dev/null @@ -1,33 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, - { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, - { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, - { name = "glearray", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "908154F695D330E06A37FAB2C04119E8F315D643206F8F32B6A6C14A8709FFF4" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "glint", version = "0.18.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "BB0F14643CC51C069A5DC6E9082EAFCD9967AFD1C9CC408803D1A40A3FD43B54" }, - { name = "lustre", version = "4.1.5", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], source = "local", path = "../.." }, - { name = "lustre_dev_tools", version = "1.1.5", build_tools = ["gleam"], requirements = ["argv", "filepath", "gleam_community_ansi", "gleam_erlang", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "simplifile", "spinner", "tom"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "CA6C10177B66C4FBE8F56B37973C7BB312A8622248D5489957B364FF2C0700AE" }, - { name = "lustre_ui", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_ui", source = "hex", outer_checksum = "FA1F9E89D89CDD5DF376ED86ABA8A38441CB2E664CD4D402F22A49DA4D7BB56D" }, - { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, - { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, - { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, - { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, -] - -[requirements] -gleam_stdlib = { version = "~> 0.36" } -gleeunit = { version = "~> 1.0" } -lustre = { path = "../../" } -lustre_dev_tools = { version = "~> 1.0" } -lustre_ui = { version = "~> 0.4" } diff --git a/packages/lustre/test-apps/svg/src/app.gleam b/packages/lustre/test-apps/svg/src/app.gleam deleted file mode 100644 index 4b0b8b2..0000000 --- a/packages/lustre/test-apps/svg/src/app.gleam +++ /dev/null @@ -1,35 +0,0 @@ -import lustre -import lustre/attribute.{attribute} -import lustre/element/html -import lustre/element/svg -import lustre/ui -import lustre/ui/icon - -pub fn main() { - let styles = [#("width", "100vw"), #("height", "100vh"), #("padding", "1rem")] - - lustre.element(ui.centre( - [attribute.style(styles)], - html.svg( - [ - attribute("version", "1.1"), - attribute("viewBox", "0 0 300 200"), - attribute("width", "300"), - attribute("height", "200"), - ], - [ - svg.rect([ - attribute("width", "100%"), - attribute("height", "100%"), - attribute("fill", "red"), - ]), - svg.circle([ - attribute("cx", "150"), - attribute("cy", "100"), - attribute("r", "80"), - attribute("fill", "green"), - ]), - ], - ), - )) -} diff --git a/packages/lustre/test-apps/vdom-test-templates/gleam.toml b/packages/lustre/test-apps/vdom-test-templates/gleam.toml deleted file mode 100644 index 24c306c..0000000 --- a/packages/lustre/test-apps/vdom-test-templates/gleam.toml +++ /dev/null @@ -1,7 +0,0 @@ -name = "app" -version = "1.0.0" -target = "javascript" - -[dependencies] -gleam_stdlib = "~> 0.36" -lustre = { path = "../../" } diff --git a/packages/lustre/test-apps/vdom-test-templates/manifest.toml b/packages/lustre/test-apps/vdom-test-templates/manifest.toml deleted file mode 100644 index 342a523..0000000 --- a/packages/lustre/test-apps/vdom-test-templates/manifest.toml +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, - { name = "lustre", version = "4.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], source = "local", path = "../.." }, - { name = "thoas", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1" }, -] - -[requirements] -gleam_stdlib = { version = "~> 0.36" } -lustre = { path = "../../" } diff --git a/packages/lustre/test-apps/vdom-test-templates/src/client_test.gleam b/packages/lustre/test-apps/vdom-test-templates/src/client_test.gleam deleted file mode 100644 index ccbb65e..0000000 --- a/packages/lustre/test-apps/vdom-test-templates/src/client_test.gleam +++ /dev/null @@ -1,64 +0,0 @@ -import gleam/int -import gleam/list -import lustre/attribute -import lustre/element -import lustre/element/html - -// VIEW HELPERS ----------------------------------------------------------------- -// Functions to get view definitions for testing client ffi, further testing could reuse examples -pub fn smoke_test() { - html.div([], [html.p([], [element.text("smoke test")])]) -} - -pub fn dynamic_content_test(number: Int, some_string: String) { - html.div([], [ - html.h1([], [element.text(some_string)]), - html.p([], [element.text(int.to_string(number))]), - ]) -} - -const mock_people = [ - #("1dfg", "Person One", 18), - #("abc3", "Person Two", 24), - #("ga4d", "Person Three", 30), -] - -pub fn fragment_test() { - let person_els = - element.fragment( - list.map(mock_people, fn(person) { - let #(_, person_name, age) = person - let person_el = [ - html.td([], [element.text(person_name)]), - html.td([], [element.text(int.to_string(age))]), - ] - element.fragment([html.tr([], person_el)]) - }), - ) - - html.table([], [ - html.head([], [ - html.tr([], [ - html.th([], [element.text("Person Name")]), - html.th([], [element.text("Person Age")]), - ]), - ]), - html.body([], [person_els]), - ]) -} - -pub fn keyed_test() { - element.keyed(html.ul([], _), { - use #(id, person_name, age) <- list.map(mock_people) - let child = - html.tr([], [ - html.td([], [element.text(person_name)]), - html.td([], [element.text(int.to_string(age))]), - ]) - #(id, child) - }) -} - -pub fn disabled_attr_test(is_disabled: Bool) { - html.div([], [html.input([attribute.disabled(is_disabled)])]) -} diff --git a/packages/lustre/test/02-interactivity.test.js b/packages/lustre/test/02-interactivity.test.js deleted file mode 100644 index 3355e94..0000000 --- a/packages/lustre/test/02-interactivity.test.js +++ /dev/null @@ -1,88 +0,0 @@ -import { beforeEach, describe, expect, test } from "vitest"; -import { setupDOM } from "./utils.js"; -// built via npm script "build:test:02" -import { main } from "@root/examples/02-interactivity/build/dev/javascript/app/app.mjs"; - -let appEl; -beforeEach(() => { - setupDOM(); - appEl = document.getElementById("app"); -}); - -describe("counter example", () => { - test("should render initially", () => { - main(); - - expect(document.toString()).toMatchSnapshot(); - }); - - test("should increment on button press", () => { - main(); - - const buttons = document.querySelectorAll("button.lustre-ui-button"); - const incrementButton = buttons[0]; - const count = document.querySelector("p"); - - expect(incrementButton).toBeTruthy(); - - incrementButton.click(); - - expect(count.innerText).toBe("1"); - - incrementButton.click(); - expect(count.innerText).toBe("2"); - - incrementButton.click(); - expect(count.innerText).toBe("3"); - - }); - - test("should decrement on button press", () => { - main(); - - const buttons = document.querySelectorAll("button.lustre-ui-button"); - const decrementButton = buttons[1]; - const count = document.querySelector("p"); - - expect(decrementButton).toBeTruthy(); - - decrementButton.click(); - expect(count.innerText).toBe("-1"); - - decrementButton.click(); - expect(count.innerText).toBe("-2"); - - decrementButton.click(); - expect(count.innerText).toBe("-3"); - }); - - test("should increment and decrement on button press", () => { - main(); - - const buttons = document.querySelectorAll("button.lustre-ui-button"); - const incrementButton = buttons[0]; - const decrementButton = buttons[1]; - const count = document.querySelector("p"); - - incrementButton.click(); - - expect(count.innerText).toBe("1"); - - incrementButton.click(); - expect(count.innerText).toBe("2"); - - incrementButton.click(); - expect(count.innerText).toBe("3"); - - expect(decrementButton).toBeTruthy(); - - decrementButton.click(); - expect(count.innerText).toBe("2"); - - decrementButton.click(); - expect(count.innerText).toBe("1"); - - decrementButton.click(); - expect(count.innerText).toBe("0"); - }); -}); diff --git a/packages/lustre/test/README.md b/packages/lustre/test/README.md deleted file mode 100644 index 6577c5c..0000000 --- a/packages/lustre/test/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Client testing for Lustre runtime - -1. Build and test example projects -2. Build and test vdom - -Depends on: -- `linkedom` - headless DOM testing -- `npm-run-all` - run watch in parallel -- `vitest` - execute tests - - -### Commands - -Run from the `test` directory -Each command will run a `build` command to build project dependencies - -#### Benchmark - -- `npm run bench` - -#### Test - -- ##### Single - - - `npm run test` - -- ##### Watch - - - `npm run test:watch` diff --git a/packages/lustre/test/apps/counter.gleam b/packages/lustre/test/apps/counter.gleam deleted file mode 100644 index b58b2ee..0000000 --- a/packages/lustre/test/apps/counter.gleam +++ /dev/null @@ -1,41 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/int -import lustre/element.{text} -import lustre/element/html.{button, div, p} -import lustre/event - -// MODEL ----------------------------------------------------------------------- - -pub type Model = - Int - -pub fn init(count) { - count -} - -// UPDATE ---------------------------------------------------------------------- - -pub type Msg { - Increment - Decrement -} - -pub fn update(model, msg) { - case msg { - Increment -> model + 1 - Decrement -> model - 1 - } -} - -// VIEW ------------------------------------------------------------------------ - -pub fn view(model) { - let count = int.to_string(model) - - div([], [ - p([], [text(count)]), - button([event.on_click(Decrement)], [text("-")]), - button([event.on_click(Increment)], [text("+")]), - ]) -} diff --git a/packages/lustre/test/apps/fragment.gleam b/packages/lustre/test/apps/fragment.gleam deleted file mode 100644 index d20400e..0000000 --- a/packages/lustre/test/apps/fragment.gleam +++ /dev/null @@ -1,45 +0,0 @@ -// Similar to count app, with fragments and edge cases - -// IMPORTS --------------------------------------------------------------------- - -import gleam/int -import lustre/element.{text} -import lustre/element/html.{button, p} -import lustre/event - -// MODEL ----------------------------------------------------------------------- - -pub type Model = - Int - -pub fn init(count) { - count -} - -// UPDATE ---------------------------------------------------------------------- - -pub type Msg { - Increment - Decrement -} - -pub fn update(model, msg) { - case msg { - Increment -> model + 1 - Decrement -> model - 1 - } -} - -// VIEW ------------------------------------------------------------------------ - -pub fn view(model) { - let count = int.to_string(model) - element.fragment([ - element.fragment([p([], [element.text("start fragment")])]), - element.fragment([p([], [element.text("middle fragment")])]), - element.fragment([p([], [element.text(count)])]), - button([event.on_click(Decrement)], [text("-")]), - button([event.on_click(Increment)], [text("+")]), - p([], [element.text("order check, last element")]), - ]) -} diff --git a/packages/lustre/test/apps/static.gleam b/packages/lustre/test/apps/static.gleam deleted file mode 100644 index fcf52f3..0000000 --- a/packages/lustre/test/apps/static.gleam +++ /dev/null @@ -1,29 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import lustre/attribute.{attribute, class, disabled, src, style} -import lustre/element.{text} -import lustre/element/html.{body, div, h1, head, html, img, input, title} - -// VIEW ------------------------------------------------------------------------ - -pub fn view() { - html([], [ - head([], [title([], "Hello, World!")]), - body([], [ - h1([], [text("Hello, World!")]), - input([disabled(True)]), - img([src("https://source.unsplash.com/random")]), - ]), - ]) -} - -pub fn escaped_attribute() { - div( - [ - class("'badquotes'"), - style([#("background", "\">")]), - attribute("example", "{\"mykey\": \"myvalue\"}"), - ], - [], - ) -} diff --git a/packages/lustre/test/build.gleam b/packages/lustre/test/build.gleam deleted file mode 100644 index b223027..0000000 --- a/packages/lustre/test/build.gleam +++ /dev/null @@ -1,122 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/bool -import gleam/io -import gleam/regex.{Options} -import gleam/result -import gleam/string -import shellout -import simplifile - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - io.debug({ - use exists <- try(verify_esbuild(), SimplifileError) - use <- bool.guard(!exists, Error(MissingEsbuild)) - - use _ <- try(build_for_javascript(), ShelloutError) - use _ <- try(bundle_server_component(), ShelloutError) - use _ <- try(bundle_minified_server_component(), ShelloutError) - - use script <- try(read_script(), SimplifileError) - use module <- try(read_module(), SimplifileError) - use _ <- try(inject_script(script, module), SimplifileError) - - use _ <- try(format_project(), ShelloutError) - - Ok(Nil) - }) -} - -// CONSTANTS ------------------------------------------------------------------- - -const esbuild = "./build/.lustre/bin/esbuild" - -// STEPS ----------------------------------------------------------------------- - -fn verify_esbuild() { - simplifile.verify_is_file(esbuild) -} - -fn build_for_javascript() { - shellout.command( - run: "gleam", - with: ["build", "--target", "javascript"], - in: ".", - opt: [], - ) -} - -fn bundle_server_component() { - shellout.command( - run: esbuild, - with: [ - "./src/server-component.mjs", "--bundle", "--format=esm", - "--outfile=./priv/static/lustre-server-component.mjs", - ], - in: ".", - opt: [], - ) -} - -fn bundle_minified_server_component() { - shellout.command( - run: esbuild, - with: [ - "./src/server-component.mjs", "--bundle", "--minify", "--format=esm", - "--outfile=./priv/static/lustre-server-component.min.mjs", - ], - in: ".", - opt: [], - ) -} - -fn read_script() { - simplifile.read("./priv/static/lustre-server-component.min.mjs") - |> result.map(string.replace(_, "\"", "\\\"")) - |> result.map(string.trim) -} - -fn read_module() { - simplifile.read("./src/lustre/server_component.gleam") -} - -fn inject_script(script, module) { - let inject_regex = "// <>\\n.+\\n.+\\n \\)," - let options = Options(case_insensitive: False, multi_line: True) - let assert Ok(re) = regex.compile(inject_regex, options) - let assert [before, after] = regex.split(re, module) - - simplifile.write( - "./src/lustre/server_component.gleam", - before - <> "// <>\n element.text(\"" - <> script - <> "\")," - <> after, - ) -} - -fn format_project() { - shellout.command(run: "gleam", with: ["format"], in: ".", opt: []) -} - -// ERROR HANDLING -------------------------------------------------------------- - -pub type Error { - MissingEsbuild - ShelloutError(#(Int, String)) - SimplifileError(simplifile.FileError) -} - -fn try( - result: Result(a, e), - to_error: fn(e) -> Error, - then: fn(a) -> Result(b, Error), -) -> Result(b, Error) { - case result { - Ok(value) -> then(value) - Error(error) -> Error(to_error(error)) - } -} diff --git a/packages/lustre/test/lustre_test.gleam b/packages/lustre/test/lustre_test.gleam deleted file mode 100644 index 2330b3c..0000000 --- a/packages/lustre/test/lustre_test.gleam +++ /dev/null @@ -1,175 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import apps/counter -import apps/fragment -import apps/static -import birdie -import gleam/erlang/process -import gleam/json -import gleeunit -import lustre -import lustre/element -import lustre/internals/patch -import lustre/internals/runtime.{Debug, Dispatch, Shutdown, View} - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - gleeunit.main() -} - -// TESTS ----------------------------------------------------------------------- - -pub fn static_test() { - let title = "Can render static HTML" - let el = static.view() - - birdie.snap(element.to_string(el), title) -} - -@target(erlang) -pub fn counter_init_test() { - let title = "Can render an application's initial state." - let app = lustre.simple(counter.init, counter.update, counter.view) - let assert Ok(runtime) = lustre.start_actor(app, 0) - let el = - process.call( - runtime, - fn(reply) { - process.send(reply, _) - |> View - |> Debug - }, - 100, - ) - - birdie.snap(element.to_string(el), title) - process.send(runtime, Shutdown) -} - -@target(erlang) -pub fn counter_update_test() { - let title = "Can render an application's state after some updates." - let app = lustre.simple(counter.init, counter.update, counter.view) - let assert Ok(runtime) = lustre.start_actor(app, 0) - - process.send(runtime, Dispatch(counter.Increment)) - process.send(runtime, Dispatch(counter.Increment)) - process.send(runtime, Dispatch(counter.Increment)) - - let el = - process.call( - runtime, - fn(reply) { - process.send(reply, _) - |> View - |> Debug - }, - 100, - ) - - birdie.snap(element.to_string(el), title) - process.send(runtime, Shutdown) -} - -@target(erlang) -pub fn counter_diff_test() { - let title = "Can compute a diff from one render to the next" - let app = lustre.simple(counter.init, counter.update, counter.view) - let assert Ok(runtime) = lustre.start_actor(app, 0) - - let prev = - process.call( - runtime, - fn(reply) { - process.send(reply, _) - |> View - |> Debug - }, - 100, - ) - - process.send(runtime, Dispatch(counter.Increment)) - process.send(runtime, Dispatch(counter.Increment)) - process.send(runtime, Dispatch(counter.Increment)) - - let next = - process.call( - runtime, - fn(reply) { - process.send(reply, _) - |> View - |> Debug - }, - 100, - ) - - let diff = patch.elements(prev, next) - - birdie.snap(json.to_string(patch.element_diff_to_json(diff)), title) - process.send(runtime, Shutdown) -} - -@target(erlang) -pub fn fragment_init_test() { - let title = "Can render an application's initial state when using fragments" - let app = lustre.simple(fragment.init, fragment.update, fragment.view) - let assert Ok(runtime) = lustre.start_actor(app, 0) - let el = - process.call( - runtime, - fn(reply) { - process.send(reply, _) - |> View - |> Debug - }, - 100, - ) - - birdie.snap(element.to_string(el), title) - process.send(runtime, Shutdown) -} - -@target(erlang) -pub fn fragment_counter_diff_test() { - let title = "Can compute a diff from one render to the next with fragments" - let app = lustre.simple(fragment.init, fragment.update, fragment.view) - let assert Ok(runtime) = lustre.start_actor(app, 0) - - let prev = - process.call( - runtime, - fn(reply) { - process.send(reply, _) - |> View - |> Debug - }, - 100, - ) - - process.send(runtime, Dispatch(fragment.Increment)) - process.send(runtime, Dispatch(fragment.Increment)) - process.send(runtime, Dispatch(fragment.Increment)) - - let next = - process.call( - runtime, - fn(reply) { - process.send(reply, _) - |> View - |> Debug - }, - 100, - ) - - let diff = patch.elements(prev, next) - - birdie.snap(json.to_string(patch.element_diff_to_json(diff)), title) - process.send(runtime, Shutdown) -} - -pub fn escaped_attribute_test() { - let title = "Can safely escape dangerous symbols in attributes" - let el = static.escaped_attribute() - birdie.snap(element.to_string(el), title) -} diff --git a/packages/lustre/test/utils.js b/packages/lustre/test/utils.js deleted file mode 100644 index c3abaee..0000000 --- a/packages/lustre/test/utils.js +++ /dev/null @@ -1,29 +0,0 @@ -import { parseHTML } from 'linkedom'; -import { vi } from 'vitest'; - -// Parse the starting state of the basic starting template -export function setupDOM() { - const result = parseHTML(` - - - - - - - 🚧 {app_name} - - - - -
- -`); - - global.HTMLElement = result.HTMLElement; - global.Node = result.Node; - global.document = result.document; - global.window = result.window; - global.window.requestAnimationFrame = vi.fn().mockImplementation((cb) => cb()); - - return result; -} \ No newline at end of file diff --git a/packages/lustre/test/vdom.ffi.bench.js b/packages/lustre/test/vdom.ffi.bench.js deleted file mode 100644 index a8e139e..0000000 --- a/packages/lustre/test/vdom.ffi.bench.js +++ /dev/null @@ -1,27 +0,0 @@ -import { bench, describe } from "vitest"; -import { setupDOM } from "./utils"; -import { morph } from "../src/vdom.ffi.mjs"; -import { smoke_test } from "../test-apps/vdom-test-templates/build/dev/javascript/app/client_test.mjs"; - -// BENCH ------------------------------------------------------------------------ - -describe("vdom morph bench", () => { - let appEl; - let template; - bench( - "smoke test morph", - () => { - appEl = morph(appEl, template); - }, - { - setup: () => { - const result = setupDOM(); - - global.Node = result.Node; - global.document = result.document; - appEl = document.getElementById("app"); - template = smoke_test(); - } - } - ); -}); diff --git a/packages/lustre/test/vdom.ffi.test.js b/packages/lustre/test/vdom.ffi.test.js deleted file mode 100644 index 2614c71..0000000 --- a/packages/lustre/test/vdom.ffi.test.js +++ /dev/null @@ -1,150 +0,0 @@ -import { beforeEach, describe, expect, test } from "vitest"; -import { setupDOM } from "./utils.js"; - -import { morph } from "@root/src/vdom.ffi.mjs"; - -// built via npm script "build:test:vdom" -import { - disabled_attr_test, - dynamic_content_test, - fragment_test, - keyed_test, - smoke_test, -} from "../test-apps/vdom-test-templates/build/dev/javascript/app/client_test.mjs"; - -let appEl; -beforeEach(() => { - setupDOM(); - appEl = document.getElementById("app"); -}); - -// TEST ------------------------------------------------------------------------ - -const singleMorphSnapshot = (name, template) => { - appEl = morph(appEl, template); - - const currentState = document.toString(); - - expect(currentState).toMatchSnapshot(name); -}; - -describe("vdom morph", () => { - test(`should render smoke test with vdom morph`, () => { - const template = smoke_test(); - - singleMorphSnapshot("smoke_test", template); - }); - - test(`should render using vdom morph with fragments`, () => { - const template = fragment_test(); - - singleMorphSnapshot("fragment_test", template); - }); - - test(`should render using vdom morph with keys`, () => { - const template = keyed_test(); - - singleMorphSnapshot("fragment_test", template); - }); - - test(`should be stable when vdom morph is called multiple times with no changes using fragment`, () => { - const template = fragment_test(); - appEl = morph(appEl, template); - - const initialState = document.toString(); - - const states = []; - for (let i = 0; i < 5; i++) { - appEl = morph(appEl, template); - states.push(document.toString()); - } - - states.forEach((state) => { - expect(state).toEqual(initialState); - }); - }); - - test(`should be stable when vdom morph is called multiple times with no changes using keys`, () => { - const template = keyed_test(); - appEl = morph(appEl, template); - - const initialState = document.toString(); - - const states = []; - for (let i = 0; i < 5; i++) { - appEl = morph(appEl, template); - states.push(document.toString()); - } - - states.forEach((state) => { - expect(state).toEqual(initialState); - }); - }); - - test(`should render updated templates`, () => { - const initialTemplate = dynamic_content_test(0, "initial_name"); - - appEl = morph(appEl, initialTemplate); - - const initialState = document.toString(); - - expect(initialState).toContain("0"); - expect(initialState).toContain("initial_name"); - - const updatedtemplate = dynamic_content_test(56, "updated_name"); - - appEl = morph(appEl, updatedtemplate); - - const updatedState = document.toString(); - - expect(updatedState).toContain("56"); - expect(updatedState).toContain("updated_name"); - }); -}); - -describe("vdom morph attribute", () => { - describe("disabled", () => { - test("should not be disabled when is_disabled is false", () => { - const template = disabled_attr_test(false); - - appEl = morph(appEl, template); - - const domResult = document.toString(); - - expect(domResult).toContain("input"); - expect(domResult).not.toContain("disabled"); - }); - - test("should be disabled when is_disabled is true", () => { - const template = disabled_attr_test(true); - - appEl = morph(appEl, template); - - const domResult = document.toString(); - - expect(domResult).toContain("input"); - expect(domResult).toContain("disabled"); - }); - - - // this fails today - test.skip("should be stable when disabled attribute does not change", () => { - const template = disabled_attr_test(true); - - appEl = morph(appEl, template); - - const initialState = document.toString(); - - const states = []; - for (let i = 0; i < 5; i++) { - appEl = morph(appEl, template); - states.push(document.toString()); - } - - states.forEach((state) => { - expect(state).toEqual(initialState); - }); - }); - }); -}); - diff --git a/packages/lustre/vitest.config.js b/packages/lustre/vitest.config.js deleted file mode 100644 index e0a777d..0000000 --- a/packages/lustre/vitest.config.js +++ /dev/null @@ -1,18 +0,0 @@ -import { configDefaults, defineConfig } from "vitest/config"; -import { basename, dirname, join, resolve } from 'node:path'; - -export default defineConfig({ - test: { - alias: { - '@root': resolve(__dirname) - }, - benchmark: { - include: ["**/test/**/*.bench.js"], - exclude: [...configDefaults.exclude, "**/build/**/*"], - }, - include: ["**/test/**/*.test.js"], - exclude: [...configDefaults.exclude, "**/build/**/*"], - resolveSnapshotPath: (testPath, snapExtension) => - join(join(dirname(testPath), '../', 'vitest_snapshots'), `${basename(testPath)}${snapExtension}`) - }, -}); diff --git a/packages/lustre/vitest_snapshots/02-interactivity.test.js.snap b/packages/lustre/vitest_snapshots/02-interactivity.test.js.snap deleted file mode 100644 index c7c7ce2..0000000 --- a/packages/lustre/vitest_snapshots/02-interactivity.test.js.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`counter example > should render initially 1`] = ` -" - - - - - - - 🚧 {app_name} - - - - -

0

- -" -`; diff --git a/packages/lustre/vitest_snapshots/vdom.ffi.test.js.snap b/packages/lustre/vitest_snapshots/vdom.ffi.test.js.snap deleted file mode 100644 index fa11c73..0000000 --- a/packages/lustre/vitest_snapshots/vdom.ffi.test.js.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`vdom morph > should render smoke test with vdom morph > smoke_test 1`] = ` -" - - - - - - - 🚧 {app_name} - - - - -

smoke test

- -" -`; - -exports[`vdom morph > should render using vdom morph with fragments > fragment_test 1`] = ` -" - - - - - - - 🚧 {app_name} - - - - -
Person NamePerson Age
Person One18
Person Two24
Person Three30
- -" -`; - -exports[`vdom morph > should render using vdom morph with keys > fragment_test 1`] = ` -" - - - - - - - 🚧 {app_name} - - - - -
    Person One18Person Two24Person Three30
- -" -`; diff --git a/packages/pgo/.gitignore b/packages/pgo/.gitignore deleted file mode 100644 index 4fa557d..0000000 --- a/packages/pgo/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -*.beam -*.iml -*.o -*.plt -*.swo -*.swp -*~ -.erlang.cookie -.eunit -.idea -.rebar -.rebar3 -_* -_build -ebin -erl_crash.dump -gen -log -logs -rebar3.crashdump -doc -build diff --git a/packages/pgo/CHANGELOG.md b/packages/pgo/CHANGELOG.md deleted file mode 100644 index af70bf3..0000000 --- a/packages/pgo/CHANGELOG.md +++ /dev/null @@ -1,48 +0,0 @@ -# Changelog - -## v0.8.0 - 2024-05-11 - -- Added `array` column handling, accepting a `List` as value. -- Added support for `ssl_options` when an SSL connection is setuped. - -## v0.7.0 - 2024-04-05 - -- The password is now optional in the `url_config` function, defaulting to no - password if none is given. - -## v0.6.1 - 2024-01-16 - -- Relaxed the stdlib version constraint. - -## v0.6.0 - 2023-11-06 - -- Updated for Gleam v0.32.0. - -## v0.5.0 - 2023-08-03 - -- Updated for Gleam v0.30.0. - -## v0.4.1 - 2023-03-02 - -- Updated for Gleam v0.27.0. - -## v0.4.0 - 2022-06-12 - -- IPv6 is now supported through a config option. - -## v0.3.0 - 2022-02-17 - -- Added the `ConnectionUnavailable` error variant. - -## v0.2.0 - 2022-01-26 - -- Migrate to the Gleam build tool. -- API redesigned. - -## v0.1.1 - 2020-11-26 - -- Dependencies updates and versions relaxed. - -## v0.1.0 - 2020-08-24 - -- Initial release. diff --git a/packages/pgo/LICENSE b/packages/pgo/LICENSE deleted file mode 100644 index 59e1345..0000000 --- a/packages/pgo/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - Copyright {{copyright_year}}, {{author_name}} <{{author_email}}>. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/packages/pgo/README.md b/packages/pgo/README.md deleted file mode 100644 index d776483..0000000 --- a/packages/pgo/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Gleam PGO - -A PostgreSQL database client for Gleam, based on [PGO][erlang-pgo]. - -[erlang-pgo]: https://github.com/erleans/pgo - -```gleam -import gleam/pgo -import gleam/dynamic -import gleeunit/should - -pub fn main() { - // Start a database connection pool. - // Typically you will want to create one pool for use in your program - let db = pgo.connect(pgo.Config( - ..pgo.default_config(), - host: "localhost", - database: "my_database", - pool_size: 15, - )) - - // An SQL statement to run. It takes one int as a parameter - let sql = " - select - name, age, colour, friends - from - cats - where - id = $1" - - // This is the decoder for the value returned by the query - let return_type = dynamic.tuple4( - dynamic.string, - dynamic.int, - dynamic.string, - dynamic.list(dynamic.string), - ) - - // Run the query against the PostgreSQL database - // The int `1` is given as a parameter - let assert Ok(response) = - pgo.execute(sql, db, [pgo.int(1)], return_type) - - // And then do something with the returned results - response.count - |> should.equal(2) - response.rows - |> should.equal([ - #("Nubi", 3, "black", ["Al", "Cutlass"]), - ]) -} -``` - -## Installation - -```sh -gleam add gleam_pgo -``` diff --git a/packages/pgo/gleam.toml b/packages/pgo/gleam.toml deleted file mode 100644 index 94aa5d8..0000000 --- a/packages/pgo/gleam.toml +++ /dev/null @@ -1,18 +0,0 @@ -name = "gleam_pgo" -version = "0.7.0" -gleam = ">= 0.32.0" -licences = ["Apache-2.0"] -description = "Gleam bindings to the PGO PostgreSQL client" - -repository = { type = "github", user = "gleam-experiments", repo = "pgo" } -links = [ - { title = "Website", href = "https://gleam.run" }, - { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, -] - -[dependencies] -gleam_stdlib = "~> 0.20 or ~> 1.0" -pgo = "~> 0.12" - -[dev-dependencies] -gleeunit = "~> 1.0" diff --git a/packages/pgo/manifest.toml b/packages/pgo/manifest.toml deleted file mode 100644 index 048eefe..0000000 --- a/packages/pgo/manifest.toml +++ /dev/null @@ -1,17 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, - { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, - { name = "opentelemetry_api", version = "1.3.0", build_tools = ["rebar3", "mix"], requirements = ["opentelemetry_semantic_conventions"], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "B9E5FF775FD064FA098DBA3C398490B77649A352B40B0B730A6B7DC0BDD68858" }, - { name = "opentelemetry_semantic_conventions", version = "0.2.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_semantic_conventions", source = "hex", outer_checksum = "D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895" }, - { name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" }, - { name = "pgo", version = "0.14.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9" }, -] - -[requirements] -gleam_stdlib = { version = "~> 0.20 or ~> 1.0" } -gleeunit = { version = "~> 1.0" } -pgo = { version = "~> 0.12" } diff --git a/packages/pgo/src/gleam/pgo.gleam b/packages/pgo/src/gleam/pgo.gleam deleted file mode 100644 index 24fdb66..0000000 --- a/packages/pgo/src/gleam/pgo.gleam +++ /dev/null @@ -1,494 +0,0 @@ -//// Postgresql client -//// -//// Gleam wrapper around pgo library - -// TODO: transactions -// TODO: JSON support -import gleam/dynamic.{type DecodeErrors, type Decoder, type Dynamic} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import gleam/string -import gleam/uri.{Uri} - -/// The configuration for a pool of connections. -pub type Config { - Config( - /// (default: 127.0.0.1): Database server hostname. - host: String, - /// (default: 5432): Port the server is listening on. - port: Int, - /// Name of database to use. - database: String, - /// Username to connect to database as. - user: String, - /// Password for the user. - password: Option(String), - /// (default: false): Whether to use SSL or not. - ssl: Bool, - /// (default: []): List of 2-tuples, where key and value must be binary - /// strings. You can include any Postgres connection parameter here, such as - /// `#("application_name", "myappname")` and `#("timezone", "GMT")`. - connection_parameters: List(#(String, String)), - /// (default: 1): Number of connections to keep open with the database - pool_size: Int, - /// (default: 50) Checking out connections is handled through a queue. If it - /// takes longer than queue_target to get out of the queue for longer than - /// queue_interval then the queue_target will be doubled and checkouts will - /// start to be dropped if that target is surpassed. - queue_target: Int, - /// (default: 1000) - queue_interval: Int, - /// (default: 1000): The database is pinged every idle_interval when the - /// connection is idle. - idle_interval: Int, - /// trace (default: false): pgo is instrumented with [OpenCensus][1] and - /// when this option is true a span will be created (if sampled). - /// - /// [1]: https://opencensus.io/ - trace: Bool, - /// Which internet protocol to use for this connection - ip_version: IpVersion, - ) -} - -/// The internet protocol version to use. -pub type IpVersion { - /// Internet Protocol version 6 (IPv6) - Ipv4 - /// Internet Protocol version 4 (IPv4) - Ipv6 -} - -/// The default configuration for a connection pool, with a single connection. -/// You will likely want to increase the size of the pool for your application. -/// -pub fn default_config() -> Config { - Config( - host: "127.0.0.1", - port: 5432, - database: "postgres", - user: "postgres", - password: None, - ssl: False, - connection_parameters: [], - pool_size: 1, - queue_target: 50, - queue_interval: 1000, - idle_interval: 1000, - trace: False, - ip_version: Ipv4, - ) -} - -/// Parse a database url into configuration that can be used to start a pool. -pub fn url_config(database_url: String) -> Result(Config, Nil) { - use uri <- result.then(uri.parse(database_url)) - use #(userinfo, host, path, db_port) <- result.then(case uri { - Uri( - scheme: Some("postgres"), - userinfo: Some(userinfo), - host: Some(host), - port: Some(db_port), - path: path, - .., - ) -> Ok(#(userinfo, host, path, db_port)) - _ -> Error(Nil) - }) - use #(user, password) <- result.then(case string.split(userinfo, ":") { - [user] -> Ok(#(user, None)) - [user, password] -> Ok(#(user, Some(password))) - _ -> Error(Nil) - }) - case string.split(path, "/") { - ["", database] -> - Ok( - Config( - ..default_config(), - host: host, - port: db_port, - database: database, - user: user, - password: password, - ), - ) - _ -> Error(Nil) - } -} - -/// A pool of one or more database connections against which queries can be -/// made. -/// -/// Created using the `connect` function and shut-down with the `disconnect` -/// function. -pub type Connection - -/// Start a database connection pool. -/// -/// The pool is started in a new process and will asynchronously connect to the -/// PostgreSQL instance specified in the config. If the configuration is invalid -/// or it cannot connect for another reason it will continue to attempt to -/// connect, and any queries made using the connection pool will fail. -@external(erlang, "gleam_pgo_ffi", "connect") -pub fn connect(a: Config) -> Connection - -/// Shut down a connection pool. -@external(erlang, "gleam_pgo_ffi", "disconnect") -pub fn disconnect(a: Connection) -> Nil - -/// A value that can be sent to PostgreSQL as one of the arguments to a -/// parameterised SQL query. -pub type Value - -@external(erlang, "gleam_pgo_ffi", "null") -pub fn null() -> Value - -@external(erlang, "gleam_pgo_ffi", "coerce") -pub fn bool(a: Bool) -> Value - -@external(erlang, "gleam_pgo_ffi", "coerce") -pub fn int(a: Int) -> Value - -@external(erlang, "gleam_pgo_ffi", "coerce") -pub fn float(a: Float) -> Value - -@external(erlang, "gleam_pgo_ffi", "coerce") -pub fn text(a: String) -> Value - -@external(erlang, "gleam_pgo_ffi", "coerce") -pub fn bytea(a: BitArray) -> Value - -@external(erlang, "gleam_pgo_ffi", "coerce") -pub fn array(a: List(a)) -> Value - -pub fn nullable(inner_type: fn(a) -> Value, value: Option(a)) -> Value { - case value { - Some(term) -> inner_type(term) - None -> null() - } -} - -/// The rows and number of rows that are returned by a database query. -pub type Returned(t) { - Returned(count: Int, rows: List(t)) -} - -@external(erlang, "gleam_pgo_ffi", "query") -fn run_query( - a: Connection, - b: String, - c: List(Value), -) -> Result(#(Int, List(Dynamic)), QueryError) - -pub type QueryError { - /// The query failed as a database constraint would have been violated by the - /// change. - ConstraintViolated(message: String, constraint: String, detail: String) - /// The query failed within the database. - /// https://www.postgresql.org/docs/current/errcodes-appendix.html - PostgresqlError(code: String, name: String, message: String) - // The number of arguments supplied did not match the number of parameters - // that the query has. - UnexpectedArgumentCount(expected: Int, got: Int) - /// One of the arguments supplied was not of the type that the query required. - UnexpectedArgumentType(expected: String, got: String) - /// The rows returned by the database could not be decoded using the supplied - /// dynamic decoder. - UnexpectedResultType(DecodeErrors) - /// No connection was available to execute the query. This may be due to - /// invalid connection details such as an invalid username or password. - ConnectionUnavailable -} - -/// Run a query against a PostgreSQL database. -/// -/// The provided dynamic decoder is used to decode the rows returned by -/// PostgreSQL. If you are not interested in any returned rows you may want to -/// use the `dynamic.dynamic` decoder. -/// -pub fn execute( - query sql: String, - on pool: Connection, - with arguments: List(Value), - expecting decoder: Decoder(t), -) -> Result(Returned(t), QueryError) { - use #(count, rows) <- result.then(run_query(pool, sql, arguments)) - use rows <- result.then( - list.try_map(over: rows, with: decoder) - |> result.map_error(UnexpectedResultType), - ) - Ok(Returned(count, rows)) -} - -/// Get the name for a PostgreSQL error code. -/// -/// ```gleam -/// > error_code_name("01007") -/// Ok("privilege_not_granted") -/// ``` -/// -/// https://www.postgresql.org/docs/current/errcodes-appendix.html -pub fn error_code_name(error_code: String) -> Result(String, Nil) { - case error_code { - "00000" -> Ok("successful_completion") - "01000" -> Ok("warning") - "0100C" -> Ok("dynamic_result_sets_returned") - "01008" -> Ok("implicit_zero_bit_padding") - "01003" -> Ok("null_value_eliminated_in_set_function") - "01007" -> Ok("privilege_not_granted") - "01006" -> Ok("privilege_not_revoked") - "01004" -> Ok("string_data_right_truncation") - "01P01" -> Ok("deprecated_feature") - "02000" -> Ok("no_data") - "02001" -> Ok("no_additional_dynamic_result_sets_returned") - "03000" -> Ok("sql_statement_not_yet_complete") - "08000" -> Ok("connection_exception") - "08003" -> Ok("connection_does_not_exist") - "08006" -> Ok("connection_failure") - "08001" -> Ok("sqlclient_unable_to_establish_sqlconnection") - "08004" -> Ok("sqlserver_rejected_establishment_of_sqlconnection") - "08007" -> Ok("transaction_resolution_unknown") - "08P01" -> Ok("protocol_violation") - "09000" -> Ok("triggered_action_exception") - "0A000" -> Ok("feature_not_supported") - "0B000" -> Ok("invalid_transaction_initiation") - "0F000" -> Ok("locator_exception") - "0F001" -> Ok("invalid_locator_specification") - "0L000" -> Ok("invalid_grantor") - "0LP01" -> Ok("invalid_grant_operation") - "0P000" -> Ok("invalid_role_specification") - "0Z000" -> Ok("diagnostics_exception") - "0Z002" -> Ok("stacked_diagnostics_accessed_without_active_handler") - "20000" -> Ok("case_not_found") - "21000" -> Ok("cardinality_violation") - "22000" -> Ok("data_exception") - "2202E" -> Ok("array_subscript_error") - "22021" -> Ok("character_not_in_repertoire") - "22008" -> Ok("datetime_field_overflow") - "22012" -> Ok("division_by_zero") - "22005" -> Ok("error_in_assignment") - "2200B" -> Ok("escape_character_conflict") - "22022" -> Ok("indicator_overflow") - "22015" -> Ok("interval_field_overflow") - "2201E" -> Ok("invalid_argument_for_logarithm") - "22014" -> Ok("invalid_argument_for_ntile_function") - "22016" -> Ok("invalid_argument_for_nth_value_function") - "2201F" -> Ok("invalid_argument_for_power_function") - "2201G" -> Ok("invalid_argument_for_width_bucket_function") - "22018" -> Ok("invalid_character_value_for_cast") - "22007" -> Ok("invalid_datetime_format") - "22019" -> Ok("invalid_escape_character") - "2200D" -> Ok("invalid_escape_octet") - "22025" -> Ok("invalid_escape_sequence") - "22P06" -> Ok("nonstandard_use_of_escape_character") - "22010" -> Ok("invalid_indicator_parameter_value") - "22023" -> Ok("invalid_parameter_value") - "22013" -> Ok("invalid_preceding_or_following_size") - "2201B" -> Ok("invalid_regular_expression") - "2201W" -> Ok("invalid_row_count_in_limit_clause") - "2201X" -> Ok("invalid_row_count_in_result_offset_clause") - "2202H" -> Ok("invalid_tablesample_argument") - "2202G" -> Ok("invalid_tablesample_repeat") - "22009" -> Ok("invalid_time_zone_displacement_value") - "2200C" -> Ok("invalid_use_of_escape_character") - "2200G" -> Ok("most_specific_type_mismatch") - "22004" -> Ok("null_value_not_allowed") - "22002" -> Ok("null_value_no_indicator_parameter") - "22003" -> Ok("numeric_value_out_of_range") - "2200H" -> Ok("sequence_generator_limit_exceeded") - "22026" -> Ok("string_data_length_mismatch") - "22001" -> Ok("string_data_right_truncation") - "22011" -> Ok("substring_error") - "22027" -> Ok("trim_error") - "22024" -> Ok("unterminated_c_string") - "2200F" -> Ok("zero_length_character_string") - "22P01" -> Ok("floating_point_exception") - "22P02" -> Ok("invalid_text_representation") - "22P03" -> Ok("invalid_binary_representation") - "22P04" -> Ok("bad_copy_file_format") - "22P05" -> Ok("untranslatable_character") - "2200L" -> Ok("not_an_xml_document") - "2200M" -> Ok("invalid_xml_document") - "2200N" -> Ok("invalid_xml_content") - "2200S" -> Ok("invalid_xml_comment") - "2200T" -> Ok("invalid_xml_processing_instruction") - "22030" -> Ok("duplicate_json_object_key_value") - "22031" -> Ok("invalid_argument_for_sql_json_datetime_function") - "22032" -> Ok("invalid_json_text") - "22033" -> Ok("invalid_sql_json_subscript") - "22034" -> Ok("more_than_one_sql_json_item") - "22035" -> Ok("no_sql_json_item") - "22036" -> Ok("non_numeric_sql_json_item") - "22037" -> Ok("non_unique_keys_in_a_json_object") - "22038" -> Ok("singleton_sql_json_item_required") - "22039" -> Ok("sql_json_array_not_found") - "2203A" -> Ok("sql_json_member_not_found") - "2203B" -> Ok("sql_json_number_not_found") - "2203C" -> Ok("sql_json_object_not_found") - "2203D" -> Ok("too_many_json_array_elements") - "2203E" -> Ok("too_many_json_object_members") - "2203F" -> Ok("sql_json_scalar_required") - "23000" -> Ok("integrity_constraint_violation") - "23001" -> Ok("restrict_violation") - "23502" -> Ok("not_null_violation") - "23503" -> Ok("foreign_key_violation") - "23505" -> Ok("unique_violation") - "23514" -> Ok("check_violation") - "23P01" -> Ok("exclusion_violation") - "24000" -> Ok("invalid_cursor_state") - "25000" -> Ok("invalid_transaction_state") - "25001" -> Ok("active_sql_transaction") - "25002" -> Ok("branch_transaction_already_active") - "25008" -> Ok("held_cursor_requires_same_isolation_level") - "25003" -> Ok("inappropriate_access_mode_for_branch_transaction") - "25004" -> Ok("inappropriate_isolation_level_for_branch_transaction") - "25005" -> Ok("no_active_sql_transaction_for_branch_transaction") - "25006" -> Ok("read_only_sql_transaction") - "25007" -> Ok("schema_and_data_statement_mixing_not_supported") - "25P01" -> Ok("no_active_sql_transaction") - "25P02" -> Ok("in_failed_sql_transaction") - "25P03" -> Ok("idle_in_transaction_session_timeout") - "26000" -> Ok("invalid_sql_statement_name") - "27000" -> Ok("triggered_data_change_violation") - "28000" -> Ok("invalid_authorization_specification") - "28P01" -> Ok("invalid_password") - "2B000" -> Ok("dependent_privilege_descriptors_still_exist") - "2BP01" -> Ok("dependent_objects_still_exist") - "2D000" -> Ok("invalid_transaction_termination") - "2F000" -> Ok("sql_routine_exception") - "2F005" -> Ok("function_executed_no_return_statement") - "2F002" -> Ok("modifying_sql_data_not_permitted") - "2F003" -> Ok("prohibited_sql_statement_attempted") - "2F004" -> Ok("reading_sql_data_not_permitted") - "34000" -> Ok("invalid_cursor_name") - "38000" -> Ok("external_routine_exception") - "38001" -> Ok("containing_sql_not_permitted") - "38002" -> Ok("modifying_sql_data_not_permitted") - "38003" -> Ok("prohibited_sql_statement_attempted") - "38004" -> Ok("reading_sql_data_not_permitted") - "39000" -> Ok("external_routine_invocation_exception") - "39001" -> Ok("invalid_sqlstate_returned") - "39004" -> Ok("null_value_not_allowed") - "39P01" -> Ok("trigger_protocol_violated") - "39P02" -> Ok("srf_protocol_violated") - "39P03" -> Ok("event_trigger_protocol_violated") - "3B000" -> Ok("savepoint_exception") - "3B001" -> Ok("invalid_savepoint_specification") - "3D000" -> Ok("invalid_catalog_name") - "3F000" -> Ok("invalid_schema_name") - "40000" -> Ok("transaction_rollback") - "40002" -> Ok("transaction_integrity_constraint_violation") - "40001" -> Ok("serialization_failure") - "40003" -> Ok("statement_completion_unknown") - "40P01" -> Ok("deadlock_detected") - "42000" -> Ok("syntax_error_or_access_rule_violation") - "42601" -> Ok("syntax_error") - "42501" -> Ok("insufficient_privilege") - "42846" -> Ok("cannot_coerce") - "42803" -> Ok("grouping_error") - "42P20" -> Ok("windowing_error") - "42P19" -> Ok("invalid_recursion") - "42830" -> Ok("invalid_foreign_key") - "42602" -> Ok("invalid_name") - "42622" -> Ok("name_too_long") - "42939" -> Ok("reserved_name") - "42804" -> Ok("datatype_mismatch") - "42P18" -> Ok("indeterminate_datatype") - "42P21" -> Ok("collation_mismatch") - "42P22" -> Ok("indeterminate_collation") - "42809" -> Ok("wrong_object_type") - "428C9" -> Ok("generated_always") - "42703" -> Ok("undefined_column") - "42883" -> Ok("undefined_function") - "42P01" -> Ok("undefined_table") - "42P02" -> Ok("undefined_parameter") - "42704" -> Ok("undefined_object") - "42701" -> Ok("duplicate_column") - "42P03" -> Ok("duplicate_cursor") - "42P04" -> Ok("duplicate_database") - "42723" -> Ok("duplicate_function") - "42P05" -> Ok("duplicate_prepared_statement") - "42P06" -> Ok("duplicate_schema") - "42P07" -> Ok("duplicate_table") - "42712" -> Ok("duplicate_alias") - "42710" -> Ok("duplicate_object") - "42702" -> Ok("ambiguous_column") - "42725" -> Ok("ambiguous_function") - "42P08" -> Ok("ambiguous_parameter") - "42P09" -> Ok("ambiguous_alias") - "42P10" -> Ok("invalid_column_reference") - "42611" -> Ok("invalid_column_definition") - "42P11" -> Ok("invalid_cursor_definition") - "42P12" -> Ok("invalid_database_definition") - "42P13" -> Ok("invalid_function_definition") - "42P14" -> Ok("invalid_prepared_statement_definition") - "42P15" -> Ok("invalid_schema_definition") - "42P16" -> Ok("invalid_table_definition") - "42P17" -> Ok("invalid_object_definition") - "44000" -> Ok("with_check_option_violation") - "53000" -> Ok("insufficient_resources") - "53100" -> Ok("disk_full") - "53200" -> Ok("out_of_memory") - "53300" -> Ok("too_many_connections") - "53400" -> Ok("configuration_limit_exceeded") - "54000" -> Ok("program_limit_exceeded") - "54001" -> Ok("statement_too_complex") - "54011" -> Ok("too_many_columns") - "54023" -> Ok("too_many_arguments") - "55000" -> Ok("object_not_in_prerequisite_state") - "55006" -> Ok("object_in_use") - "55P02" -> Ok("cant_change_runtime_param") - "55P03" -> Ok("lock_not_available") - "55P04" -> Ok("unsafe_new_enum_value_usage") - "57000" -> Ok("operator_intervention") - "57014" -> Ok("query_canceled") - "57P01" -> Ok("admin_shutdown") - "57P02" -> Ok("crash_shutdown") - "57P03" -> Ok("cannot_connect_now") - "57P04" -> Ok("database_dropped") - "57P05" -> Ok("idle_session_timeout") - "58000" -> Ok("system_error") - "58030" -> Ok("io_error") - "58P01" -> Ok("undefined_file") - "58P02" -> Ok("duplicate_file") - "72000" -> Ok("snapshot_too_old") - "F0000" -> Ok("config_file_error") - "F0001" -> Ok("lock_file_exists") - "HV000" -> Ok("fdw_error") - "HV005" -> Ok("fdw_column_name_not_found") - "HV002" -> Ok("fdw_dynamic_parameter_value_needed") - "HV010" -> Ok("fdw_function_sequence_error") - "HV021" -> Ok("fdw_inconsistent_descriptor_information") - "HV024" -> Ok("fdw_invalid_attribute_value") - "HV007" -> Ok("fdw_invalid_column_name") - "HV008" -> Ok("fdw_invalid_column_number") - "HV004" -> Ok("fdw_invalid_data_type") - "HV006" -> Ok("fdw_invalid_data_type_descriptors") - "HV091" -> Ok("fdw_invalid_descriptor_field_identifier") - "HV00B" -> Ok("fdw_invalid_handle") - "HV00C" -> Ok("fdw_invalid_option_index") - "HV00D" -> Ok("fdw_invalid_option_name") - "HV090" -> Ok("fdw_invalid_string_length_or_buffer_length") - "HV00A" -> Ok("fdw_invalid_string_format") - "HV009" -> Ok("fdw_invalid_use_of_null_pointer") - "HV014" -> Ok("fdw_too_many_handles") - "HV001" -> Ok("fdw_out_of_memory") - "HV00P" -> Ok("fdw_no_schemas") - "HV00J" -> Ok("fdw_option_name_not_found") - "HV00K" -> Ok("fdw_reply_handle") - "HV00Q" -> Ok("fdw_schema_not_found") - "HV00R" -> Ok("fdw_table_not_found") - "HV00L" -> Ok("fdw_unable_to_create_execution") - "HV00M" -> Ok("fdw_unable_to_create_reply") - "HV00N" -> Ok("fdw_unable_to_establish_connection") - "P0000" -> Ok("plpgsql_error") - "P0001" -> Ok("raise_exception") - "P0002" -> Ok("no_data_found") - "P0003" -> Ok("too_many_rows") - "P0004" -> Ok("assert_failure") - "XX000" -> Ok("internal_error") - "XX001" -> Ok("data_corrupted") - "XX002" -> Ok("index_corrupted") - _ -> Error(Nil) - } -} diff --git a/packages/pgo/src/gleam_pgo_ffi.erl b/packages/pgo/src/gleam_pgo_ffi.erl deleted file mode 100644 index 4d88c80..0000000 --- a/packages/pgo/src/gleam_pgo_ffi.erl +++ /dev/null @@ -1,105 +0,0 @@ --module(gleam_pgo_ffi). - --export([query/3, connect/1, disconnect/1, coerce/1, null/0]). - --record(pgo_pool, {name, pid}). - --include_lib("gleam_pgo/include/gleam@pgo_Config.hrl"). --include_lib("pg_types/include/pg_types.hrl"). - -null() -> - null. - -coerce(Value) -> - Value. - -connect(Config) -> - Id = integer_to_list(erlang:unique_integer([positive])), - PoolName = list_to_atom("gleam_pgo_pool_" ++ Id), - #config{ - host = Host, - port = Port, - database = Database, - user = User, - password = Password, - ssl = Ssl, - connection_parameters = ConnectionParameters, - pool_size = PoolSize, - queue_target = QueueTarget, - queue_interval = QueueInterval, - idle_interval = IdleInterval, - trace = Trace, - ip_version = IpVersion - } = Config, - SslOptions = case Ssl of - false -> []; - true -> [ - {verify, verify_peer}, - {cacerts, public_key:cacerts_get()}, - {server_name_indication, binary_to_list(Host)}, - {customize_hostname_check, [ - {match_fun, public_key:pkix_verify_hostname_match_fun(https)} - ]} - ] - end, - Options1 = #{ - host => Host, - port => Port, - database => Database, - user => User, - ssl => Ssl, - ssl_options => SslOptions, - connection_parameters => ConnectionParameters, - pool_size => PoolSize, - queue_target => QueueTarget, - queue_interval => QueueInterval, - idle_interval => IdleInterval, - trace => Trace, - socket_options => case IpVersion of - ipv4 -> []; - ipv6 -> [inet6] - end - }, - Options2 = case Password of - {some, Pw} -> maps:put(password, Pw, Options1); - none -> Options1 - end, - {ok, Pid} = pgo_pool:start_link(PoolName, Options2), - #pgo_pool{name = PoolName, pid = Pid}. - -disconnect(#pgo_pool{pid = Pid}) -> - erlang:exit(Pid, normal), - nil. - -query(#pgo_pool{name = Name}, Sql, Arguments) -> - case pgo:query(Sql, Arguments, #{pool => Name}) of - #{rows := Rows, num_rows := NumRows} -> - {ok, {NumRows, Rows}}; - - {error, Error} -> - {error, convert_error(Error)} - end. - -convert_error(none_available) -> - connection_unavailable; -convert_error({pgo_protocol, {parameters, Expected, Got}}) -> - {unexpected_argument_count, Expected, Got}; -convert_error({pgsql_error, #{ - message := Message, - constraint := Constraint, - detail := Detail -}}) -> - {constraint_violated, Message, Constraint, Detail}; -convert_error({pgsql_error, #{code := Code, message := Message}}) -> - Constant = case gleam@pgo:error_code_name(Code) of - {ok, X} -> X; - {error, nil} -> <<"unknown">> - end, - {postgresql_error, Code, Constant, Message}; -convert_error(#{ - error := badarg_encoding, - type_info := #type_info{name = Expected}, - value := Value -}) -> - Got = list_to_binary(io_lib:format("~p", [Value])), - {unexpected_argument_type, Expected, Got}. diff --git a/packages/pgo/test/README.md b/packages/pgo/test/README.md deleted file mode 100644 index 683d62d..0000000 --- a/packages/pgo/test/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Test - -### Running tests - -Initialize a Postgres Database, and use as environment variables: - -```sh -export PGUSER="postgres" -export PGHOST="localhost" -export PGPASSWORD="postgres" -export PGUSER="postgres" -export PGPORT=5432 -``` - -You can then run `reset_db.sh` and `gleam test`. diff --git a/packages/pgo/test/gleam/pgo_test.gleam b/packages/pgo/test/gleam/pgo_test.gleam deleted file mode 100644 index 8bb59d1..0000000 --- a/packages/pgo/test/gleam/pgo_test.gleam +++ /dev/null @@ -1,376 +0,0 @@ -import gleam/dynamic.{type Decoder} -import gleam/option.{None, Some} -import gleam/pgo -import gleam/string -import gleeunit/should - -pub fn url_config_everything_test() { - pgo.url_config("postgres://u:p@db.test:1234/my_db") - |> should.equal(Ok( - pgo.Config( - ..pgo.default_config(), - host: "db.test", - port: 1234, - database: "my_db", - user: "u", - password: Some("p"), - ), - )) -} - -pub fn url_config_not_postgres_protocol_test() { - pgo.url_config("foo://u:p@db.test:1234/my_db") - |> should.equal(Error(Nil)) -} - -pub fn url_config_no_password_test() { - pgo.url_config("postgres://u@db.test:1234/my_db") - |> should.equal(Ok( - pgo.Config( - ..pgo.default_config(), - host: "db.test", - port: 1234, - database: "my_db", - user: "u", - password: None, - ), - )) -} - -pub fn url_config_path_slash_test() { - pgo.url_config("postgres://u:p@db.test:1234/my_db/foo") - |> should.equal(Error(Nil)) -} - -fn start_default() { - pgo.Config( - ..pgo.default_config(), - database: "gleam_pgo_test", - password: Some("postgres"), - pool_size: 1, - ) - |> pgo.connect -} - -pub fn inserting_new_rows_test() { - let db = start_default() - let sql = - " - INSERT INTO - cats - VALUES - (DEFAULT, 'bill', true, ARRAY ['black']), (DEFAULT, 'felix', false, ARRAY ['grey'])" - let assert Ok(returned) = pgo.execute(sql, db, [], dynamic.dynamic) - - returned.count - |> should.equal(2) - returned.rows - |> should.equal([]) - - pgo.disconnect(db) -} - -pub fn inserting_new_rows_and_returning_test() { - let db = start_default() - let sql = - " - INSERT INTO - cats - VALUES - (DEFAULT, 'bill', true, ARRAY ['black']), (DEFAULT, 'felix', false, ARRAY ['grey']) - RETURNING - name" - let assert Ok(returned) = - pgo.execute(sql, db, [], dynamic.element(0, dynamic.string)) - - returned.count - |> should.equal(2) - returned.rows - |> should.equal(["bill", "felix"]) - - pgo.disconnect(db) -} - -pub fn selecting_rows_test() { - let db = start_default() - let sql = - " - INSERT INTO - cats - VALUES - (DEFAULT, 'neo', true, ARRAY ['black']) - RETURNING - id" - - let assert Ok(pgo.Returned(rows: [id], ..)) = - pgo.execute(sql, db, [], dynamic.element(0, dynamic.int)) - - let assert Ok(returned) = - pgo.execute( - "SELECT * FROM cats WHERE id = $1", - db, - [pgo.int(id)], - dynamic.tuple4( - dynamic.int, - dynamic.string, - dynamic.bool, - dynamic.list(dynamic.string), - ), - ) - - returned.count - |> should.equal(1) - returned.rows - |> should.equal([#(id, "neo", True, ["black"])]) - - pgo.disconnect(db) -} - -pub fn invalid_sql_test() { - let db = start_default() - let sql = "select select" - - let assert Error(pgo.PostgresqlError(code, name, message)) = - pgo.execute(sql, db, [], dynamic.dynamic) - - code - |> should.equal("42601") - name - |> should.equal("syntax_error") - message - |> should.equal("syntax error at or near \"select\"") - - pgo.disconnect(db) -} - -pub fn insert_constraint_error_test() { - let db = start_default() - let sql = - " - INSERT INTO - cats - VALUES - (900, 'bill', true, ARRAY ['black']), (900, 'felix', false, ARRAY ['black'])" - - let assert Error(pgo.ConstraintViolated(message, constraint, detail)) = - pgo.execute(sql, db, [], dynamic.dynamic) - - constraint - |> should.equal("cats_pkey") - - detail - |> should.equal("Key (id)=(900) already exists.") - - message - |> should.equal( - "duplicate key value violates unique constraint \"cats_pkey\"", - ) - - pgo.disconnect(db) -} - -pub fn select_from_unknown_table_test() { - let db = start_default() - let sql = "SELECT * FROM unknown" - - let assert Error(pgo.PostgresqlError(code, name, message)) = - pgo.execute(on: db, query: sql, with: [], expecting: dynamic.dynamic) - - code - |> should.equal("42P01") - name - |> should.equal("undefined_table") - message - |> should.equal("relation \"unknown\" does not exist") - - pgo.disconnect(db) -} - -pub fn insert_with_incorrect_type_test() { - let db = start_default() - let sql = - " - INSERT INTO - cats - VALUES - (true, true, true, true)" - let assert Error(pgo.PostgresqlError(code, name, message)) = - pgo.execute(sql, db, [], dynamic.dynamic) - - code - |> should.equal("42804") - name - |> should.equal("datatype_mismatch") - message - |> should.equal( - "column \"id\" is of type integer but expression is of type boolean", - ) - - pgo.disconnect(db) -} - -pub fn execute_with_wrong_number_of_arguments_test() { - let db = start_default() - let sql = "SELECT * FROM cats WHERE id = $1" - - pgo.execute(sql, db, [], dynamic.dynamic) - |> should.equal(Error(pgo.UnexpectedArgumentCount(expected: 1, got: 0))) - - pgo.disconnect(db) -} - -fn assert_roundtrip( - db: pgo.Connection, - value: a, - type_name: String, - encoder: fn(a) -> pgo.Value, - decoder: Decoder(a), -) -> pgo.Connection { - pgo.execute( - string.append("select $1::", type_name), - db, - [encoder(value)], - dynamic.element(0, decoder), - ) - |> should.equal(Ok(pgo.Returned(count: 1, rows: [value]))) - db -} - -pub fn null_test() { - let db = start_default() - pgo.execute( - "select $1", - db, - [pgo.null()], - dynamic.element(0, dynamic.optional(dynamic.int)), - ) - |> should.equal(Ok(pgo.Returned(count: 1, rows: [None]))) - - pgo.disconnect(db) -} - -pub fn bool_test() { - start_default() - |> assert_roundtrip(True, "bool", pgo.bool, dynamic.bool) - |> assert_roundtrip(False, "bool", pgo.bool, dynamic.bool) - |> pgo.disconnect -} - -pub fn int_test() { - start_default() - |> assert_roundtrip(0, "int", pgo.int, dynamic.int) - |> assert_roundtrip(1, "int", pgo.int, dynamic.int) - |> assert_roundtrip(2, "int", pgo.int, dynamic.int) - |> assert_roundtrip(3, "int", pgo.int, dynamic.int) - |> assert_roundtrip(4, "int", pgo.int, dynamic.int) - |> assert_roundtrip(5, "int", pgo.int, dynamic.int) - |> assert_roundtrip(-0, "int", pgo.int, dynamic.int) - |> assert_roundtrip(-1, "int", pgo.int, dynamic.int) - |> assert_roundtrip(-2, "int", pgo.int, dynamic.int) - |> assert_roundtrip(-3, "int", pgo.int, dynamic.int) - |> assert_roundtrip(-4, "int", pgo.int, dynamic.int) - |> assert_roundtrip(-5, "int", pgo.int, dynamic.int) - |> assert_roundtrip(10_000_000, "int", pgo.int, dynamic.int) - |> pgo.disconnect -} - -pub fn float_test() { - start_default() - |> assert_roundtrip(0.123, "float", pgo.float, dynamic.float) - |> assert_roundtrip(1.123, "float", pgo.float, dynamic.float) - |> assert_roundtrip(2.123, "float", pgo.float, dynamic.float) - |> assert_roundtrip(3.123, "float", pgo.float, dynamic.float) - |> assert_roundtrip(4.123, "float", pgo.float, dynamic.float) - |> assert_roundtrip(5.123, "float", pgo.float, dynamic.float) - |> assert_roundtrip(-0.654, "float", pgo.float, dynamic.float) - |> assert_roundtrip(-1.654, "float", pgo.float, dynamic.float) - |> assert_roundtrip(-2.654, "float", pgo.float, dynamic.float) - |> assert_roundtrip(-3.654, "float", pgo.float, dynamic.float) - |> assert_roundtrip(-4.654, "float", pgo.float, dynamic.float) - |> assert_roundtrip(-5.654, "float", pgo.float, dynamic.float) - |> assert_roundtrip(10_000_000.0, "float", pgo.float, dynamic.float) - |> pgo.disconnect -} - -pub fn text_test() { - start_default() - |> assert_roundtrip("", "text", pgo.text, dynamic.string) - |> assert_roundtrip("✨", "text", pgo.text, dynamic.string) - |> assert_roundtrip("Hello, Joe!", "text", pgo.text, dynamic.string) - |> pgo.disconnect -} - -pub fn bytea_test() { - start_default() - |> assert_roundtrip(<<"":utf8>>, "bytea", pgo.bytea, dynamic.bit_array) - |> assert_roundtrip(<<"✨":utf8>>, "bytea", pgo.bytea, dynamic.bit_array) - |> assert_roundtrip( - <<"Hello, Joe!":utf8>>, - "bytea", - pgo.bytea, - dynamic.bit_array, - ) - |> assert_roundtrip(<<1>>, "bytea", pgo.bytea, dynamic.bit_array) - |> assert_roundtrip(<<1, 2, 3>>, "bytea", pgo.bytea, dynamic.bit_array) - |> pgo.disconnect -} - -pub fn array_test() { - let decoder = dynamic.list(dynamic.string) - start_default() - |> assert_roundtrip(["black"], "text[]", pgo.array, decoder) - |> assert_roundtrip(["gray"], "text[]", pgo.array, decoder) - |> assert_roundtrip(["gray", "black"], "text[]", pgo.array, decoder) - |> pgo.disconnect -} - -pub fn nullable_test() { - start_default() - |> assert_roundtrip( - Some("Hello, Joe"), - "text", - pgo.nullable(pgo.text, _), - dynamic.optional(dynamic.string), - ) - |> assert_roundtrip( - None, - "text", - pgo.nullable(pgo.text, _), - dynamic.optional(dynamic.string), - ) - |> assert_roundtrip( - Some(123), - "int", - pgo.nullable(pgo.int, _), - dynamic.optional(dynamic.int), - ) - |> assert_roundtrip( - None, - "int", - pgo.nullable(pgo.int, _), - dynamic.optional(dynamic.int), - ) - |> pgo.disconnect -} - -pub fn expected_argument_type_test() { - let db = start_default() - pgo.execute("select $1::int", db, [pgo.float(1.2)], dynamic.int) - |> should.equal(Error(pgo.UnexpectedArgumentType("int4", "1.2"))) - - pgo.disconnect(db) -} - -pub fn expected_return_type_test() { - let db = start_default() - pgo.execute("select 1", db, [], dynamic.element(0, dynamic.string)) - |> should.equal( - Error( - pgo.UnexpectedResultType([ - dynamic.DecodeError(expected: "String", found: "Int", path: ["0"]), - ]), - ), - ) - - pgo.disconnect(db) -} diff --git a/packages/pgo/test/gleam_pgo_test.gleam b/packages/pgo/test/gleam_pgo_test.gleam deleted file mode 100644 index ecd12ad..0000000 --- a/packages/pgo/test/gleam_pgo_test.gleam +++ /dev/null @@ -1,5 +0,0 @@ -import gleeunit - -pub fn main() { - gleeunit.main() -} diff --git a/packages/pgo/test/reset_db.sh b/packages/pgo/test/reset_db.sh deleted file mode 100755 index 1c7c389..0000000 --- a/packages/pgo/test/reset_db.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -set -eu - -echo -echo Resetting database - -psql <. + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/packages/lustre/examples/99-full-stack-applications/shared/gleam.toml b/packages/sketch_magic/gleam.toml similarity index 69% rename from packages/lustre/examples/99-full-stack-applications/shared/gleam.toml rename to packages/sketch_magic/gleam.toml index 8478ab1..382fbb2 100644 --- a/packages/lustre/examples/99-full-stack-applications/shared/gleam.toml +++ b/packages/sketch_magic/gleam.toml @@ -1,4 +1,4 @@ -name = "shared" +name = "sketch_magic" version = "1.0.0" # Fill out these fields if you intend to generate HTML documentation or publish @@ -6,14 +6,16 @@ version = "1.0.0" # # description = "" # licences = ["Apache-2.0"] -# repository = { type = "github", user = "username", repo = "project" } -# links = [{ title = "Website", href = "https://gleam.run" }] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] # # For a full reference of all the available options, you can have a look at # https://gleam.run/writing-gleam/gleam-toml/. [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" +lustre = ">= 4.6.1 and < 5.0.0" +sketch = ">= 3.1.1 and < 4.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/packages/sketch_magic/manifest.toml b/packages/sketch_magic/manifest.toml new file mode 100644 index 0000000..7546c08 --- /dev/null +++ b/packages/sketch_magic/manifest.toml @@ -0,0 +1,18 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_erlang", version = "0.28.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "BE551521F708DCE5CB954AFBBDF08519C1C44986521FD40753608825F48FFA9E" }, + { name = "gleam_json", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB10B0E7BF44282FB25162F1A24C1A025F6B93E777CCF238C4017E4EEF2CDE97" }, + { name = "gleam_otp", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "F7EA44C81679B2A6032FF1B3851F0EFB43F4F1F1CAC616CB7F4A18497F39FAB9" }, + { name = "gleam_stdlib", version = "0.41.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1B2F80CB1B66B027E3198A2FF71EF3F2F31DF89ED97AD606F25FD387A4C3C1EF" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "lustre", version = "4.6.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "486C3CFBD126939CAD2CA8B92A979A2DAADA5BABAA62BF2B163CD21E257BD4A1" }, + { name = "sketch", version = "3.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "sketch", source = "hex", outer_checksum = "6CBFAAA92C37F1F44FC552FD9E9DAC34598BDEB5F873B6191C696DC67D85AD00" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 4.6.1 and < 5.0.0" } +sketch = { version = ">= 3.1.1 and < 4.0.0" } diff --git a/packages/sketch_magic/package.json b/packages/sketch_magic/package.json new file mode 100644 index 0000000..350ffea --- /dev/null +++ b/packages/sketch_magic/package.json @@ -0,0 +1,10 @@ +{ + "name": "sketch_magic", + "packageManager": "yarn@4.3.1", + "scripts": { + "clean": "gleam clean" + }, + "devDependencies": { + "@chouqueth/gleam": "^1.6.2" + } +} diff --git a/packages/sketch_magic/src/sketch/internals/ffi.gleam b/packages/sketch_magic/src/sketch/internals/ffi.gleam new file mode 100644 index 0000000..a984848 --- /dev/null +++ b/packages/sketch_magic/src/sketch/internals/ffi.gleam @@ -0,0 +1,27 @@ +import gleam/dynamic.{type Dynamic} +import sketch + +@external(javascript, "../../sketch_magic.ffi.mjs", "setCache") +pub fn set_cache(cache: sketch.Cache) -> sketch.Cache { + cache +} + +@external(javascript, "../../sketch_magic.ffi.mjs", "getCache") +pub fn get_cache() -> Result(sketch.Cache, Nil) { + Error(Nil) +} + +@external(javascript, "../../sketch_magic.ffi.mjs", "createCssStyleSheet") +pub fn create_document_stylesheet() -> Dynamic { + dynamic.from(Nil) +} + +@external(javascript, "../../sketch_magic.ffi.mjs", "createCssStyleSheet") +pub fn create_shadow_root_stylesheet(_root: Dynamic) -> Dynamic { + dynamic.from(Nil) +} + +@external(javascript, "../../sketch_magic.ffi.mjs", "setStylesheet") +pub fn set_stylesheet(_content: String, _stylesheet: Dynamic) -> Nil { + Nil +} diff --git a/packages/sketch_magic/src/sketch/magic.gleam b/packages/sketch_magic/src/sketch/magic.gleam new file mode 100644 index 0000000..66a1138 --- /dev/null +++ b/packages/sketch_magic/src/sketch/magic.gleam @@ -0,0 +1,69 @@ +import gleam/dynamic.{type Dynamic} +import gleam/list +import lustre/element as el +import lustre/element/html +import sketch.{type Cache} +import sketch/internals/ffi + +pub type StyleSheet { + CssStyleSheet(stylesheet: Dynamic) + NodeStyleSheet +} + +pub fn setup(cache: Cache) { + ffi.set_cache(cache) + Ok(cache) +} + +/// Wrap the view function in lustre. This should have no impact on your app. +pub fn render(stylesheets: List(StyleSheet), view: fn() -> el.Element(msg)) { + let new_view = view() + let assert Ok(cache) = ffi.get_cache() + let content = sketch.render(cache) + use view, stylesheet <- list.fold(stylesheets, new_view) + render_stylesheet(content, stylesheet, view) +} + +fn render_stylesheet(content, stylesheet, view) { + case stylesheet { + CssStyleSheet(stylesheet:) -> render_css(content, stylesheet, view) + NodeStyleSheet -> render_node(content, view) + } +} + +fn render_css(content, stylesheet, view) { + ffi.set_stylesheet(content, stylesheet) + view +} + +fn render_node(content, view) { + let style = html.style([], content) + el.fragment([style, view]) +} + +/// Generate a class name from a `Class`, using the provided `Cache` in the +/// environment. +pub fn class_name(class: sketch.Class) { + let assert Ok(cache) = ffi.get_cache() + let #(cache, class_name) = sketch.class_name(class, cache) + ffi.set_cache(cache) + class_name +} + +/// Output the StyleSheet in a `CSSStyleSheet` in `document`. +/// `document` cannot be used on server. +pub fn document() -> StyleSheet { + let stylesheet = ffi.create_document_stylesheet() + CssStyleSheet(stylesheet:) +} + +/// Output the StyleSheet in a `CSSStyleSheet` in a shadow root. +/// `shadow` cannot be used on server. +pub fn shadow(root: Dynamic) -> StyleSheet { + let stylesheet = ffi.create_shadow_root_stylesheet(root) + CssStyleSheet(stylesheet:) +} + +pub fn node() -> StyleSheet { + NodeStyleSheet +} diff --git a/packages/sketch_magic/src/sketch/magic/element.gleam b/packages/sketch_magic/src/sketch/magic/element.gleam new file mode 100644 index 0000000..3207867 --- /dev/null +++ b/packages/sketch_magic/src/sketch/magic/element.gleam @@ -0,0 +1,53 @@ +import lustre/attribute.{type Attribute} +import lustre/element as el +import sketch +import sketch/magic + +pub const keyed = el.keyed + +pub const fragment = el.fragment + +pub const none = el.none + +pub const text = el.text + +pub const map = el.map + +pub fn element( + tag tag: String, + class class: sketch.Class, + attributes attributes: List(Attribute(msg)), + children children: List(el.Element(msg)), +) { + let class_name = magic.class_name(class) + el.element(tag, [attribute.class(class_name), ..attributes], children) +} + +pub fn element_( + tag tag: String, + attributes attributes: List(Attribute(msg)), + children children: List(el.Element(msg)), +) { + el.element(tag, attributes, children) +} + +pub fn namespaced( + tag tag: String, + namespace namespace: String, + class class: sketch.Class, + attributes attributes: List(Attribute(msg)), + children children: List(el.Element(msg)), +) { + let class_name = magic.class_name(class) + let attributes = [attribute.class(class_name), ..attributes] + el.namespaced(tag, namespace, attributes, children) +} + +pub fn namespaced_( + tag tag: String, + namespace namespace: String, + attributes attributes: List(Attribute(msg)), + children children: List(el.Element(msg)), +) { + el.namespaced(tag, namespace, attributes, children) +} diff --git a/packages/sketch_magic/src/sketch/magic/element/html.gleam b/packages/sketch_magic/src/sketch/magic/element/html.gleam new file mode 100644 index 0000000..846a98a --- /dev/null +++ b/packages/sketch_magic/src/sketch/magic/element/html.gleam @@ -0,0 +1,1583 @@ +import lustre/attribute.{type Attribute} +import lustre/element.{type Element} as _ +import sketch.{type Class} +import sketch/magic/element.{element, element_} + +// + +pub fn html( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("html", attributes, children) +} + +pub fn base(attributes: List(Attribute(b))) -> Element(b) { + element_("base", attributes, []) +} + +pub fn head( + attributes: List(Attribute(c)), + children: List(Element(c)), +) -> Element(c) { + element_("head", attributes, children) +} + +pub fn link(attributes: List(Attribute(d))) -> Element(d) { + element_("link", attributes, []) +} + +pub fn meta(attributes: List(Attribute(e))) -> Element(e) { + element_("meta", attributes, []) +} + +pub fn style(attributes: List(Attribute(f)), child: String) -> Element(f) { + element_("style", attributes, [text(child)]) +} + +pub fn title(attributes: List(Attribute(g)), title: String) -> Element(g) { + element_("title", attributes, [text(title)]) +} + +// + +pub fn text(content: String) -> Element(h) { + element.text(content) +} + +// + +pub fn a( + class: Class, + attributes: List(Attribute(i)), + children: List(Element(i)), +) -> Element(i) { + element("a", class, attributes, children) +} + +pub fn a_( + attributes: List(Attribute(i)), + children: List(Element(i)), +) -> Element(i) { + element_("a", attributes, children) +} + +pub fn abbr( + class: Class, + attributes: List(Attribute(j)), + children: List(Element(j)), +) -> Element(j) { + element("abbr", class, attributes, children) +} + +pub fn abbr_( + attributes: List(Attribute(k)), + children: List(Element(k)), +) -> Element(k) { + element_("abbr", attributes, children) +} + +pub fn address( + class: Class, + attributes: List(Attribute(l)), + children: List(Element(l)), +) -> Element(l) { + element("address", class, attributes, children) +} + +pub fn address_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("address", attributes, children) +} + +pub fn area(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("area", class, attributes, []) +} + +pub fn area_(attributes: List(Attribute(a))) -> Element(a) { + element_("area", attributes, []) +} + +pub fn article( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("article", class, attributes, children) +} + +pub fn article_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("article", attributes, children) +} + +pub fn aside( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("aside", class, attributes, children) +} + +pub fn aside_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("aside", attributes, children) +} + +pub fn audio( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("audio", class, attributes, children) +} + +pub fn audio_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("audio", attributes, children) +} + +pub fn b( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("b", class, attributes, children) +} + +pub fn b_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("b", attributes, children) +} + +pub fn bdi( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("bdi", class, attributes, children) +} + +pub fn bdi_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("bdi", attributes, children) +} + +pub fn bdo( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("bdo", class, attributes, children) +} + +pub fn bdo_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("bdo", attributes, children) +} + +pub fn blockquote( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("blockquote", class, attributes, children) +} + +pub fn blockquote_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("blockquote", attributes, children) +} + +pub fn body( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("body", class, attributes, children) +} + +pub fn body_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("body", attributes, children) +} + +pub fn br(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("br", class, attributes, []) +} + +pub fn br_(attributes: List(Attribute(a))) -> Element(a) { + element_("br", attributes, []) +} + +pub fn button( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("button", class, attributes, children) +} + +pub fn button_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("button", attributes, children) +} + +pub fn canvas( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("canvas", class, attributes, children) +} + +pub fn canvas_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("canvas", attributes, children) +} + +pub fn caption( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("caption", class, attributes, children) +} + +pub fn caption_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("caption", attributes, children) +} + +pub fn cite( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("cite", class, attributes, children) +} + +pub fn cite_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("cite", attributes, children) +} + +pub fn code( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("code", class, attributes, children) +} + +pub fn code_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("code", attributes, children) +} + +pub fn col(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("col", class, attributes, []) +} + +pub fn col_(attributes: List(Attribute(a))) -> Element(a) { + element_("col", attributes, []) +} + +pub fn colgroup( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("colgroup", class, attributes, children) +} + +pub fn colgroup_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("colgroup", attributes, children) +} + +pub fn data( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("data", class, attributes, children) +} + +pub fn data_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("data", attributes, children) +} + +pub fn datalist( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("datalist", class, attributes, children) +} + +pub fn datalist_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("datalist", attributes, children) +} + +pub fn dd( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("dd", class, attributes, children) +} + +pub fn dd_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("dd", attributes, children) +} + +pub fn del( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("del", class, attributes, children) +} + +pub fn del_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("del", attributes, children) +} + +pub fn details( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("details", class, attributes, children) +} + +pub fn details_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("details", attributes, children) +} + +pub fn dfn( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("dfn", class, attributes, children) +} + +pub fn dfn_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("dfn", attributes, children) +} + +pub fn dialog( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("dialog", class, attributes, children) +} + +pub fn dialog_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("dialog", attributes, children) +} + +pub fn div( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("div", class, attributes, children) +} + +pub fn div_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("div", attributes, children) +} + +pub fn dl( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("dl", class, attributes, children) +} + +pub fn dl_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("dl", attributes, children) +} + +pub fn dt( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("dt", class, attributes, children) +} + +pub fn dt_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("dt", attributes, children) +} + +pub fn em( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("em", class, attributes, children) +} + +pub fn em_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("em", attributes, children) +} + +pub fn embed(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("embed", class, attributes, []) +} + +pub fn embed_(attributes: List(Attribute(a))) -> Element(a) { + element_("embed", attributes, []) +} + +pub fn fieldset( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("fieldset", class, attributes, children) +} + +pub fn fieldset_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("fieldset", attributes, children) +} + +pub fn figcaption( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("figcaption", class, attributes, children) +} + +pub fn figcaption_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("figcaption", attributes, children) +} + +pub fn figure( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("figure", class, attributes, children) +} + +pub fn figure_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("figure", attributes, children) +} + +pub fn footer( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("footer", class, attributes, children) +} + +pub fn footer_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("footer", attributes, children) +} + +pub fn form( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("form", class, attributes, children) +} + +pub fn form_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("form", attributes, children) +} + +pub fn h1( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("h1", class, attributes, children) +} + +pub fn h1_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("h1", attributes, children) +} + +pub fn h2( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("h2", class, attributes, children) +} + +pub fn h2_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("h2", attributes, children) +} + +pub fn h3( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("h3", class, attributes, children) +} + +pub fn h3_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("h3", attributes, children) +} + +pub fn h4( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("h4", class, attributes, children) +} + +pub fn h4_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("h4", attributes, children) +} + +pub fn h5( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("h5", class, attributes, children) +} + +pub fn h5_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("h5", attributes, children) +} + +pub fn h6( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("h6", class, attributes, children) +} + +pub fn h6_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("h6", attributes, children) +} + +pub fn header( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("header", class, attributes, children) +} + +pub fn header_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("header", attributes, children) +} + +pub fn hgroup( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("hgroup", class, attributes, children) +} + +pub fn hgroup_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("hgroup", attributes, children) +} + +pub fn hr(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("hr", class, attributes, []) +} + +pub fn hr_(attributes: List(Attribute(a))) -> Element(a) { + element_("hr", attributes, []) +} + +pub fn i( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("i", class, attributes, children) +} + +pub fn i_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("i", attributes, children) +} + +pub fn iframe( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("iframe", class, attributes, children) +} + +pub fn iframe_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("iframe", attributes, children) +} + +pub fn img(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("img", class, attributes, []) +} + +pub fn img_(attributes: List(Attribute(a))) -> Element(a) { + element_("img", attributes, []) +} + +pub fn input(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("input", class, attributes, []) +} + +pub fn input_(attributes: List(Attribute(a))) -> Element(a) { + element_("input", attributes, []) +} + +pub fn ins( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("ins", class, attributes, children) +} + +pub fn ins_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("ins", attributes, children) +} + +pub fn kbd( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("kbd", class, attributes, children) +} + +pub fn kbd_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("kbd", attributes, children) +} + +pub fn label( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("label", class, attributes, children) +} + +pub fn label_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("label", attributes, children) +} + +pub fn legend( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("legend", class, attributes, children) +} + +pub fn legend_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("legend", attributes, children) +} + +pub fn li( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("li", class, attributes, children) +} + +pub fn li_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("li", attributes, children) +} + +pub fn main( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("main", class, attributes, children) +} + +pub fn main_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("main", attributes, children) +} + +pub fn map( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("map", class, attributes, children) +} + +pub fn map_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("map", attributes, children) +} + +pub fn mark( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("mark", class, attributes, children) +} + +pub fn mark_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("mark", attributes, children) +} + +pub fn math( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("math", class, attributes, children) +} + +pub fn math_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("math", attributes, children) +} + +pub fn menu( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("menu", class, attributes, children) +} + +pub fn menu_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("menu", attributes, children) +} + +pub fn meter( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("meter", class, attributes, children) +} + +pub fn meter_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("meter", attributes, children) +} + +pub fn nav( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("nav", class, attributes, children) +} + +pub fn nav_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("nav", attributes, children) +} + +pub fn noscript( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("noscript", class, attributes, children) +} + +pub fn noscript_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("noscript", attributes, children) +} + +pub fn object( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("object", class, attributes, children) +} + +pub fn object_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("object", attributes, children) +} + +pub fn ol( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("ol", class, attributes, children) +} + +pub fn ol_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("ol", attributes, children) +} + +pub fn optgroup( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("optgroup", class, attributes, children) +} + +pub fn optgroup_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("optgroup", attributes, children) +} + +pub fn option( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("option", class, attributes, children) +} + +pub fn option_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("option", attributes, children) +} + +pub fn output( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("output", class, attributes, children) +} + +pub fn output_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("output", attributes, children) +} + +pub fn p( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("p", class, attributes, children) +} + +pub fn p_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("p", attributes, children) +} + +pub fn picture( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("picture", class, attributes, children) +} + +pub fn picture_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("picture", attributes, children) +} + +pub fn portal( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("portal", class, attributes, children) +} + +pub fn portal_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("portal", attributes, children) +} + +pub fn pre( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("pre", class, attributes, children) +} + +pub fn pre_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("pre", attributes, children) +} + +pub fn progress( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("progress", class, attributes, children) +} + +pub fn progress_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("progress", attributes, children) +} + +pub fn q( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("q", class, attributes, children) +} + +pub fn q_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("q", attributes, children) +} + +pub fn rp( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("rp", class, attributes, children) +} + +pub fn rp_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("rp", attributes, children) +} + +pub fn rt( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("rt", class, attributes, children) +} + +pub fn rt_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("rt", attributes, children) +} + +pub fn ruby( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("ruby", class, attributes, children) +} + +pub fn ruby_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("ruby", attributes, children) +} + +pub fn s( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("s", class, attributes, children) +} + +pub fn s_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("s", attributes, children) +} + +pub fn samp( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("samp", class, attributes, children) +} + +pub fn samp_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("samp", attributes, children) +} + +pub fn script( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("script", class, attributes, children) +} + +pub fn script_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("script", attributes, children) +} + +pub fn search( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("search", class, attributes, children) +} + +pub fn search_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("search", attributes, children) +} + +pub fn section( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("section", class, attributes, children) +} + +pub fn section_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("section", attributes, children) +} + +pub fn select( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("select", class, attributes, children) +} + +pub fn select_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("select", attributes, children) +} + +pub fn slot( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("slot", class, attributes, children) +} + +pub fn slot_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("slot", attributes, children) +} + +pub fn small( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("small", class, attributes, children) +} + +pub fn small_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("small", attributes, children) +} + +pub fn source(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("source", class, attributes, []) +} + +pub fn source_(attributes: List(Attribute(a))) -> Element(a) { + element_("source", attributes, []) +} + +pub fn span( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("span", class, attributes, children) +} + +pub fn span_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("span", attributes, children) +} + +pub fn strong( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("strong", class, attributes, children) +} + +pub fn strong_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("strong", attributes, children) +} + +pub fn sub( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("sub", class, attributes, children) +} + +pub fn sub_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("sub", attributes, children) +} + +pub fn summary( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("summary", class, attributes, children) +} + +pub fn summary_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("summary", attributes, children) +} + +pub fn sup( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("sup", class, attributes, children) +} + +pub fn sup_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("sup", attributes, children) +} + +pub fn table( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("table", class, attributes, children) +} + +pub fn table_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("table", attributes, children) +} + +pub fn tbody( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("tbody", class, attributes, children) +} + +pub fn tbody_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("tbody", attributes, children) +} + +pub fn td( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("td", class, attributes, children) +} + +pub fn td_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("td", attributes, children) +} + +pub fn template( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("template", class, attributes, children) +} + +pub fn template_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("template", attributes, children) +} + +pub fn textarea( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("textarea", class, attributes, children) +} + +pub fn textarea_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("textarea", attributes, children) +} + +pub fn tfoot( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("tfoot", class, attributes, children) +} + +pub fn tfoot_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("tfoot", attributes, children) +} + +pub fn th( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("th", class, attributes, children) +} + +pub fn th_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("th", attributes, children) +} + +pub fn thead( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("thead", class, attributes, children) +} + +pub fn thead_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("thead", attributes, children) +} + +pub fn time( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("time", class, attributes, children) +} + +pub fn time_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("time", attributes, children) +} + +pub fn tr( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("tr", class, attributes, children) +} + +pub fn tr_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("tr", attributes, children) +} + +pub fn track(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("track", class, attributes, []) +} + +pub fn track_(attributes: List(Attribute(a))) -> Element(a) { + element_("track", attributes, []) +} + +pub fn u( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("u", class, attributes, children) +} + +pub fn u_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("u", attributes, children) +} + +pub fn ul( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("ul", class, attributes, children) +} + +pub fn ul_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("ul", attributes, children) +} + +pub fn var( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("var", class, attributes, children) +} + +pub fn var_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("var", attributes, children) +} + +pub fn video( + class: Class, + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element("video", class, attributes, children) +} + +pub fn video_( + attributes: List(Attribute(a)), + children: List(Element(a)), +) -> Element(a) { + element_("video", attributes, children) +} + +pub fn wbr(class: Class, attributes: List(Attribute(a))) -> Element(a) { + element("wbr", class, attributes, []) +} + +pub fn wbr_(attributes: List(Attribute(a))) -> Element(a) { + element_("wbr", attributes, []) +} diff --git a/packages/sketch_magic/src/sketch_magic.ffi.mjs b/packages/sketch_magic/src/sketch_magic.ffi.mjs new file mode 100644 index 0000000..8f55178 --- /dev/null +++ b/packages/sketch_magic/src/sketch_magic.ffi.mjs @@ -0,0 +1,28 @@ +import * as gleam from './gleam.mjs' + +let _cache = null + +export function setCache(cache) { + _cache = cache + return cache +} + +export function getCache() { + if (!_cache) return new gleam.Error() + return new gleam.Ok(_cache) +} + +export function createCssStyleSheet(root) { + if (!(root instanceof ShadowRoot)) throw new Error(`root is not a ShadowRoot`) + const stylesheet = new CSSStyleSheet() + if (root && root.adoptedStyleSheets) { + root.adoptedStyleSheets.push(stylesheet) + } else { + document.adoptedStyleSheets.push(stylesheet) + } + return stylesheet +} + +export function setStylesheet(content, stylesheet) { + stylesheet.replaceSync(content) +} diff --git a/packages/sketch_magic/test/sketch_magic_test.gleam b/packages/sketch_magic/test/sketch_magic_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/packages/sketch_magic/test/sketch_magic_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/packages/tom/CHANGELOG.md b/packages/tom/CHANGELOG.md deleted file mode 100644 index 0867008..0000000 --- a/packages/tom/CHANGELOG.md +++ /dev/null @@ -1,22 +0,0 @@ -# Changelog - -## v0.4.0 - Unreleased - -- Added support for `\e`, `\f`, `\b`. - -## v0.3.0 - 2023-12-07 - -- Updated for Gleam v0.33.0. - -## v0.2.1 - 2023-11-20 - -- Documents with no trailing newline can now be parsed. - -## v0.2.0 - 2023-11-14 - -- The library can now parse full TOML documents, with the exception of the - string escape codes `\b`, `\f`, `\e`, `\xHH`, `\uHHHH`, and `\UHHHHHHHH`. - -## v0.1.0 - 2023-11-12 - -- Initial release diff --git a/packages/tom/README.md b/packages/tom/README.md deleted file mode 100644 index cca986a..0000000 --- a/packages/tom/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# tom - -A Gleam TOML parser! - -[![Package Version](https://img.shields.io/hexpm/v/tom)](https://hex.pm/packages/tom) -[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/tom/) - - -```sh -gleam add tom -``` -```gleam -import tom - -const config = " - [person] - name = \"Lucy\" - is_cool = true -" - -pub fn main() { - // Parse a string of TOML - let assert Ok(parsed) = tom.parse(config) - - // Now you can work with the data directly, or you can use the `get_*` - // functions to retrieve values. - - tom.get_string(parsed, ["person", "name"]) - // -> Ok("Lucy") - - let is_cool = tom.get_bool(parsed, ["person", "is_cool"]) - // -> Ok(True) -} -``` - -Further documentation can be found at . - -## Status - -The following string escape sequences are not supported yet: - -- `\xHH` -- `\uHHHH` -- `\UHHHHHHHH` diff --git a/packages/tom/gleam.toml b/packages/tom/gleam.toml deleted file mode 100644 index 3bfe267..0000000 --- a/packages/tom/gleam.toml +++ /dev/null @@ -1,13 +0,0 @@ -name = "tom" -version = "0.3.0" - -description = "A pure Gleam TOML parser!" -licences = ["Apache-2.0"] -repository = { type = "github", user = "lpil", repo = "tom" } -links = [{ title = "TOML website", href = "https://toml.io/en/" }] - -[dependencies] -gleam_stdlib = "~> 0.33" - -[dev-dependencies] -gleeunit = "~> 1.0" diff --git a/packages/tom/manifest.toml b/packages/tom/manifest.toml deleted file mode 100644 index 2a00d86..0000000 --- a/packages/tom/manifest.toml +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "gleam_stdlib", version = "0.33.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "3CEAD7B153D896499C78390B22CC968620C27500C922AED3A5DD7B536F922B25" }, - { name = "gleeunit", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D3682ED8C5F9CAE1C928F2506DE91625588CC752495988CBE0F5653A42A6F334" }, -] - -[requirements] -gleam_stdlib = { version = "~> 0.33" } -gleeunit = { version = "~> 1.0" } diff --git a/packages/tom/src/tom.gleam b/packages/tom/src/tom.gleam deleted file mode 100644 index 3bd7e82..0000000 --- a/packages/tom/src/tom.gleam +++ /dev/null @@ -1,1330 +0,0 @@ -//// A pure Gleam TOML parser! -//// -//// ```gleam -//// import tom -//// -//// const config = " -//// [person] -//// name = \"Lucy\" -//// is_cool = true -//// " -//// -//// pub fn main() { -//// // Parse a string of TOML -//// let assert Ok(parsed) = tom.parse(config) -//// -//// // Now you can work with the data directly, or you can use the `get_*` -//// // functions to retrieve values. -//// -//// tom.get_string(parsed, ["person", "name"]) -//// // -> Ok("Lucy") -//// -//// let is_cool = tom.get_bool(parsed, ["person", "is_cool"]) -//// // -> Ok(True) -//// } -//// ``` - -import gleam/dict.{type Dict} -import gleam/float -import gleam/int -import gleam/list -import gleam/result -import gleam/string - -/// A TOML document. -pub type Toml { - Int(Int) - Float(Float) - /// Infinity is a valid number in TOML but Gleam does not support it, so this - /// variant represents the infinity values. - Infinity(Sign) - /// NaN is a valid number in TOML but Gleam does not support it, so this - /// variant represents the NaN values. - Nan(Sign) - Bool(Bool) - String(String) - Date(Date) - Time(Time) - DateTime(DateTime) - Array(List(Toml)) - ArrayOfTables(List(Dict(String, Toml))) - Table(Dict(String, Toml)) - InlineTable(Dict(String, Toml)) -} - -pub type DateTime { - DateTimeValue(date: Date, time: Time, offset: Offset) -} - -pub type Date { - DateValue(year: Int, month: Int, day: Int) -} - -pub type Time { - TimeValue(hour: Int, minute: Int, second: Int, millisecond: Int) -} - -pub type Offset { - Local - Offset(direction: Sign, hours: Int, minutes: Int) -} - -pub type Sign { - Positive - Negative -} - -/// An error that can occur when parsing a TOML document. -pub type ParseError { - /// An unexpected character was encountered when parsing the document. - Unexpected(got: String, expected: String) - /// More than one items have the same key in the document. - KeyAlreadyInUse(key: List(String)) -} - -type Tokens = - List(String) - -type Parsed(a) = - Result(#(a, Tokens), ParseError) - -/// A number of any kind, returned by the `get_number` function. -pub type Number { - NumberInt(Int) - NumberFloat(Float) - NumberInfinity(Sign) - NumberNan(Sign) -} - -/// An error that can occur when retrieving a value from a TOML document with -/// one of the `get_*` functions. -pub type GetError { - /// There was no value at the given key. - NotFound(key: List(String)) - /// The value at the given key was not of the expected type. - WrongType(key: List(String), expected: String, got: String) -} - -// TODO: test -/// Get a value of any type from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = 1") -/// get(parsed, ["a", "b", "c"]) -/// // -> Ok(Int(1)) -/// ``` -/// -pub fn get( - toml: Dict(String, Toml), - key: List(String), -) -> Result(Toml, GetError) { - case key { - [] -> Error(NotFound([])) - [k] -> result.replace_error(dict.get(toml, k), NotFound([k])) - [k, ..key] -> { - case dict.get(toml, k) { - Ok(Table(t)) -> push_key(get(t, key), k) - Ok(InlineTable(t)) -> push_key(get(t, key), k) - Ok(other) -> Error(WrongType([k], "Table", classify(other))) - Error(_) -> Error(NotFound([k])) - } - } - } -} - -// TODO: test -/// Get an int from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = 1") -/// get_int(parsed, ["a", "b", "c"]) -/// // -> Ok(1) -/// ``` -/// -pub fn get_int( - toml: Dict(String, Toml), - key: List(String), -) -> Result(Int, GetError) { - case get(toml, key) { - Ok(Int(i)) -> Ok(i) - Ok(other) -> Error(WrongType(key, "Int", classify(other))) - Error(e) -> Error(e) - } -} - -// TODO: test -/// Get a float from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = 1.1") -/// get_float(parsed, ["a", "b", "c"]) -/// // -> Ok(1.1) -/// ``` -/// -pub fn get_float( - toml: Dict(String, Toml), - key: List(String), -) -> Result(Float, GetError) { - case get(toml, key) { - Ok(Float(i)) -> Ok(i) - Ok(other) -> Error(WrongType(key, "Float", classify(other))) - Error(e) -> Error(e) - } -} - -// TODO: test -/// Get a bool from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = true") -/// get_bool(parsed, ["a", "b", "c"]) -/// // -> Ok(True) -/// ``` -/// -pub fn get_bool( - toml: Dict(String, Toml), - key: List(String), -) -> Result(Bool, GetError) { - case get(toml, key) { - Ok(Bool(i)) -> Ok(i) - Ok(other) -> Error(WrongType(key, "Bool", classify(other))) - Error(e) -> Error(e) - } -} - -// TODO: test -/// Get a string from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = \"ok\"") -/// get_string(parsed, ["a", "b", "c"]) -/// // -> Ok("ok") -/// ``` -/// -pub fn get_string( - toml: Dict(String, Toml), - key: List(String), -) -> Result(String, GetError) { - case get(toml, key) { - Ok(String(i)) -> Ok(i) - Ok(other) -> Error(WrongType(key, "String", classify(other))) - Error(e) -> Error(e) - } -} - -// TODO: test -/// Get a date from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = 1979-05-27") -/// get_date(parsed, ["a", "b", "c"]) -/// // -> Ok("1979-05-27") -/// ``` -/// -pub fn get_date( - toml: Dict(String, Toml), - key: List(String), -) -> Result(Date, GetError) { - case get(toml, key) { - Ok(Date(i)) -> Ok(i) - Ok(other) -> Error(WrongType(key, "Date", classify(other))) - Error(e) -> Error(e) - } -} - -// TODO: test -/// Get a time from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = 07:32:00") -/// get_time(parsed, ["a", "b", "c"]) -/// // -> Ok("07:32:00") -/// ``` -/// -pub fn get_time( - toml: Dict(String, Toml), - key: List(String), -) -> Result(Time, GetError) { - case get(toml, key) { - Ok(Time(i)) -> Ok(i) - Ok(other) -> Error(WrongType(key, "Time", classify(other))) - Error(e) -> Error(e) - } -} - -// TODO: test -/// Get a date-time from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = 1979-05-27T07:32:00") -/// get_date_time(parsed, ["a", "b", "c"]) -/// // -> Ok("1979-05-27T07:32:00") -/// ``` -/// -pub fn get_date_time( - toml: Dict(String, Toml), - key: List(String), -) -> Result(DateTime, GetError) { - case get(toml, key) { - Ok(DateTime(i)) -> Ok(i) - Ok(other) -> Error(WrongType(key, "DateTime", classify(other))) - Error(e) -> Error(e) - } -} - -// TODO: test -/// Get an array from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = [1, 2]") -/// get_array(parsed, ["a", "b", "c"]) -/// // -> Ok([Int(1), Int(2)]) -/// ``` -/// -pub fn get_array( - toml: Dict(String, Toml), - key: List(String), -) -> Result(List(Toml), GetError) { - case get(toml, key) { - Ok(Array(i)) -> Ok(i) - Ok(ArrayOfTables(i)) -> Ok(list.map(i, Table)) - Ok(other) -> Error(WrongType(key, "Array", classify(other))) - Error(e) -> Error(e) - } -} - -// TODO: test -/// Get a table from a TOML document. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = { d = 1 }") -/// get_table(parsed, ["a", "b", "c"]) -/// // -> Ok(dict.from_list([#("d", Int(1))])) -/// ``` -/// -pub fn get_table( - toml: Dict(String, Toml), - key: List(String), -) -> Result(Dict(String, Toml), GetError) { - case get(toml, key) { - Ok(Table(i)) -> Ok(i) - Ok(InlineTable(i)) -> Ok(i) - Ok(other) -> Error(WrongType(key, "Table", classify(other))) - Error(e) -> Error(e) - } -} - -// TODO: test -/// Get a number of any kind from a TOML document. -/// This could be an int, a float, a NaN, or an infinity. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(parsed) = parse("a.b.c = { d = inf }") -/// get_number(parsed, ["a", "b", "c"]) -/// // -> Ok(NumberInfinity(Positive))) -/// ``` -/// -pub fn get_number( - toml: Dict(String, Toml), - key: List(String), -) -> Result(Number, GetError) { - case get(toml, key) { - Ok(Int(x)) -> Ok(NumberInt(x)) - Ok(Float(x)) -> Ok(NumberFloat(x)) - Ok(Nan(x)) -> Ok(NumberNan(x)) - Ok(Infinity(x)) -> Ok(NumberInfinity(x)) - Ok(other) -> Error(WrongType(key, "Number", classify(other))) - Error(e) -> Error(e) - } -} - -fn classify(toml: Toml) -> String { - case toml { - Int(_) -> "Int" - Float(_) -> "Float" - Nan(Positive) -> "NaN" - Nan(Negative) -> "Negative NaN" - Infinity(Positive) -> "Infinity" - Infinity(Negative) -> "Negative Infinity" - Bool(_) -> "Bool" - String(_) -> "String" - Date(_) -> "Date" - Time(_) -> "Time" - DateTime(_) -> "DateTime" - Array(_) -> "Array" - ArrayOfTables(_) -> "Array" - Table(_) -> "Table" - InlineTable(_) -> "Table" - } -} - -fn push_key(result: Result(t, GetError), key: String) -> Result(t, GetError) { - case result { - Ok(t) -> Ok(t) - Error(NotFound(path)) -> Error(NotFound([key, ..path])) - Error(WrongType(path, expected, got)) -> - Error(WrongType([key, ..path], expected, got)) - } -} - -pub fn parse(input: String) -> Result(Dict(String, Toml), ParseError) { - let input = string.to_graphemes(input) - let input = drop_comments(input, [], False) - let input = skip_whitespace(input) - use toml, input <- do(parse_table(input, dict.new())) - case parse_tables(input, toml) { - Ok(toml) -> Ok(reverse_arrays_of_tables_table(toml)) - Error(e) -> Error(e) - } -} - -fn parse_tables( - input: Tokens, - toml: Dict(String, Toml), -) -> Result(Dict(String, Toml), ParseError) { - case input { - ["[", "[", ..input] -> { - case parse_array_of_tables(input) { - Error(e) -> Error(e) - Ok(#(#(key, table), input)) -> { - case insert(toml, key, ArrayOfTables([table])) { - Ok(toml) -> parse_tables(input, toml) - Error(e) -> Error(e) - } - } - } - } - ["[", ..input] -> { - case parse_table_and_header(input) { - Error(e) -> Error(e) - Ok(#(#(key, table), input)) -> { - case insert(toml, key, Table(table)) { - Ok(toml) -> parse_tables(input, toml) - Error(e) -> Error(e) - } - } - } - } - [g, ..] -> Error(Unexpected(g, "[")) - [] -> Ok(toml) - } -} - -fn parse_array_of_tables( - input: Tokens, -) -> Parsed(#(List(String), Dict(String, Toml))) { - let input = skip_line_whitespace(input) - use key, input <- do(parse_key(input, [])) - use input <- expect(input, "]") - use input <- expect(input, "]") - use table, input <- do(parse_table(input, dict.new())) - Ok(#(#(key, table), input)) -} - -fn parse_table_header(input: Tokens) -> Parsed(List(String)) { - let input = skip_line_whitespace(input) - use key, input <- do(parse_key(input, [])) - use input <- expect(input, "]") - let input = skip_line_whitespace(input) - use input <- expect_end_of_line(input) - Ok(#(key, input)) -} - -fn parse_table_and_header( - input: Tokens, -) -> Parsed(#(List(String), Dict(String, Toml))) { - use key, input <- do(parse_table_header(input)) - use table, input <- do(parse_table(input, dict.new())) - Ok(#(#(key, table), input)) -} - -fn parse_table( - input: Tokens, - toml: Dict(String, Toml), -) -> Parsed(Dict(String, Toml)) { - let input = skip_whitespace(input) - case input { - ["[", ..] | [] -> Ok(#(toml, input)) - _ -> - case parse_key_value(input, toml) { - Ok(#(toml, input)) -> - case skip_line_whitespace(input) { - [] -> Ok(#(toml, [])) - ["\n", ..in] | ["\r\n", ..in] -> parse_table(in, toml) - [g, ..] -> Error(Unexpected(g, "\n")) - } - e -> e - } - } -} - -fn parse_key_value( - input: Tokens, - toml: Dict(String, Toml), -) -> Parsed(Dict(String, Toml)) { - use key, input <- do(parse_key(input, [])) - let input = skip_line_whitespace(input) - use input <- expect(input, "=") - let input = skip_line_whitespace(input) - use value, input <- do(parse_value(input)) - case insert(toml, key, value) { - Ok(toml) -> Ok(#(toml, input)) - Error(e) -> Error(e) - } -} - -fn insert( - table: Dict(String, Toml), - key: List(String), - value: Toml, -) -> Result(Dict(String, Toml), ParseError) { - case insert_loop(table, key, value) { - Ok(table) -> Ok(table) - Error(path) -> Error(KeyAlreadyInUse(path)) - } -} - -fn insert_loop( - table: Dict(String, Toml), - key: List(String), - value: Toml, -) -> Result(Dict(String, Toml), List(String)) { - case key { - [] -> panic as "unreachable" - [k] -> { - case dict.get(table, k) { - Error(Nil) -> Ok(dict.insert(table, k, value)) - Ok(old) -> merge(table, k, old, value) - } - } - [k, ..key] -> { - case dict.get(table, k) { - Error(Nil) -> { - case insert_loop(dict.new(), key, value) { - Ok(inner) -> Ok(dict.insert(table, k, Table(inner))) - Error(path) -> Error([k, ..path]) - } - } - Ok(ArrayOfTables([inner, ..rest])) -> { - case insert_loop(inner, key, value) { - Ok(inner) -> - Ok(dict.insert(table, k, ArrayOfTables([inner, ..rest]))) - Error(path) -> Error([k, ..path]) - } - } - Ok(Table(inner)) -> { - case insert_loop(inner, key, value) { - Ok(inner) -> Ok(dict.insert(table, k, Table(inner))) - Error(path) -> Error([k, ..path]) - } - } - Ok(_) -> Error([k]) - } - } - } -} - -fn merge( - table: Dict(String, Toml), - key: String, - old: Toml, - new: Toml, -) -> Result(Dict(String, Toml), List(String)) { - case old, new { - // When both are arrays of tables then they are merged together - ArrayOfTables(tables), ArrayOfTables(new) -> - Ok(dict.insert(table, key, ArrayOfTables(list.append(new, tables)))) - - _, _ -> Error([key]) - } -} - -fn expect_end_of_line(input: Tokens, next: fn(Tokens) -> Parsed(a)) -> Parsed(a) { - case input { - ["\n", ..input] -> next(input) - ["\r\n", ..input] -> next(input) - [g, ..] -> Error(Unexpected(g, "\n")) - [] -> Error(Unexpected("EOF", "\n")) - } -} - -fn parse_value(input) -> Parsed(Toml) { - case input { - ["t", "r", "u", "e", ..input] -> Ok(#(Bool(True), input)) - ["f", "a", "l", "s", "e", ..input] -> Ok(#(Bool(False), input)) - - ["n", "a", "n", ..input] -> Ok(#(Nan(Positive), input)) - ["+", "n", "a", "n", ..input] -> Ok(#(Nan(Positive), input)) - ["-", "n", "a", "n", ..input] -> Ok(#(Nan(Negative), input)) - - ["i", "n", "f", ..input] -> Ok(#(Infinity(Positive), input)) - ["+", "i", "n", "f", ..input] -> Ok(#(Infinity(Positive), input)) - ["-", "i", "n", "f", ..input] -> Ok(#(Infinity(Negative), input)) - - ["[", ..input] -> parse_array(input, []) - ["{", ..input] -> parse_inline_table(input, dict.new()) - - ["0", "x", ..input] -> parse_hex(input, 0, Positive) - ["+", "0", "x", ..input] -> parse_hex(input, 0, Positive) - ["-", "0", "x", ..input] -> parse_hex(input, 0, Negative) - - ["0", "o", ..input] -> parse_octal(input, 0, Positive) - ["+", "0", "o", ..input] -> parse_octal(input, 0, Positive) - ["-", "0", "o", ..input] -> parse_octal(input, 0, Negative) - - ["0", "b", ..input] -> parse_binary(input, 0, Positive) - ["+", "0", "b", ..input] -> parse_binary(input, 0, Positive) - ["-", "0", "b", ..input] -> parse_binary(input, 0, Negative) - - ["+", ..input] -> parse_number(input, 0, Positive) - ["-", ..input] -> parse_number(input, 0, Negative) - ["0", ..] - | ["1", ..] - | ["2", ..] - | ["3", ..] - | ["4", ..] - | ["5", ..] - | ["6", ..] - | ["7", ..] - | ["8", ..] - | ["9", ..] -> parse_number(input, 0, Positive) - - ["\"", "\"", "\"", ..input] -> parse_multi_line_string(input, "") - ["\"", ..input] -> parse_string(input, "") - - ["'", "'", "'", ..input] -> parse_multi_line_literal_string(input, "") - ["'", ..input] -> parse_literal_string(input, "") - - [g, ..] -> Error(Unexpected(g, "value")) - [] -> Error(Unexpected("EOF", "value")) - } -} - -fn parse_key(input: Tokens, segments: List(String)) -> Parsed(List(String)) { - use segment, input <- do(parse_key_segment(input)) - let segments = [segment, ..segments] - let input = skip_line_whitespace(input) - - case input { - [".", ..input] -> parse_key(input, segments) - _ -> Ok(#(list.reverse(segments), input)) - } -} - -fn parse_key_segment(input: Tokens) -> Parsed(String) { - let input = skip_line_whitespace(input) - case input { - ["=", ..] -> Error(Unexpected("=", "Key")) - ["\n", ..] -> Error(Unexpected("\n", "Key")) - ["\r\n", ..] -> Error(Unexpected("\r\n", "Key")) - ["[", ..] -> Error(Unexpected("[", "Key")) - ["\"", ..input] -> parse_key_quoted(input, "\"", "") - ["'", ..input] -> parse_key_quoted(input, "'", "") - _ -> parse_key_bare(input, "") - } -} - -fn parse_key_quoted( - input: Tokens, - close: String, - name: String, -) -> Parsed(String) { - case input { - [g, ..input] if g == close -> Ok(#(name, input)) - [g, ..input] -> parse_key_quoted(input, close, name <> g) - [] -> Error(Unexpected("EOF", close)) - } -} - -fn parse_key_bare(input: Tokens, name: String) -> Parsed(String) { - case input { - [" ", ..input] if name != "" -> Ok(#(name, input)) - ["=", ..] if name != "" -> Ok(#(name, input)) - [".", ..] if name != "" -> Ok(#(name, input)) - ["]", ..] if name != "" -> Ok(#(name, input)) - [",", ..] if name != "" -> Error(Unexpected(",", "=")) - ["\n", ..] if name != "" -> Error(Unexpected("\n", "=")) - ["\r\n", ..] if name != "" -> Error(Unexpected("\r\n", "=")) - ["\n", ..] -> Error(Unexpected("\n", "key")) - ["\r\n", ..] -> Error(Unexpected("\r\n", "key")) - ["]", ..] -> Error(Unexpected("]", "key")) - [",", ..] -> Error(Unexpected(",", "key")) - [g, ..input] -> parse_key_bare(input, name <> g) - [] -> Error(Unexpected("EOF", "key")) - } -} - -fn skip_line_whitespace(input: Tokens) -> Tokens { - list.drop_while(input, fn(g) { g == " " || g == "\t" }) -} - -fn skip_whitespace(input: Tokens) -> Tokens { - case input { - [" ", ..input] -> skip_whitespace(input) - ["\t", ..input] -> skip_whitespace(input) - ["\n", ..input] -> skip_whitespace(input) - ["\r\n", ..input] -> skip_whitespace(input) - input -> input - } -} - -fn drop_comments(input: Tokens, acc: Tokens, in_string: Bool) -> Tokens { - case input { - ["\\", "\"", ..input] if in_string -> - drop_comments(input, ["\"", "\\", ..acc], in_string) - ["\"", ..input] -> drop_comments(input, ["\"", ..acc], !in_string) - ["#", ..input] if in_string -> drop_comments(input, ["#", ..acc], in_string) - ["#", ..input] if !in_string -> - input - |> list.drop_while(fn(g) { g != "\n" }) - |> drop_comments(acc, in_string) - [g, ..input] -> drop_comments(input, [g, ..acc], in_string) - [] -> list.reverse(acc) - } -} - -fn do( - result: Result(#(a, Tokens), ParseError), - next: fn(a, Tokens) -> Result(b, ParseError), -) -> Result(b, ParseError) { - case result { - Ok(#(a, input)) -> next(a, input) - Error(e) -> Error(e) - } -} - -fn expect( - input: Tokens, - expected: String, - next: fn(Tokens) -> Parsed(a), -) -> Parsed(a) { - case input { - [g, ..input] if g == expected -> next(input) - [g, ..] -> Error(Unexpected(g, expected)) - [] -> Error(Unexpected("EOF", expected)) - } -} - -fn parse_inline_table( - input: Tokens, - properties: Dict(String, Toml), -) -> Parsed(Toml) { - let input = skip_whitespace(input) - case input { - ["}", ..input] -> Ok(#(InlineTable(properties), input)) - _ -> - case parse_inline_table_property(input, properties) { - Ok(#(properties, input)) -> { - let input = skip_whitespace(input) - case input { - ["}", ..input] -> Ok(#(InlineTable(properties), input)) - [",", ..input] -> { - let input = skip_whitespace(input) - parse_inline_table(input, properties) - } - [g, ..] -> Error(Unexpected(g, "}")) - [] -> Error(Unexpected("EOF", "}")) - } - } - Error(e) -> Error(e) - } - } -} - -fn parse_inline_table_property( - input: Tokens, - properties: Dict(String, Toml), -) -> Parsed(Dict(String, Toml)) { - let input = skip_whitespace(input) - use key, input <- do(parse_key(input, [])) - let input = skip_line_whitespace(input) - use input <- expect(input, "=") - let input = skip_line_whitespace(input) - use value, input <- do(parse_value(input)) - case insert(properties, key, value) { - Ok(properties) -> Ok(#(properties, input)) - Error(e) -> Error(e) - } -} - -fn parse_array(input: Tokens, elements: List(Toml)) -> Parsed(Toml) { - let input = skip_whitespace(input) - case input { - ["]", ..input] -> Ok(#(Array(list.reverse(elements)), input)) - _ -> { - use element, input <- do(parse_value(input)) - let elements = [element, ..elements] - let input = skip_whitespace(input) - case input { - ["]", ..input] -> Ok(#(Array(list.reverse(elements)), input)) - [",", ..input] -> { - let input = skip_whitespace(input) - parse_array(input, elements) - } - [g, ..] -> Error(Unexpected(g, "]")) - [] -> Error(Unexpected("EOF", "]")) - } - } - } -} - -fn parse_hex(input: Tokens, number: Int, sign: Sign) -> Parsed(Toml) { - case input { - ["_", ..input] -> parse_hex(input, number, sign) - ["0", ..input] -> parse_hex(input, number * 16 + 0, sign) - ["1", ..input] -> parse_hex(input, number * 16 + 1, sign) - ["2", ..input] -> parse_hex(input, number * 16 + 2, sign) - ["3", ..input] -> parse_hex(input, number * 16 + 3, sign) - ["4", ..input] -> parse_hex(input, number * 16 + 4, sign) - ["5", ..input] -> parse_hex(input, number * 16 + 5, sign) - ["6", ..input] -> parse_hex(input, number * 16 + 6, sign) - ["7", ..input] -> parse_hex(input, number * 16 + 7, sign) - ["8", ..input] -> parse_hex(input, number * 16 + 8, sign) - ["9", ..input] -> parse_hex(input, number * 16 + 9, sign) - ["a", ..input] -> parse_hex(input, number * 16 + 10, sign) - ["b", ..input] -> parse_hex(input, number * 16 + 11, sign) - ["c", ..input] -> parse_hex(input, number * 16 + 12, sign) - ["d", ..input] -> parse_hex(input, number * 16 + 13, sign) - ["e", ..input] -> parse_hex(input, number * 16 + 14, sign) - ["f", ..input] -> parse_hex(input, number * 16 + 15, sign) - ["A", ..input] -> parse_hex(input, number * 16 + 10, sign) - ["B", ..input] -> parse_hex(input, number * 16 + 11, sign) - ["C", ..input] -> parse_hex(input, number * 16 + 12, sign) - ["D", ..input] -> parse_hex(input, number * 16 + 13, sign) - ["E", ..input] -> parse_hex(input, number * 16 + 14, sign) - ["F", ..input] -> parse_hex(input, number * 16 + 15, sign) - - // Anything else and the number is terminated - input -> { - let number = case sign { - Positive -> number - Negative -> -number - } - Ok(#(Int(number), input)) - } - } -} - -fn parse_octal(input: Tokens, number: Int, sign: Sign) -> Parsed(Toml) { - case input { - ["_", ..input] -> parse_octal(input, number, sign) - ["0", ..input] -> parse_octal(input, number * 8 + 0, sign) - ["1", ..input] -> parse_octal(input, number * 8 + 1, sign) - ["2", ..input] -> parse_octal(input, number * 8 + 2, sign) - ["3", ..input] -> parse_octal(input, number * 8 + 3, sign) - ["4", ..input] -> parse_octal(input, number * 8 + 4, sign) - ["5", ..input] -> parse_octal(input, number * 8 + 5, sign) - ["6", ..input] -> parse_octal(input, number * 8 + 6, sign) - ["7", ..input] -> parse_octal(input, number * 8 + 7, sign) - - // Anything else and the number is terminated - input -> { - let number = case sign { - Positive -> number - Negative -> -number - } - Ok(#(Int(number), input)) - } - } -} - -fn parse_binary(input: Tokens, number: Int, sign: Sign) -> Parsed(Toml) { - case input { - ["_", ..input] -> parse_binary(input, number, sign) - ["0", ..input] -> parse_binary(input, number * 2 + 0, sign) - ["1", ..input] -> parse_binary(input, number * 2 + 1, sign) - - // Anything else and the number is terminated - input -> { - let number = case sign { - Positive -> number - Negative -> -number - } - Ok(#(Int(number), input)) - } - } -} - -fn parse_number(input: Tokens, number: Int, sign: Sign) -> Parsed(Toml) { - case input { - ["_", ..input] -> parse_number(input, number, sign) - ["0", ..input] -> parse_number(input, number * 10 + 0, sign) - ["1", ..input] -> parse_number(input, number * 10 + 1, sign) - ["2", ..input] -> parse_number(input, number * 10 + 2, sign) - ["3", ..input] -> parse_number(input, number * 10 + 3, sign) - ["4", ..input] -> parse_number(input, number * 10 + 4, sign) - ["5", ..input] -> parse_number(input, number * 10 + 5, sign) - ["6", ..input] -> parse_number(input, number * 10 + 6, sign) - ["7", ..input] -> parse_number(input, number * 10 + 7, sign) - ["8", ..input] -> parse_number(input, number * 10 + 8, sign) - ["9", ..input] -> parse_number(input, number * 10 + 9, sign) - - ["-", ..input] -> parse_date(input, number) - [":", ..input] if number < 24 -> parse_time_minute(input, number) - - [".", ..input] -> parse_float(input, int.to_float(number), sign, 0.1) - - ["e", "+", ..input] -> - parse_exponent(input, int.to_float(number), sign, 0, Positive) - ["e", "-", ..input] -> - parse_exponent(input, int.to_float(number), sign, 0, Negative) - ["e", ..input] -> - parse_exponent(input, int.to_float(number), sign, 0, Positive) - ["E", "+", ..input] -> - parse_exponent(input, int.to_float(number), sign, 0, Positive) - ["E", "-", ..input] -> - parse_exponent(input, int.to_float(number), sign, 0, Negative) - ["E", ..input] -> - parse_exponent(input, int.to_float(number), sign, 0, Positive) - - // Anything else and the number is terminated - input -> { - let number = case sign { - Positive -> number - Negative -> -number - } - Ok(#(Int(number), input)) - } - } -} - -fn parse_exponent( - input: Tokens, - n: Float, - n_sign: Sign, - ex: Int, - ex_sign: Sign, -) -> Parsed(Toml) { - case input { - ["_", ..input] -> parse_exponent(input, n, n_sign, ex, ex_sign) - ["0", ..input] -> parse_exponent(input, n, n_sign, ex * 10, ex_sign) - ["1", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 1, ex_sign) - ["2", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 2, ex_sign) - ["3", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 3, ex_sign) - ["4", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 4, ex_sign) - ["5", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 5, ex_sign) - ["6", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 6, ex_sign) - ["7", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 7, ex_sign) - ["8", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 8, ex_sign) - ["9", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 9, ex_sign) - - // Anything else and the number is terminated - input -> { - let number = case n_sign { - Positive -> n - Negative -> n *. -1.0 - } - let exponent = - int.to_float(case ex_sign { - Positive -> ex - Negative -> -ex - }) - let multiplier = case float.power(10.0, exponent) { - Ok(multiplier) -> multiplier - Error(_) -> 1.0 - } - Ok(#(Float(number *. multiplier), input)) - } - } -} - -fn parse_float( - input: Tokens, - number: Float, - sign: Sign, - unit: Float, -) -> Parsed(Toml) { - case input { - ["_", ..input] -> parse_float(input, number, sign, unit) - ["0", ..input] -> parse_float(input, number, sign, unit *. 0.1) - ["1", ..input] -> - parse_float(input, number +. 1.0 *. unit, sign, unit *. 0.1) - ["2", ..input] -> - parse_float(input, number +. 2.0 *. unit, sign, unit *. 0.1) - ["3", ..input] -> - parse_float(input, number +. 3.0 *. unit, sign, unit *. 0.1) - ["4", ..input] -> - parse_float(input, number +. 4.0 *. unit, sign, unit *. 0.1) - ["5", ..input] -> - parse_float(input, number +. 5.0 *. unit, sign, unit *. 0.1) - ["6", ..input] -> - parse_float(input, number +. 6.0 *. unit, sign, unit *. 0.1) - ["7", ..input] -> - parse_float(input, number +. 7.0 *. unit, sign, unit *. 0.1) - ["8", ..input] -> - parse_float(input, number +. 8.0 *. unit, sign, unit *. 0.1) - ["9", ..input] -> - parse_float(input, number +. 9.0 *. unit, sign, unit *. 0.1) - - ["e", "+", ..input] -> parse_exponent(input, number, sign, 0, Positive) - ["e", "-", ..input] -> parse_exponent(input, number, sign, 0, Negative) - ["e", ..input] -> parse_exponent(input, number, sign, 0, Positive) - ["E", "+", ..input] -> parse_exponent(input, number, sign, 0, Positive) - ["E", "-", ..input] -> parse_exponent(input, number, sign, 0, Negative) - ["E", ..input] -> parse_exponent(input, number, sign, 0, Positive) - - // Anything else and the number is terminated - input -> { - let number = case sign { - Positive -> number - Negative -> number *. -1.0 - } - Ok(#(Float(number), input)) - } - } -} - -fn parse_string(input: Tokens, string: String) -> Parsed(Toml) { - case input { - ["\"", ..input] -> Ok(#(String(string), input)) - ["\\", "t", ..input] -> parse_string(input, string <> "\t") - ["\\", "e", ..input] -> parse_string(input, string <> "\u{001b}") - ["\\", "b", ..input] -> parse_string(input, string <> "\u{0008}") - ["\\", "n", ..input] -> parse_string(input, string <> "\n") - ["\\", "r", ..input] -> parse_string(input, string <> "\r") - ["\\", "f", ..input] -> parse_string(input, string <> "\f") - ["\\", "\"", ..input] -> parse_string(input, string <> "\"") - ["\\", "\\", ..input] -> parse_string(input, string <> "\\") - [] -> Error(Unexpected("EOF", "\"")) - ["\n", ..] -> Error(Unexpected("\n", "\"")) - ["\r\n", ..] -> Error(Unexpected("\r\n", "\"")) - [g, ..input] -> parse_string(input, string <> g) - } -} - -fn parse_multi_line_string(input: Tokens, string: String) -> Parsed(Toml) { - case input { - ["\"", "\"", "\"", ..input] -> Ok(#(String(string), input)) - ["\\", "\n", ..input] -> - parse_multi_line_string(skip_whitespace(input), string) - ["\\", "\r\n", ..input] -> - parse_multi_line_string(skip_whitespace(input), string) - ["\r\n", ..input] if string == "" -> parse_multi_line_string(input, string) - ["\n", ..input] if string == "" -> parse_multi_line_string(input, string) - ["\r\n", ..input] if string == "" -> parse_multi_line_string(input, string) - ["\\", "t", ..input] -> parse_multi_line_string(input, string <> "\t") - ["\\", "n", ..input] -> parse_multi_line_string(input, string <> "\n") - ["\\", "r", ..input] -> parse_multi_line_string(input, string <> "\r") - ["\\", "\"", ..input] -> parse_multi_line_string(input, string <> "\"") - ["\\", "\\", ..input] -> parse_multi_line_string(input, string <> "\\") - [] -> Error(Unexpected("EOF", "\"")) - [g, ..input] -> parse_multi_line_string(input, string <> g) - } -} - -fn parse_multi_line_literal_string( - input: Tokens, - string: String, -) -> Parsed(Toml) { - case input { - [] -> Error(Unexpected("EOF", "\"")) - ["'", "'", "'", "'", ..] -> Error(Unexpected("''''", "'''")) - ["'", "'", "'", ..input] -> Ok(#(String(string), input)) - ["\n", ..input] if string == "" -> - parse_multi_line_literal_string(input, string) - ["\r\n", ..input] if string == "" -> - parse_multi_line_literal_string(input, string) - [g, ..input] -> parse_multi_line_literal_string(input, string <> g) - } -} - -fn parse_literal_string(input: Tokens, string: String) -> Parsed(Toml) { - case input { - [] -> Error(Unexpected("EOF", "\"")) - ["\n", ..] -> Error(Unexpected("\n", "'")) - ["\r\n", ..] -> Error(Unexpected("\r\n", "'")) - ["'", ..input] -> Ok(#(String(string), input)) - [g, ..input] -> parse_literal_string(input, string <> g) - } -} - -fn reverse_arrays_of_tables(toml: Toml) -> Toml { - case toml { - ArrayOfTables(tables) -> - ArrayOfTables(reverse_arrays_of_tables_array(tables, [])) - - Table(table) -> Table(reverse_arrays_of_tables_table(table)) - - _ -> toml - } -} - -fn reverse_arrays_of_tables_table( - table: Dict(String, Toml), -) -> Dict(String, Toml) { - dict.map_values(table, fn(_, v) { reverse_arrays_of_tables(v) }) -} - -fn reverse_arrays_of_tables_array( - array: List(Dict(String, Toml)), - acc: List(Dict(String, Toml)), -) -> List(Dict(String, Toml)) { - case array { - [] -> acc - [first, ..rest] -> { - let first = reverse_arrays_of_tables_table(first) - reverse_arrays_of_tables_array(rest, [first, ..acc]) - } - } -} - -fn parse_time_minute(input: Tokens, hours: Int) -> Parsed(Toml) { - use minutes, input <- do(parse_number_under_60(input, "minutes")) - use #(seconds, ms), input <- do(parse_time_s_ms(input)) - let time = TimeValue(hours, minutes, seconds, ms) - Ok(#(Time(time), input)) -} - -fn parse_hour_minute(input: Tokens) -> Parsed(#(Int, Int)) { - use hours, input <- do(case input { - ["0", "0", ":", ..input] -> Ok(#(0, input)) - ["0", "1", ":", ..input] -> Ok(#(1, input)) - ["0", "2", ":", ..input] -> Ok(#(2, input)) - ["0", "3", ":", ..input] -> Ok(#(3, input)) - ["0", "4", ":", ..input] -> Ok(#(4, input)) - ["0", "5", ":", ..input] -> Ok(#(5, input)) - ["0", "6", ":", ..input] -> Ok(#(6, input)) - ["0", "7", ":", ..input] -> Ok(#(7, input)) - ["0", "8", ":", ..input] -> Ok(#(8, input)) - ["0", "9", ":", ..input] -> Ok(#(9, input)) - ["1", "0", ":", ..input] -> Ok(#(10, input)) - ["1", "1", ":", ..input] -> Ok(#(11, input)) - ["1", "2", ":", ..input] -> Ok(#(12, input)) - ["1", "3", ":", ..input] -> Ok(#(13, input)) - ["1", "4", ":", ..input] -> Ok(#(14, input)) - ["1", "5", ":", ..input] -> Ok(#(15, input)) - ["1", "6", ":", ..input] -> Ok(#(16, input)) - ["1", "7", ":", ..input] -> Ok(#(17, input)) - ["1", "8", ":", ..input] -> Ok(#(18, input)) - ["1", "9", ":", ..input] -> Ok(#(19, input)) - ["2", "0", ":", ..input] -> Ok(#(20, input)) - ["2", "1", ":", ..input] -> Ok(#(21, input)) - ["2", "2", ":", ..input] -> Ok(#(22, input)) - ["2", "3", ":", ..input] -> Ok(#(23, input)) - [g, ..] -> Error(Unexpected(g, "time")) - [] -> Error(Unexpected("EOF", "time")) - }) - - use minutes, input <- do(parse_number_under_60(input, "minutes")) - Ok(#(#(hours, minutes), input)) -} - -fn parse_time_value(input: Tokens) -> Parsed(Time) { - use #(hours, minutes), input <- do(parse_hour_minute(input)) - use #(seconds, ms), input <- do(parse_time_s_ms(input)) - let time = TimeValue(hours, minutes, seconds, ms) - Ok(#(time, input)) -} - -fn parse_time_s_ms(input: Tokens) -> Parsed(#(Int, Int)) { - case input { - [":", ..input] -> { - use seconds, input <- do(parse_number_under_60(input, "seconds")) - case input { - [".", ..input] -> parse_time_ms(input, seconds, 0) - _ -> Ok(#(#(seconds, 0), input)) - } - } - - _ -> Ok(#(#(0, 0), input)) - } -} - -fn parse_time_ms(input: Tokens, seconds: Int, ms: Int) -> Parsed(#(Int, Int)) { - case input { - ["0", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 0) - ["1", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 1) - ["2", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 2) - ["3", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 3) - ["4", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 4) - ["5", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 5) - ["6", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 6) - ["7", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 7) - ["8", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 8) - ["9", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 9) - - // Anything else and the number is terminated - _ -> Ok(#(#(seconds, ms), input)) - } -} - -fn parse_number_under_60(input: Tokens, expected: String) -> Parsed(Int) { - case input { - ["0", "0", ..input] -> Ok(#(0, input)) - ["0", "1", ..input] -> Ok(#(1, input)) - ["0", "2", ..input] -> Ok(#(2, input)) - ["0", "3", ..input] -> Ok(#(3, input)) - ["0", "4", ..input] -> Ok(#(4, input)) - ["0", "5", ..input] -> Ok(#(5, input)) - ["0", "6", ..input] -> Ok(#(6, input)) - ["0", "7", ..input] -> Ok(#(7, input)) - ["0", "8", ..input] -> Ok(#(8, input)) - ["0", "9", ..input] -> Ok(#(9, input)) - ["1", "0", ..input] -> Ok(#(10, input)) - ["1", "1", ..input] -> Ok(#(11, input)) - ["1", "2", ..input] -> Ok(#(12, input)) - ["1", "3", ..input] -> Ok(#(13, input)) - ["1", "4", ..input] -> Ok(#(14, input)) - ["1", "5", ..input] -> Ok(#(15, input)) - ["1", "6", ..input] -> Ok(#(16, input)) - ["1", "7", ..input] -> Ok(#(17, input)) - ["1", "8", ..input] -> Ok(#(18, input)) - ["1", "9", ..input] -> Ok(#(19, input)) - ["2", "0", ..input] -> Ok(#(20, input)) - ["2", "1", ..input] -> Ok(#(21, input)) - ["2", "2", ..input] -> Ok(#(22, input)) - ["2", "3", ..input] -> Ok(#(23, input)) - ["2", "4", ..input] -> Ok(#(24, input)) - ["2", "5", ..input] -> Ok(#(25, input)) - ["2", "6", ..input] -> Ok(#(26, input)) - ["2", "7", ..input] -> Ok(#(27, input)) - ["2", "8", ..input] -> Ok(#(28, input)) - ["2", "9", ..input] -> Ok(#(29, input)) - ["3", "0", ..input] -> Ok(#(30, input)) - ["3", "1", ..input] -> Ok(#(31, input)) - ["3", "2", ..input] -> Ok(#(32, input)) - ["3", "3", ..input] -> Ok(#(33, input)) - ["3", "4", ..input] -> Ok(#(34, input)) - ["3", "5", ..input] -> Ok(#(35, input)) - ["3", "6", ..input] -> Ok(#(36, input)) - ["3", "7", ..input] -> Ok(#(37, input)) - ["3", "8", ..input] -> Ok(#(38, input)) - ["3", "9", ..input] -> Ok(#(39, input)) - ["4", "0", ..input] -> Ok(#(40, input)) - ["4", "1", ..input] -> Ok(#(41, input)) - ["4", "2", ..input] -> Ok(#(42, input)) - ["4", "3", ..input] -> Ok(#(43, input)) - ["4", "4", ..input] -> Ok(#(44, input)) - ["4", "5", ..input] -> Ok(#(45, input)) - ["4", "6", ..input] -> Ok(#(46, input)) - ["4", "7", ..input] -> Ok(#(47, input)) - ["4", "8", ..input] -> Ok(#(48, input)) - ["4", "9", ..input] -> Ok(#(49, input)) - ["5", "0", ..input] -> Ok(#(50, input)) - ["5", "1", ..input] -> Ok(#(51, input)) - ["5", "2", ..input] -> Ok(#(52, input)) - ["5", "3", ..input] -> Ok(#(53, input)) - ["5", "4", ..input] -> Ok(#(54, input)) - ["5", "5", ..input] -> Ok(#(55, input)) - ["5", "6", ..input] -> Ok(#(56, input)) - ["5", "7", ..input] -> Ok(#(57, input)) - ["5", "8", ..input] -> Ok(#(58, input)) - ["5", "9", ..input] -> Ok(#(59, input)) - - [g, ..] -> Error(Unexpected(g, expected)) - [] -> Error(Unexpected("EOF", expected)) - } -} - -fn parse_date(input: Tokens, year: Int) -> Parsed(Toml) { - case input { - ["0", "1", "-", ..input] -> parse_date_day(input, year, 1) - ["0", "2", "-", ..input] -> parse_date_day(input, year, 2) - ["0", "3", "-", ..input] -> parse_date_day(input, year, 3) - ["0", "4", "-", ..input] -> parse_date_day(input, year, 4) - ["0", "5", "-", ..input] -> parse_date_day(input, year, 5) - ["0", "6", "-", ..input] -> parse_date_day(input, year, 6) - ["0", "7", "-", ..input] -> parse_date_day(input, year, 7) - ["0", "8", "-", ..input] -> parse_date_day(input, year, 8) - ["0", "9", "-", ..input] -> parse_date_day(input, year, 9) - ["1", "0", "-", ..input] -> parse_date_day(input, year, 10) - ["1", "1", "-", ..input] -> parse_date_day(input, year, 11) - ["1", "2", "-", ..input] -> parse_date_day(input, year, 12) - - [g, ..] -> Error(Unexpected(g, "date month")) - [] -> Error(Unexpected("EOF", "date month")) - } -} - -fn parse_date_day(input: Tokens, year: Int, month: Int) -> Parsed(Toml) { - case input { - ["0", "1", ..input] -> parse_date_end(input, year, month, 1) - ["0", "2", ..input] -> parse_date_end(input, year, month, 2) - ["0", "3", ..input] -> parse_date_end(input, year, month, 3) - ["0", "4", ..input] -> parse_date_end(input, year, month, 4) - ["0", "5", ..input] -> parse_date_end(input, year, month, 5) - ["0", "6", ..input] -> parse_date_end(input, year, month, 6) - ["0", "7", ..input] -> parse_date_end(input, year, month, 7) - ["0", "8", ..input] -> parse_date_end(input, year, month, 8) - ["0", "9", ..input] -> parse_date_end(input, year, month, 9) - ["1", "0", ..input] -> parse_date_end(input, year, month, 10) - ["1", "1", ..input] -> parse_date_end(input, year, month, 11) - ["1", "2", ..input] -> parse_date_end(input, year, month, 12) - ["1", "3", ..input] -> parse_date_end(input, year, month, 13) - ["1", "4", ..input] -> parse_date_end(input, year, month, 14) - ["1", "5", ..input] -> parse_date_end(input, year, month, 15) - ["1", "6", ..input] -> parse_date_end(input, year, month, 16) - ["1", "7", ..input] -> parse_date_end(input, year, month, 17) - ["1", "8", ..input] -> parse_date_end(input, year, month, 18) - ["1", "9", ..input] -> parse_date_end(input, year, month, 19) - ["2", "0", ..input] -> parse_date_end(input, year, month, 20) - ["2", "1", ..input] -> parse_date_end(input, year, month, 21) - ["2", "2", ..input] -> parse_date_end(input, year, month, 22) - ["2", "3", ..input] -> parse_date_end(input, year, month, 23) - ["2", "4", ..input] -> parse_date_end(input, year, month, 24) - ["2", "5", ..input] -> parse_date_end(input, year, month, 25) - ["2", "6", ..input] -> parse_date_end(input, year, month, 26) - ["2", "7", ..input] -> parse_date_end(input, year, month, 27) - ["2", "8", ..input] -> parse_date_end(input, year, month, 28) - ["2", "9", ..input] -> parse_date_end(input, year, month, 29) - ["3", "0", ..input] -> parse_date_end(input, year, month, 30) - ["3", "1", ..input] -> parse_date_end(input, year, month, 31) - - [g, ..] -> Error(Unexpected(g, "date day")) - [] -> Error(Unexpected("EOF", "date day")) - } -} - -fn parse_date_end( - input: Tokens, - year: Int, - month: Int, - day: Int, -) -> Parsed(Toml) { - let date = DateValue(year, month, day) - case input { - [" ", ..input] | ["T", ..input] -> { - use time, input <- do(parse_time_value(input)) - use offset, input <- do(parse_offset(input)) - Ok(#(DateTime(DateTimeValue(date, time, offset)), input)) - } - - _ -> Ok(#(Date(date), input)) - } -} - -fn parse_offset(input: Tokens) -> Parsed(Offset) { - case input { - ["Z", ..input] -> Ok(#(Offset(Positive, 0, 0), input)) - ["+", ..input] -> parse_offset_hours(input, Positive) - ["-", ..input] -> parse_offset_hours(input, Negative) - _ -> Ok(#(Local, input)) - } -} - -fn parse_offset_hours(input: Tokens, sign: Sign) -> Parsed(Offset) { - use #(hours, minutes), input <- do(parse_hour_minute(input)) - Ok(#(Offset(sign, hours, minutes), input)) -} diff --git a/packages/tom/test/tom_test.gleam b/packages/tom/test/tom_test.gleam deleted file mode 100644 index ce51743..0000000 --- a/packages/tom/test/tom_test.gleam +++ /dev/null @@ -1,930 +0,0 @@ -import gleam/dict -import gleeunit -import gleeunit/should -import tom - -pub fn main() { - gleeunit.main() -} - -pub fn parse_empty_test() { - "" - |> tom.parse - |> should.equal(Ok(dict.from_list([]))) -} - -pub fn parse_spaces_test() { - " " - |> tom.parse - |> should.equal(Ok(dict.from_list([]))) -} - -pub fn parse_newline_test() { - "\n" - |> tom.parse - |> should.equal(Ok(dict.from_list([]))) -} - -pub fn parse_crlf_test() { - "\r\n" - |> tom.parse - |> should.equal(Ok(dict.from_list([]))) -} - -pub fn parse_quoted_key_test() { - let expected = dict.from_list([#(" ", tom.Bool(True))]) - "\" \" = true\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_single_key_test() { - let expected = dict.from_list([#("", tom.Bool(True))]) - "'' = true\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_true_test() { - let expected = dict.from_list([#("cool", tom.Bool(True))]) - "cool = true\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_false_test() { - let expected = dict.from_list([#("cool", tom.Bool(False))]) - "cool = false\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_unicode_key_test() { - let expected = dict.from_list([#("பெண்", tom.Bool(False))]) - "பெண் = false\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_int_test() { - let expected = dict.from_list([#("it", tom.Int(1))]) - "it = 1\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_int_underscored_test() { - let expected = dict.from_list([#("it", tom.Int(1_000_009))]) - "it = 1_000_0__0_9\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_int_positive_test() { - let expected = dict.from_list([#("it", tom.Int(234))]) - "it = +234\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_int_negative_test() { - let expected = dict.from_list([#("it", tom.Int(-234))]) - "it = -234\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_string_test() { - let expected = dict.from_list([#("hello", tom.String("Joe"))]) - "hello = \"Joe\"\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_string_escaped_quote_test() { - let expected = dict.from_list([#("hello", tom.String("\""))]) - "hello = \"\\\"\"\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_string_tab_test() { - let expected = dict.from_list([#("hello", tom.String("\t"))]) - "hello = \"\\t\"\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_string_newline_test() { - let expected = dict.from_list([#("hello", tom.String("\n"))]) - "hello = \"\\n\"\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_string_linefeed_test() { - let expected = dict.from_list([#("hello", tom.String("\r"))]) - "hello = \"\\r\"\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_escaped_slash_test() { - let expected = dict.from_list([#("hello", tom.String("\\"))]) - "hello = \"\\\\\"\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_float_test() { - let expected = dict.from_list([#("it", tom.Float(1.0))]) - "it = 1.0\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_bigger_float_test() { - let expected = dict.from_list([#("it", tom.Float(123_456_789.9876))]) - "it = 123456789.9876\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multi_segment_key_test() { - let expected = - dict.from_list([ - #( - "one", - tom.Table( - dict.from_list([ - #("two", tom.Table(dict.from_list([#("three", tom.Bool(True))]))), - ]), - ), - ), - ]) - "one.two.three = true\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multi_segment_key_with_spaeces_test() { - let expected = - dict.from_list([ - #( - "one", - tom.Table( - dict.from_list([ - #("two", tom.Table(dict.from_list([#("three", tom.Bool(True))]))), - ]), - ), - ), - ]) - "one . two . three = true\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multi_segment_key_quotes_test() { - let expected = - dict.from_list([ - #( - "1", - tom.Table( - dict.from_list([ - #("two", tom.Table(dict.from_list([#("3", tom.Bool(True))]))), - ]), - ), - ), - ]) - "\"1\".two.\"3\" = true\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multiple_keys_test() { - let expected = dict.from_list([#("a", tom.Int(1)), #("b", tom.Int(2))]) - "a = 1\nb = 2\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_duplicate_key_test() { - "a = 1\na = 2\n" - |> tom.parse - |> should.equal(Error(tom.KeyAlreadyInUse(["a"]))) -} - -pub fn parse_conflicting_keys_test() { - "a = 1\na.b = 2\n" - |> tom.parse - |> should.equal(Error(tom.KeyAlreadyInUse(["a"]))) -} - -pub fn parse_empty_array_test() { - let expected = dict.from_list([#("a", tom.Array([]))]) - "a = []\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_array_test() { - let expected = dict.from_list([#("a", tom.Array([tom.Int(1), tom.Int(2)]))]) - "a = [1, 2]\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multi_line_array_test() { - let expected = dict.from_list([#("a", tom.Array([tom.Int(1), tom.Int(2)]))]) - "a = [\n 1 \n ,\n 2,\n]\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_table_test() { - let expected = dict.from_list([#("a", tom.Table(dict.from_list([])))]) - "[a]\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_table_with_values_test() { - let expected = - dict.from_list([ - #( - "a", - tom.Table( - dict.from_list([ - #("a", tom.Int(1)), - #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), - ]), - ), - ), - ]) - "[a] -a = 1 -b.c = 2 -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_table_with_values_before_test() { - let expected = - dict.from_list([ - #("name", tom.String("Joe")), - #("size", tom.Int(123)), - #( - "a", - tom.Table( - dict.from_list([ - #("a", tom.Int(1)), - #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), - ]), - ), - ), - ]) - "name = \"Joe\" -size = 123 - -[a] -a = 1 -b.c = 2 -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multiple_tables_test() { - let expected = - dict.from_list([ - #("name", tom.String("Joe")), - #("size", tom.Int(123)), - #( - "a", - tom.Table( - dict.from_list([ - #("a", tom.Int(1)), - #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), - ]), - ), - ), - #("b", tom.Table(dict.from_list([#("a", tom.Int(1))]))), - ]) - "name = \"Joe\" -size = 123 - -[a] -a = 1 -b.c = 2 - -[b] -a = 1 -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_inline_table_empty_test() { - let expected = dict.from_list([#("a", tom.InlineTable(dict.from_list([])))]) - "a = {}\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_inline_table_test() { - let expected = - dict.from_list([ - #( - "a", - tom.InlineTable( - dict.from_list([ - #("a", tom.Int(1)), - #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), - ]), - ), - ), - ]) - "a = { - a = 1, - b.c = 2 -} -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_inline_trailing_comma_table_test() { - let expected = - dict.from_list([ - #( - "a", - tom.InlineTable( - dict.from_list([ - #("a", tom.Int(1)), - #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), - ]), - ), - ), - ]) - "a = { - a = 1, - b.c = 2, -} -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_invalid_newline_in_string_test() { - "a = \"\n\"" - |> tom.parse - |> should.equal(Error(tom.Unexpected("\n", "\""))) -} - -pub fn parse_invalid_newline_windows_in_string_test() { - "a = \"\r\n\"" - |> tom.parse - |> should.equal(Error(tom.Unexpected("\r\n", "\""))) -} - -pub fn parse_array_of_tables_empty_test() { - let expected = - dict.from_list([ - #( - "a", - tom.ArrayOfTables([ - dict.from_list([]), - dict.from_list([]), - dict.from_list([]), - ]), - ), - ]) - "[[a]] -[[a]] -[[a]] -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_array_of_tables_nonempty_test() { - let expected = - dict.from_list([ - #( - "a", - tom.ArrayOfTables([ - dict.from_list([#("a", tom.Int(1))]), - dict.from_list([#("a", tom.Int(2))]), - dict.from_list([#("a", tom.Int(3))]), - ]), - ), - ]) - "[[a]] -a = 1 - -[[a]] -a = 2 - -[[a]] -a = 3 -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_array_of_tables_with_subtable_test() { - let expected = - dict.from_list([ - #( - "fruits", - tom.ArrayOfTables([ - dict.from_list([]), - dict.from_list([ - #("name", tom.String("apple")), - #( - "physical", - tom.Table( - dict.from_list([ - #("color", tom.String("red")), - #("shape", tom.String("round")), - ]), - ), - ), - ]), - ]), - ), - ]) - "[[fruits]] - -[[fruits]] -name = \"apple\" - -[fruits.physical] # subtable -color = \"red\" -shape = \"round\" -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_single_quote_string_test() { - let expected = dict.from_list([#("a", tom.String("\\n"))]) - "a = '\\n'\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multi_line_string_test() { - let expected = dict.from_list([#("a", tom.String("hello\nworld"))]) - "a = \"\"\" -hello -world\"\"\" -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multi_line_single_quote_string_test() { - let expected = dict.from_list([#("a", tom.String("hello\\n\nworld"))]) - "a = ''' -hello\\n -world''' -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multi_line_single_quote_string_too_many_quotes_test() { - "a = ''' -'''' -''' -" - |> tom.parse - |> should.equal(Error(tom.Unexpected("''''", "'''"))) -} - -pub fn parse_multi_line_string_escape_newline_test() { - let expected = - dict.from_list([ - #("a", tom.String("The quick brown fox jumps over the lazy dog.")), - ]) - "a = \"\"\" -The quick brown \\ - - - fox jumps over \\ - the lazy dog.\"\"\" -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_multi_line_string_escape_newline_windows_test() { - let expected = - dict.from_list([ - #("a", tom.String("The quick brown fox jumps over the lazy dog.")), - ]) - "a = \"\"\" -The quick brown \\\r\n - - - fox jumps over \\\r\n - the lazy dog.\"\"\" -" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_nan_test() { - let expected = dict.from_list([#("a", tom.Nan(tom.Positive))]) - "a = nan\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_positive_nan_test() { - let expected = dict.from_list([#("a", tom.Nan(tom.Positive))]) - "a = +nan\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_negative_nan_test() { - let expected = dict.from_list([#("a", tom.Nan(tom.Negative))]) - "a = -nan\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_infinity_test() { - let expected = dict.from_list([#("a", tom.Infinity(tom.Positive))]) - "a = inf\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_positive_infinity_test() { - let expected = dict.from_list([#("a", tom.Infinity(tom.Positive))]) - "a = +inf\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_negative_infinity_test() { - let expected = dict.from_list([#("a", tom.Infinity(tom.Negative))]) - "a = -inf\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_write_to_key_that_does_not_exist_test() { - let expected = - dict.from_list([ - #("apple", tom.Table(dict.from_list([#("smooth", tom.Bool(True))]))), - ]) - "apple.smooth = true\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_binary_test() { - let expected = dict.from_list([#("a", tom.Int(0b101010))]) - "a = 0b101010\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_binary_positive_test() { - let expected = dict.from_list([#("a", tom.Int(0b101010))]) - "a = +0b101010\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_binary_negative_test() { - let expected = dict.from_list([#("a", tom.Int(0b101010 * -1))]) - "a = -0b101010\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_binary_underscores_test() { - let expected = dict.from_list([#("a", tom.Int(0b101010))]) - "a = 0b1__010___1_0\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_octal_test() { - let expected = dict.from_list([#("a", tom.Int(0o1234567))]) - "a = 0o1234567\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_octal_positive_test() { - let expected = dict.from_list([#("a", tom.Int(0o1234567))]) - "a = +0o1234567\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_octal_negative_test() { - let expected = dict.from_list([#("a", tom.Int(0o1234567 * -1))]) - "a = -0o1234567\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_octal_underscores_test() { - let expected = dict.from_list([#("a", tom.Int(0o1234567))]) - "a = 0o1_23_45__6_7\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_hex_test() { - let expected = dict.from_list([#("a", tom.Int(0xdeadbeef))]) - "a = 0xdeadbeef\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_hex_positive_test() { - let expected = dict.from_list([#("a", tom.Int(0xdeadbeef))]) - "a = +0xdeadbeef\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_hex_negative_test() { - let expected = dict.from_list([#("a", tom.Int(0xdeadbeef * -1))]) - "a = -0xdeadbeef\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_hex_underscores_test() { - let expected = dict.from_list([#("a", tom.Int(0xdeadbeef))]) - "a = 0xd_e_a_d__b___e____e______f\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_hex_uppercase_test() { - let expected = dict.from_list([#("a", tom.Int(0xdeadbeef))]) - "a = +0xDEADBEEF\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_float_exponent_test() { - let expected = dict.from_list([#("a", tom.Float(1.0e6))]) - "a = 1e6\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_float_exponent_uppercase_test() { - let expected = dict.from_list([#("a", tom.Float(1.0e6))]) - "a = 1E6\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_float_exponent_postive_test() { - let expected = dict.from_list([#("a", tom.Float(5.0e22))]) - "a = 5e+22\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_float_exponent_negative_test() { - let expected = dict.from_list([#("a", tom.Float(-2.0e-22))]) - "a = -2e-22\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_float_decimal_and_exponent_test() { - let expected = dict.from_list([#("a", tom.Float(6.626e25))]) - "a = 6.626e25\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_float_decimal_and_exponent_positive_test() { - let expected = dict.from_list([#("a", tom.Float(6.626e25))]) - "a = 6.626e+25\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_float_decimal_and_exponent_negative_test() { - let expected = dict.from_list([#("a", tom.Float(6.626e-25))]) - "a = 6.626e-25\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_date_test() { - let expected = dict.from_list([#("a", tom.Date(tom.DateValue(1979, 5, 27)))]) - "a = 1979-05-27\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_time_test() { - let expected = dict.from_list([#("a", tom.Time(tom.TimeValue(7, 32, 1, 0)))]) - "a = 07:32:01\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_time_zero_minute_test() { - let expected = dict.from_list([#("a", tom.Time(tom.TimeValue(7, 0, 1, 0)))]) - "a = 07:00:01\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_time_milliseconds_test() { - let expected = - dict.from_list([#("a", tom.Time(tom.TimeValue(7, 32, 1, 999_999)))]) - "a = 07:32:01.999999\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_time_milliseconds_1_test() { - let expected = - dict.from_list([#("a", tom.Time(tom.TimeValue(7, 32, 1, 9179)))]) - "a = 07:32:01.09179\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_time_no_seconds_test() { - let expected = dict.from_list([#("a", tom.Time(tom.TimeValue(7, 32, 0, 0)))]) - "a = 07:32\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_date_time_test() { - let expected = - dict.from_list([ - #( - "a", - tom.DateTime(tom.DateTimeValue( - tom.DateValue(1979, 5, 27), - tom.TimeValue(7, 32, 0, 0), - offset: tom.Local, - )), - ), - ]) - "a = 1979-05-27T07:32:00\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_date_time_space_test() { - let expected = - dict.from_list([ - #( - "a", - tom.DateTime(tom.DateTimeValue( - tom.DateValue(1979, 5, 27), - tom.TimeValue(7, 0, 1, 0), - offset: tom.Local, - )), - ), - ]) - "a = 1979-05-27 07:00:01\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_offset_z_date_time_test() { - let expected = - dict.from_list([ - #( - "a", - tom.DateTime(tom.DateTimeValue( - tom.DateValue(1979, 5, 27), - tom.TimeValue(7, 32, 0, 0), - offset: tom.Offset(tom.Positive, 0, 0), - )), - ), - ]) - "a = 1979-05-27T07:32:00Z\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_offset_z_date_time_space_test() { - let expected = - dict.from_list([ - #( - "a", - tom.DateTime(tom.DateTimeValue( - tom.DateValue(1979, 5, 27), - tom.TimeValue(7, 0, 1, 0), - offset: tom.Offset(tom.Positive, 0, 0), - )), - ), - ]) - "a = 1979-05-27 07:00:01Z\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_offset_positive_date_time_space_test() { - let expected = - dict.from_list([ - #( - "a", - tom.DateTime(tom.DateTimeValue( - tom.DateValue(1979, 5, 27), - tom.TimeValue(7, 0, 1, 0), - offset: tom.Offset(tom.Positive, 7, 40), - )), - ), - ]) - "a = 1979-05-27 07:00:01+07:40\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_offset_negative_date_time_space_test() { - let expected = - dict.from_list([ - #( - "a", - tom.DateTime(tom.DateTimeValue( - tom.DateValue(1979, 5, 27), - tom.TimeValue(7, 0, 1, 0), - offset: tom.Offset(tom.Negative, 7, 1), - )), - ), - ]) - "a = 1979-05-27 07:00:01-07:01\n" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_no_trailing_newline_test() { - let expected = dict.from_list([#("a", tom.Int(1))]) - "a = 1" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_trailing_whitespace_test() { - let expected = dict.from_list([#("a", tom.Int(1))]) - "a = 1 " - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_trailing_other_test() { - "a = 1 b" - |> tom.parse - |> should.equal(Error(tom.Unexpected("b", "\n"))) -} - -pub fn parse_sequence_e_test() { - "a = \"\\e\"" - |> tom.parse - |> should.equal(Ok(dict.from_list([#("a", tom.String("\u{001b}"))]))) -} - -pub fn parse_sequence_f_test() { - "a = \"\\f\"" - |> tom.parse - |> should.equal(Ok(dict.from_list([#("a", tom.String("\f"))]))) -} - -pub fn parse_sequence_b_test() { - "a = \"\\b\"" - |> tom.parse - |> should.equal(Ok(dict.from_list([#("a", tom.String("\u{0008}"))]))) -} - -pub fn parse_ignore_comments_test() { - let expected = dict.from_list([#("field", tom.String("#"))]) - "# This should be ignored -field = \"#\"" - |> tom.parse - |> should.equal(Ok(expected)) -} - -pub fn parse_not_remove_hash_in_string_test() { - let content = tom.Table(dict.from_list([#("field", tom.String("#"))])) - let expected = dict.from_list([#("section", content)]) - "[section] -field = \"#\"" - |> tom.parse - |> should.equal(Ok(expected)) -} diff --git a/packages/vitools/package.json b/packages/vitools/package.json index 8264498..b7d29ef 100644 --- a/packages/vitools/package.json +++ b/packages/vitools/package.json @@ -2,6 +2,6 @@ "name": "vitools", "packageManager": "yarn@4.3.1", "devDependencies": { - "@chouqueth/gleam": "^1.3.2" + "@chouqueth/gleam": "^1.6.2" } } diff --git a/plugins/yarn-mise.js b/plugins/yarn-mise.js index 893b509..7c2bbfd 100644 --- a/plugins/yarn-mise.js +++ b/plugins/yarn-mise.js @@ -48,7 +48,7 @@ function addVenvInPath(path, tomlDir, venvPath, scriptEnv) { } module.exports = { - name: `plugin-hello-world`, + name: `yarn-mise`, factory: require => { const path = require('path') const fs = require('fs') diff --git a/yarn.lock b/yarn.lock index 7c816ea..1416ec6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -304,22 +304,15 @@ __metadata: languageName: node linkType: hard -"@chouqueth/gleam@npm:^1.4.1": - version: 1.4.1 - resolution: "@chouqueth/gleam@npm:1.4.1" +"@chouqueth/gleam@npm:^1.6.2": + version: 1.6.2 + resolution: "@chouqueth/gleam@npm:1.6.2" dependencies: cachedir: "npm:^2.4.0" tar: "npm:^7.1.0" bin: gleam: bin/cli.mjs - checksum: 10c0/d7238a489c699ff1fdab1701603e3066132826eb005926b1e3f2ed4fae0a894c34016cf8b32ac88495c2359a6ec4d447697a09fe3680ddd6b441611b4f42efb4 - languageName: node - linkType: hard - -"@esbuild/aix-ppc64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/aix-ppc64@npm:0.20.2" - conditions: os=aix & cpu=ppc64 + checksum: 10c0/e138f2ca54937f6fad1740b425c420b094a09cc6c56d442d9425b1c5600012d21780f4f356ca868d8708b0ce49e4fde89eee2aca224a1bec738e76a15e19046c languageName: node linkType: hard @@ -337,13 +330,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/android-arm64@npm:0.20.2" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm64@npm:0.21.5" @@ -358,13 +344,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/android-arm@npm:0.20.2" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm@npm:0.21.5" @@ -379,13 +358,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/android-x64@npm:0.20.2" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-x64@npm:0.21.5" @@ -400,13 +372,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/darwin-arm64@npm:0.20.2" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-arm64@npm:0.21.5" @@ -421,13 +386,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/darwin-x64@npm:0.20.2" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-x64@npm:0.21.5" @@ -442,13 +400,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/freebsd-arm64@npm:0.20.2" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-arm64@npm:0.21.5" @@ -463,13 +414,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/freebsd-x64@npm:0.20.2" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-x64@npm:0.21.5" @@ -484,13 +428,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-arm64@npm:0.20.2" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm64@npm:0.21.5" @@ -505,13 +442,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-arm@npm:0.20.2" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm@npm:0.21.5" @@ -526,13 +456,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-ia32@npm:0.20.2" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ia32@npm:0.21.5" @@ -547,13 +470,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-loong64@npm:0.20.2" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-loong64@npm:0.21.5" @@ -568,13 +484,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-mips64el@npm:0.20.2" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-mips64el@npm:0.21.5" @@ -589,13 +498,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-ppc64@npm:0.20.2" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ppc64@npm:0.21.5" @@ -610,13 +512,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-riscv64@npm:0.20.2" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-riscv64@npm:0.21.5" @@ -631,13 +526,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-s390x@npm:0.20.2" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-s390x@npm:0.21.5" @@ -652,13 +540,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/linux-x64@npm:0.20.2" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-x64@npm:0.21.5" @@ -673,13 +554,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/netbsd-x64@npm:0.20.2" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/netbsd-x64@npm:0.21.5" @@ -694,13 +568,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/openbsd-x64@npm:0.20.2" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/openbsd-x64@npm:0.21.5" @@ -715,13 +582,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/sunos-x64@npm:0.20.2" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/sunos-x64@npm:0.21.5" @@ -736,13 +596,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/win32-arm64@npm:0.20.2" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-arm64@npm:0.21.5" @@ -757,13 +610,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/win32-ia32@npm:0.20.2" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-ia32@npm:0.21.5" @@ -778,13 +624,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.20.2": - version: 0.20.2 - resolution: "@esbuild/win32-x64@npm:0.20.2" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-x64@npm:0.21.5" @@ -799,13 +638,6 @@ __metadata: languageName: node linkType: hard -"@iarna/toml@npm:^2.2.5": - version: 2.2.5 - resolution: "@iarna/toml@npm:2.2.5" - checksum: 10c0/d095381ad4554aca233b7cf5a91f243ef619e5e15efd3157bc640feac320545450d14b394aebbf6f02a2047437ced778ae598d5879a995441ab7b6c0b2c2f201 - languageName: node - linkType: hard - "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -829,15 +661,6 @@ __metadata: languageName: node linkType: hard -"@jest/schemas@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/schemas@npm:29.6.3" - dependencies: - "@sinclair/typebox": "npm:^0.27.8" - checksum: 10c0/b329e89cd5f20b9278ae1233df74016ebf7b385e0d14b9f4c1ad18d096c4c19d1e687aa113a9c976b16ec07f021ae53dea811fb8c1248a50ac34fbe009fdf6be - languageName: node - linkType: hard - "@jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.5 resolution: "@jridgewell/gen-mapping@npm:0.3.5" @@ -1234,13 +1057,6 @@ __metadata: languageName: node linkType: hard -"@sinclair/typebox@npm:^0.27.8": - version: 0.27.8 - resolution: "@sinclair/typebox@npm:0.27.8" - checksum: 10c0/ef6351ae073c45c2ac89494dbb3e1f87cc60a93ce4cde797b782812b6f97da0d620ae81973f104b43c9b7eaa789ad20ba4f6a1359f1cc62f63729a55a7d22d4e - languageName: node - linkType: hard - "@trivago/prettier-plugin-sort-imports@npm:^4.3.0": version: 4.3.0 resolution: "@trivago/prettier-plugin-sort-imports@npm:4.3.0" @@ -1261,83 +1077,13 @@ __metadata: languageName: node linkType: hard -"@types/dompurify@npm:^3.0.5": - version: 3.0.5 - resolution: "@types/dompurify@npm:3.0.5" - dependencies: - "@types/trusted-types": "npm:*" - checksum: 10c0/a34dcc4498ca250815ccf9aecbe82df96ba5db247d0440cf266a876757d47c52519c240db3475e794d7deb0d6b1af23328e02879be368ad0e26b20c0f0865dba - languageName: node - linkType: hard - -"@types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": +"@types/estree@npm:1.0.5": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" checksum: 10c0/b3b0e334288ddb407c7b3357ca67dbee75ee22db242ca7c56fe27db4e1a31989cb8af48a84dd401deb787fe10cc6b2ab1ee82dc4783be87ededbe3d53c79c70d languageName: node linkType: hard -"@types/trusted-types@npm:*": - version: 2.0.7 - resolution: "@types/trusted-types@npm:2.0.7" - checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c - languageName: node - linkType: hard - -"@vitest/expect@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/expect@npm:1.6.0" - dependencies: - "@vitest/spy": "npm:1.6.0" - "@vitest/utils": "npm:1.6.0" - chai: "npm:^4.3.10" - checksum: 10c0/a4351f912a70543e04960f5694f1f1ac95f71a856a46e87bba27d3eb72a08c5d11d35021cbdc6077452a152e7d93723fc804bba76c2cc53c8896b7789caadae3 - languageName: node - linkType: hard - -"@vitest/runner@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/runner@npm:1.6.0" - dependencies: - "@vitest/utils": "npm:1.6.0" - p-limit: "npm:^5.0.0" - pathe: "npm:^1.1.1" - checksum: 10c0/27d67fa51f40effe0e41ee5f26563c12c0ef9a96161f806036f02ea5eb9980c5cdf305a70673942e7a1e3d472d4d7feb40093ae93024ef1ccc40637fc65b1d2f - languageName: node - linkType: hard - -"@vitest/snapshot@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/snapshot@npm:1.6.0" - dependencies: - magic-string: "npm:^0.30.5" - pathe: "npm:^1.1.1" - pretty-format: "npm:^29.7.0" - checksum: 10c0/be027fd268d524589ff50c5fad7b4faa1ac5742b59ac6c1dc6f5a3930aad553560e6d8775e90ac4dfae4be746fc732a6f134ba95606a1519707ce70db3a772a5 - languageName: node - linkType: hard - -"@vitest/spy@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/spy@npm:1.6.0" - dependencies: - tinyspy: "npm:^2.2.0" - checksum: 10c0/df66ea6632b44fb76ef6a65c1abbace13d883703aff37cd6d062add6dcd1b883f19ce733af8e0f7feb185b61600c6eb4042a518e4fb66323d0690ec357f9401c - languageName: node - linkType: hard - -"@vitest/utils@npm:1.6.0": - version: 1.6.0 - resolution: "@vitest/utils@npm:1.6.0" - dependencies: - diff-sequences: "npm:^29.6.3" - estree-walker: "npm:^3.0.3" - loupe: "npm:^2.3.7" - pretty-format: "npm:^29.7.0" - checksum: 10c0/8b0d19835866455eb0b02b31c5ca3d8ad45f41a24e4c7e1f064b480f6b2804dc895a70af332f14c11ed89581011b92b179718523f55f5b14787285a0321b1301 - languageName: node - linkType: hard - "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -1345,16 +1091,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.3.2": - version: 8.3.3 - resolution: "acorn-walk@npm:8.3.3" - dependencies: - acorn: "npm:^8.11.0" - checksum: 10c0/4a9e24313e6a0a7b389e712ba69b66b455b4cb25988903506a8d247e7b126f02060b05a8a5b738a9284214e4ca95f383dd93443a4ba84f1af9b528305c7f243b - languageName: node - linkType: hard - -"acorn@npm:^8.11.0, acorn@npm:^8.11.3, acorn@npm:^8.8.1": +"acorn@npm:^8.8.1": version: 8.12.1 resolution: "acorn@npm:8.12.1" bin: @@ -1423,13 +1160,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^5.0.0": - version: 5.2.0 - resolution: "ansi-styles@npm:5.2.0" - checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df - languageName: node - linkType: hard - "ansi-styles@npm:^6.1.0": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" @@ -1447,53 +1177,11 @@ __metadata: languageName: node linkType: hard -"array-buffer-byte-length@npm:^1.0.1": - version: 1.0.1 - resolution: "array-buffer-byte-length@npm:1.0.1" - dependencies: - call-bind: "npm:^1.0.5" - is-array-buffer: "npm:^3.0.4" - checksum: 10c0/f5cdf54527cd18a3d2852ddf73df79efec03829e7373a8322ef5df2b4ef546fb365c19c71d6b42d641cb6bfe0f1a2f19bc0ece5b533295f86d7c3d522f228917 - languageName: node - linkType: hard - -"arraybuffer.prototype.slice@npm:^1.0.3": - version: 1.0.3 - resolution: "arraybuffer.prototype.slice@npm:1.0.3" - dependencies: - array-buffer-byte-length: "npm:^1.0.1" - call-bind: "npm:^1.0.5" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.22.3" - es-errors: "npm:^1.2.1" - get-intrinsic: "npm:^1.2.3" - is-array-buffer: "npm:^3.0.4" - is-shared-array-buffer: "npm:^1.0.2" - checksum: 10c0/d32754045bcb2294ade881d45140a5e52bda2321b9e98fa514797b7f0d252c4c5ab0d1edb34112652c62fa6a9398def568da63a4d7544672229afea283358c36 - languageName: node - linkType: hard - -"assertion-error@npm:^1.1.0": - version: 1.1.0 - resolution: "assertion-error@npm:1.1.0" - checksum: 10c0/25456b2aa333250f01143968e02e4884a34588a8538fbbf65c91a637f1dbfb8069249133cd2f4e530f10f624d206a664e7df30207830b659e9f5298b00a4099b - languageName: node - linkType: hard - -"available-typed-arrays@npm:^1.0.7": - version: 1.0.7 - resolution: "available-typed-arrays@npm:1.0.7" - dependencies: - possible-typed-array-names: "npm:^1.0.0" - checksum: 10c0/d07226ef4f87daa01bd0fe80f8f310982e345f372926da2e5296aecc25c41cab440916bbaa4c5e1034b453af3392f67df5961124e4b586df1e99793a1374bdb2 - languageName: node - linkType: hard - "backend@workspace:apps/backend": version: 0.0.0-use.local resolution: "backend@workspace:apps/backend" dependencies: - "@chouqueth/gleam": "npm:^1.3.2" + "@chouqueth/gleam": "npm:^1.6.2" languageName: unknown linkType: soft @@ -1511,23 +1199,6 @@ __metadata: languageName: node linkType: hard -"boolbase@npm:^1.0.0": - version: 1.0.0 - resolution: "boolbase@npm:1.0.0" - checksum: 10c0/e4b53deb4f2b85c52be0e21a273f2045c7b6a6ea002b0e139c744cb6f95e9ec044439a52883b0d74dedd1ff3da55ed140cfdddfed7fb0cccbed373de5dce1bcf - languageName: node - linkType: hard - -"brace-expansion@npm:^1.1.7": - version: 1.1.11 - resolution: "brace-expansion@npm:1.1.11" - dependencies: - balanced-match: "npm:^1.0.0" - concat-map: "npm:0.0.1" - checksum: 10c0/695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668 - languageName: node - linkType: hard - "brace-expansion@npm:^2.0.1": version: 2.0.1 resolution: "brace-expansion@npm:2.0.1" @@ -1560,13 +1231,6 @@ __metadata: languageName: node linkType: hard -"cac@npm:^6.7.14": - version: 6.7.14 - resolution: "cac@npm:6.7.14" - checksum: 10c0/4ee06aaa7bab8981f0d54e5f5f9d4adcd64058e9697563ce336d8a3878ed018ee18ebe5359b2430eceae87e0758e62ea2019c3f52ae6e211b1bd2e133856cd10 - languageName: node - linkType: hard - "cacache@npm:^18.0.0": version: 18.0.4 resolution: "cacache@npm:18.0.4" @@ -1594,19 +1258,6 @@ __metadata: languageName: node linkType: hard -"call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": - version: 1.0.7 - resolution: "call-bind@npm:1.0.7" - dependencies: - es-define-property: "npm:^1.0.0" - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.4" - set-function-length: "npm:^1.2.1" - checksum: 10c0/a3ded2e423b8e2a265983dba81c27e125b48eefb2655e7dfab6be597088da3d47c47976c24bc51b8fd9af1061f8f87b4ab78a314f3c77784b2ae2ba535ad8b8d - languageName: node - linkType: hard - "caniuse-lite@npm:^1.0.30001640": version: 1.0.30001642 resolution: "caniuse-lite@npm:1.0.30001642" @@ -1614,22 +1265,7 @@ __metadata: languageName: node linkType: hard -"chai@npm:^4.3.10": - version: 4.4.1 - resolution: "chai@npm:4.4.1" - dependencies: - assertion-error: "npm:^1.1.0" - check-error: "npm:^1.0.3" - deep-eql: "npm:^4.1.3" - get-func-name: "npm:^2.0.2" - loupe: "npm:^2.3.6" - pathval: "npm:^1.1.1" - type-detect: "npm:^4.0.8" - checksum: 10c0/91590a8fe18bd6235dece04ccb2d5b4ecec49984b50924499bdcd7a95c02cb1fd2a689407c19bb854497bde534ef57525cfad6c7fdd2507100fd802fbc2aefbd - languageName: node - linkType: hard - -"chalk@npm:^2.4.1, chalk@npm:^2.4.2": +"chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" dependencies: @@ -1659,15 +1295,6 @@ __metadata: languageName: node linkType: hard -"check-error@npm:^1.0.3": - version: 1.0.3 - resolution: "check-error@npm:1.0.3" - dependencies: - get-func-name: "npm:^2.0.2" - checksum: 10c0/94aa37a7315c0e8a83d0112b5bfb5a8624f7f0f81057c73e4707729cdd8077166c6aefb3d8e2b92c63ee130d4a2ff94bad46d547e12f3238cc1d78342a973841 - languageName: node - linkType: hard - "chokidar@npm:^3.5.3": version: 3.6.0 resolution: "chokidar@npm:3.6.0" @@ -1751,13 +1378,6 @@ __metadata: languageName: node linkType: hard -"concat-map@npm:0.0.1": - version: 0.0.1 - resolution: "concat-map@npm:0.0.1" - checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f - languageName: node - linkType: hard - "concurrently@npm:^8.2.2": version: 8.2.2 resolution: "concurrently@npm:8.2.2" @@ -1778,13 +1398,6 @@ __metadata: languageName: node linkType: hard -"confbox@npm:^0.1.7": - version: 0.1.7 - resolution: "confbox@npm:0.1.7" - checksum: 10c0/18b40c2f652196a833f3f1a5db2326a8a579cd14eacabfe637e4fc8cb9b68d7cf296139a38c5e7c688ce5041bf46f9adce05932d43fde44cf7e012840b5da111 - languageName: node - linkType: hard - "convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" @@ -1792,20 +1405,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^6.0.5": - version: 6.0.5 - resolution: "cross-spawn@npm:6.0.5" - dependencies: - nice-try: "npm:^1.0.4" - path-key: "npm:^2.0.1" - semver: "npm:^5.5.0" - shebang-command: "npm:^1.2.0" - which: "npm:^1.2.9" - checksum: 10c0/e05544722e9d7189b4292c66e42b7abeb21db0d07c91b785f4ae5fefceb1f89e626da2703744657b287e86dcd4af57b54567cef75159957ff7a8a761d9055012 - languageName: node - linkType: hard - -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3": +"cross-spawn@npm:^7.0.0": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -1816,66 +1416,6 @@ __metadata: languageName: node linkType: hard -"css-select@npm:^5.1.0": - version: 5.1.0 - resolution: "css-select@npm:5.1.0" - dependencies: - boolbase: "npm:^1.0.0" - css-what: "npm:^6.1.0" - domhandler: "npm:^5.0.2" - domutils: "npm:^3.0.1" - nth-check: "npm:^2.0.1" - checksum: 10c0/551c60dba5b54054741032c1793b5734f6ba45e23ae9e82761a3c0ed1acbb8cfedfa443aaba3a3c1a54cac12b456d2012a09d2cd5f0e82e430454c1b9d84d500 - languageName: node - linkType: hard - -"css-what@npm:^6.1.0": - version: 6.1.0 - resolution: "css-what@npm:6.1.0" - checksum: 10c0/a09f5a6b14ba8dcf57ae9a59474722e80f20406c53a61e9aedb0eedc693b135113ffe2983f4efc4b5065ae639442e9ae88df24941ef159c218b231011d733746 - languageName: node - linkType: hard - -"cssom@npm:^0.5.0": - version: 0.5.0 - resolution: "cssom@npm:0.5.0" - checksum: 10c0/8c4121c243baf0678c65dcac29b201ff0067dfecf978de9d5c83b2ff127a8fdefd2bfd54577f5ad8c80ed7d2c8b489ae01c82023545d010c4ecb87683fb403dd - languageName: node - linkType: hard - -"data-view-buffer@npm:^1.0.1": - version: 1.0.1 - resolution: "data-view-buffer@npm:1.0.1" - dependencies: - call-bind: "npm:^1.0.6" - es-errors: "npm:^1.3.0" - is-data-view: "npm:^1.0.1" - checksum: 10c0/8984119e59dbed906a11fcfb417d7d861936f16697a0e7216fe2c6c810f6b5e8f4a5281e73f2c28e8e9259027190ac4a33e2a65fdd7fa86ac06b76e838918583 - languageName: node - linkType: hard - -"data-view-byte-length@npm:^1.0.1": - version: 1.0.1 - resolution: "data-view-byte-length@npm:1.0.1" - dependencies: - call-bind: "npm:^1.0.7" - es-errors: "npm:^1.3.0" - is-data-view: "npm:^1.0.1" - checksum: 10c0/b7d9e48a0cf5aefed9ab7d123559917b2d7e0d65531f43b2fd95b9d3a6b46042dd3fca597c42bba384e66b70d7ad66ff23932f8367b241f53d93af42cfe04ec2 - languageName: node - linkType: hard - -"data-view-byte-offset@npm:^1.0.0": - version: 1.0.0 - resolution: "data-view-byte-offset@npm:1.0.0" - dependencies: - call-bind: "npm:^1.0.6" - es-errors: "npm:^1.3.0" - is-data-view: "npm:^1.0.1" - checksum: 10c0/21b0d2e53fd6e20cc4257c873bf6d36d77bd6185624b84076c0a1ddaa757b49aaf076254006341d35568e89f52eecd1ccb1a502cfb620f2beca04f48a6a62a8f - languageName: node - linkType: hard - "date-fns@npm:^2.30.0": version: 2.30.0 resolution: "date-fns@npm:2.30.0" @@ -1897,71 +1437,6 @@ __metadata: languageName: node linkType: hard -"deep-eql@npm:^4.1.3": - version: 4.1.4 - resolution: "deep-eql@npm:4.1.4" - dependencies: - type-detect: "npm:^4.0.0" - checksum: 10c0/264e0613493b43552fc908f4ff87b8b445c0e6e075656649600e1b8a17a57ee03e960156fce7177646e4d2ddaf8e5ee616d76bd79929ff593e5c79e4e5e6c517 - languageName: node - linkType: hard - -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": - version: 1.1.4 - resolution: "define-data-property@npm:1.1.4" - dependencies: - es-define-property: "npm:^1.0.0" - es-errors: "npm:^1.3.0" - gopd: "npm:^1.0.1" - checksum: 10c0/dea0606d1483eb9db8d930d4eac62ca0fa16738b0b3e07046cddfacf7d8c868bbe13fa0cb263eb91c7d0d527960dc3f2f2471a69ed7816210307f6744fe62e37 - languageName: node - linkType: hard - -"define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": - version: 1.2.1 - resolution: "define-properties@npm:1.2.1" - dependencies: - define-data-property: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.0" - object-keys: "npm:^1.1.1" - checksum: 10c0/88a152319ffe1396ccc6ded510a3896e77efac7a1bfbaa174a7b00414a1747377e0bb525d303794a47cf30e805c2ec84e575758512c6e44a993076d29fd4e6c3 - languageName: node - linkType: hard - -"diff-sequences@npm:^29.6.3": - version: 29.6.3 - resolution: "diff-sequences@npm:29.6.3" - checksum: 10c0/32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2 - languageName: node - linkType: hard - -"dom-serializer@npm:^2.0.0": - version: 2.0.0 - resolution: "dom-serializer@npm:2.0.0" - dependencies: - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.2" - entities: "npm:^4.2.0" - checksum: 10c0/d5ae2b7110ca3746b3643d3ef60ef823f5f078667baf530cec096433f1627ec4b6fa8c072f09d079d7cda915fd2c7bc1b7b935681e9b09e591e1e15f4040b8e2 - languageName: node - linkType: hard - -"domelementtype@npm:^2.3.0": - version: 2.3.0 - resolution: "domelementtype@npm:2.3.0" - checksum: 10c0/686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9 - languageName: node - linkType: hard - -"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": - version: 5.0.3 - resolution: "domhandler@npm:5.0.3" - dependencies: - domelementtype: "npm:^2.3.0" - checksum: 10c0/bba1e5932b3e196ad6862286d76adc89a0dbf0c773e5ced1eb01f9af930c50093a084eff14b8de5ea60b895c56a04d5de8bbc4930c5543d029091916770b2d2a - languageName: node - linkType: hard - "dompurify@npm:^3.1.4": version: 3.1.6 resolution: "dompurify@npm:3.1.6" @@ -1969,18 +1444,7 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^3.0.1, domutils@npm:^3.1.0": - version: 3.1.0 - resolution: "domutils@npm:3.1.0" - dependencies: - dom-serializer: "npm:^2.0.0" - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.3" - checksum: 10c0/342d64cf4d07b8a0573fb51e0a6312a88fb520c7fefd751870bf72fa5fc0f2e0cb9a3958a573610b1d608c6e2a69b8e9b4b40f0bfb8f87a71bce4f180cca1887 - languageName: node - linkType: hard - -"dotenv@npm:^16.3.1, dotenv@npm:^16.4.5": +"dotenv@npm:^16.3.1": version: 16.4.5 resolution: "dotenv@npm:16.4.5" checksum: 10c0/48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f @@ -2024,13 +1488,6 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.2.0, entities@npm:^4.5.0": - version: 4.5.0 - resolution: "entities@npm:4.5.0" - checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250 - languageName: node - linkType: hard - "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -2045,116 +1502,6 @@ __metadata: languageName: node linkType: hard -"error-ex@npm:^1.3.1": - version: 1.3.2 - resolution: "error-ex@npm:1.3.2" - dependencies: - is-arrayish: "npm:^0.2.1" - checksum: 10c0/ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce - languageName: node - linkType: hard - -"es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2": - version: 1.23.3 - resolution: "es-abstract@npm:1.23.3" - dependencies: - array-buffer-byte-length: "npm:^1.0.1" - arraybuffer.prototype.slice: "npm:^1.0.3" - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.7" - data-view-buffer: "npm:^1.0.1" - data-view-byte-length: "npm:^1.0.1" - data-view-byte-offset: "npm:^1.0.0" - es-define-property: "npm:^1.0.0" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-set-tostringtag: "npm:^2.0.3" - es-to-primitive: "npm:^1.2.1" - function.prototype.name: "npm:^1.1.6" - get-intrinsic: "npm:^1.2.4" - get-symbol-description: "npm:^1.0.2" - globalthis: "npm:^1.0.3" - gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.2" - has-proto: "npm:^1.0.3" - has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.2" - internal-slot: "npm:^1.0.7" - is-array-buffer: "npm:^3.0.4" - is-callable: "npm:^1.2.7" - is-data-view: "npm:^1.0.1" - is-negative-zero: "npm:^2.0.3" - is-regex: "npm:^1.1.4" - is-shared-array-buffer: "npm:^1.0.3" - is-string: "npm:^1.0.7" - is-typed-array: "npm:^1.1.13" - is-weakref: "npm:^1.0.2" - object-inspect: "npm:^1.13.1" - object-keys: "npm:^1.1.1" - object.assign: "npm:^4.1.5" - regexp.prototype.flags: "npm:^1.5.2" - safe-array-concat: "npm:^1.1.2" - safe-regex-test: "npm:^1.0.3" - string.prototype.trim: "npm:^1.2.9" - string.prototype.trimend: "npm:^1.0.8" - string.prototype.trimstart: "npm:^1.0.8" - typed-array-buffer: "npm:^1.0.2" - typed-array-byte-length: "npm:^1.0.1" - typed-array-byte-offset: "npm:^1.0.2" - typed-array-length: "npm:^1.0.6" - unbox-primitive: "npm:^1.0.2" - which-typed-array: "npm:^1.1.15" - checksum: 10c0/d27e9afafb225c6924bee9971a7f25f20c314f2d6cb93a63cada4ac11dcf42040896a6c22e5fb8f2a10767055ed4ddf400be3b1eb12297d281726de470b75666 - languageName: node - linkType: hard - -"es-define-property@npm:^1.0.0": - version: 1.0.0 - resolution: "es-define-property@npm:1.0.0" - dependencies: - get-intrinsic: "npm:^1.2.4" - checksum: 10c0/6bf3191feb7ea2ebda48b577f69bdfac7a2b3c9bcf97307f55fd6ef1bbca0b49f0c219a935aca506c993d8c5d8bddd937766cb760cd5e5a1071351f2df9f9aa4 - languageName: node - linkType: hard - -"es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": - version: 1.3.0 - resolution: "es-errors@npm:1.3.0" - checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 - languageName: node - linkType: hard - -"es-object-atoms@npm:^1.0.0": - version: 1.0.0 - resolution: "es-object-atoms@npm:1.0.0" - dependencies: - es-errors: "npm:^1.3.0" - checksum: 10c0/1fed3d102eb27ab8d983337bb7c8b159dd2a1e63ff833ec54eea1311c96d5b08223b433060ba240541ca8adba9eee6b0a60cdbf2f80634b784febc9cc8b687b4 - languageName: node - linkType: hard - -"es-set-tostringtag@npm:^2.0.3": - version: 2.0.3 - resolution: "es-set-tostringtag@npm:2.0.3" - dependencies: - get-intrinsic: "npm:^1.2.4" - has-tostringtag: "npm:^1.0.2" - hasown: "npm:^2.0.1" - checksum: 10c0/f22aff1585eb33569c326323f0b0d175844a1f11618b86e193b386f8be0ea9474cfbe46df39c45d959f7aa8f6c06985dc51dd6bce5401645ec5a74c4ceaa836a - languageName: node - linkType: hard - -"es-to-primitive@npm:^1.2.1": - version: 1.2.1 - resolution: "es-to-primitive@npm:1.2.1" - dependencies: - is-callable: "npm:^1.1.4" - is-date-object: "npm:^1.0.1" - is-symbol: "npm:^1.0.2" - checksum: 10c0/0886572b8dc075cb10e50c0af62a03d03a68e1e69c388bd4f10c0649ee41b1fbb24840a1b7e590b393011b5cdbe0144b776da316762653685432df37d6de60f1 - languageName: node - linkType: hard - "esbuild@npm:^0.18.10": version: 0.18.20 resolution: "esbuild@npm:0.18.20" @@ -2232,86 +1579,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.20.2": - version: 0.20.2 - resolution: "esbuild@npm:0.20.2" - dependencies: - "@esbuild/aix-ppc64": "npm:0.20.2" - "@esbuild/android-arm": "npm:0.20.2" - "@esbuild/android-arm64": "npm:0.20.2" - "@esbuild/android-x64": "npm:0.20.2" - "@esbuild/darwin-arm64": "npm:0.20.2" - "@esbuild/darwin-x64": "npm:0.20.2" - "@esbuild/freebsd-arm64": "npm:0.20.2" - "@esbuild/freebsd-x64": "npm:0.20.2" - "@esbuild/linux-arm": "npm:0.20.2" - "@esbuild/linux-arm64": "npm:0.20.2" - "@esbuild/linux-ia32": "npm:0.20.2" - "@esbuild/linux-loong64": "npm:0.20.2" - "@esbuild/linux-mips64el": "npm:0.20.2" - "@esbuild/linux-ppc64": "npm:0.20.2" - "@esbuild/linux-riscv64": "npm:0.20.2" - "@esbuild/linux-s390x": "npm:0.20.2" - "@esbuild/linux-x64": "npm:0.20.2" - "@esbuild/netbsd-x64": "npm:0.20.2" - "@esbuild/openbsd-x64": "npm:0.20.2" - "@esbuild/sunos-x64": "npm:0.20.2" - "@esbuild/win32-arm64": "npm:0.20.2" - "@esbuild/win32-ia32": "npm:0.20.2" - "@esbuild/win32-x64": "npm:0.20.2" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10c0/66398f9fb2c65e456a3e649747b39af8a001e47963b25e86d9c09d2a48d61aa641b27da0ce5cad63df95ad246105e1d83e7fee0e1e22a0663def73b1c5101112 - languageName: node - linkType: hard - "esbuild@npm:^0.21.3": version: 0.21.5 resolution: "esbuild@npm:0.21.5" @@ -2406,32 +1673,6 @@ __metadata: languageName: node linkType: hard -"estree-walker@npm:^3.0.3": - version: 3.0.3 - resolution: "estree-walker@npm:3.0.3" - dependencies: - "@types/estree": "npm:^1.0.0" - checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d - languageName: node - linkType: hard - -"execa@npm:^8.0.1": - version: 8.0.1 - resolution: "execa@npm:8.0.1" - dependencies: - cross-spawn: "npm:^7.0.3" - get-stream: "npm:^8.0.1" - human-signals: "npm:^5.0.0" - is-stream: "npm:^3.0.0" - merge-stream: "npm:^2.0.0" - npm-run-path: "npm:^5.1.0" - onetime: "npm:^6.0.0" - signal-exit: "npm:^4.1.0" - strip-final-newline: "npm:^3.0.0" - checksum: 10c0/2c52d8775f5bf103ce8eec9c7ab3059909ba350a5164744e9947ed14a53f51687c040a250bda833f906d1283aa8803975b84e6c8f7a7c42f99dc8ef80250d1af - languageName: node - linkType: hard - "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -2458,15 +1699,6 @@ __metadata: languageName: node linkType: hard -"for-each@npm:^0.3.3": - version: 0.3.3 - resolution: "for-each@npm:0.3.3" - dependencies: - is-callable: "npm:^1.1.3" - checksum: 10c0/22330d8a2db728dbf003ec9182c2d421fbcd2969b02b4f97ec288721cda63eb28f2c08585ddccd0f77cb2930af8d958005c9e72f47141dc51816127a118f39aa - languageName: node - linkType: hard - "foreground-child@npm:^3.1.0": version: 3.2.1 resolution: "foreground-child@npm:3.2.1" @@ -2481,21 +1713,17 @@ __metadata: version: 0.0.0-use.local resolution: "frontend@workspace:apps/frontend" dependencies: - "@chouqueth/gleam": "npm:^1.4.1" + "@chouqueth/gleam": "npm:^1.6.2" "@gleam-lang/highlight.js-gleam": "npm:^1.5.0" "@sentry/browser": "npm:^8.0.0" "@sentry/vite-plugin": "npm:^2.16.1" "@trivago/prettier-plugin-sort-imports": "npm:^4.3.0" - "@types/dompurify": "npm:^3.0.5" chart.js: "npm:^4.4.4" dompurify: "npm:^3.1.4" - dotenv: "npm:^16.4.5" highlight.js: "npm:^11.9.0" marked: "npm:^12.0.2" marked-highlight: "npm:^2.1.1" prettier: "npm:^3.2.5" - ts-gleam: "npm:^1.0.1" - typescript: "npm:^5.4.2" vite: "npm:^5.1.6" vite-gleam: "npm:^0.4.0" languageName: unknown @@ -2545,32 +1773,6 @@ __metadata: languageName: node linkType: hard -"function-bind@npm:^1.1.2": - version: 1.1.2 - resolution: "function-bind@npm:1.1.2" - checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 - languageName: node - linkType: hard - -"function.prototype.name@npm:^1.1.6": - version: 1.1.6 - resolution: "function.prototype.name@npm:1.1.6" - dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - functions-have-names: "npm:^1.2.3" - checksum: 10c0/9eae11294905b62cb16874adb4fc687927cda3162285e0ad9612e6a1d04934005d46907362ea9cdb7428edce05a2f2c3dabc3b2d21e9fd343e9bb278230ad94b - languageName: node - linkType: hard - -"functions-have-names@npm:^1.2.3": - version: 1.2.3 - resolution: "functions-have-names@npm:1.2.3" - checksum: 10c0/33e77fd29bddc2d9bb78ab3eb854c165909201f88c75faa8272e35899e2d35a8a642a15e7420ef945e1f64a9670d6aa3ec744106b2aa42be68ca5114025954ca - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -2585,44 +1787,6 @@ __metadata: languageName: node linkType: hard -"get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": - version: 2.0.2 - resolution: "get-func-name@npm:2.0.2" - checksum: 10c0/89830fd07623fa73429a711b9daecdb304386d237c71268007f788f113505ef1d4cc2d0b9680e072c5082490aec9df5d7758bf5ac6f1c37062855e8e3dc0b9df - languageName: node - linkType: hard - -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4": - version: 1.2.4 - resolution: "get-intrinsic@npm:1.2.4" - dependencies: - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - has-proto: "npm:^1.0.1" - has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.0" - checksum: 10c0/0a9b82c16696ed6da5e39b1267104475c47e3a9bdbe8b509dfe1710946e38a87be70d759f4bb3cda042d76a41ef47fe769660f3b7c0d1f68750299344ffb15b7 - languageName: node - linkType: hard - -"get-stream@npm:^8.0.1": - version: 8.0.1 - resolution: "get-stream@npm:8.0.1" - checksum: 10c0/5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290 - languageName: node - linkType: hard - -"get-symbol-description@npm:^1.0.2": - version: 1.0.2 - resolution: "get-symbol-description@npm:1.0.2" - dependencies: - call-bind: "npm:^1.0.5" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.4" - checksum: 10c0/867be6d63f5e0eb026cb3b0ef695ec9ecf9310febb041072d2e142f260bd91ced9eeb426b3af98791d1064e324e653424afa6fd1af17dee373bea48ae03162bc - languageName: node - linkType: hard - "glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -2667,16 +1831,6 @@ __metadata: languageName: node linkType: hard -"globalthis@npm:^1.0.3": - version: 1.0.4 - resolution: "globalthis@npm:1.0.4" - dependencies: - define-properties: "npm:^1.2.1" - gopd: "npm:^1.0.1" - checksum: 10c0/9d156f313af79d80b1566b93e19285f481c591ad6d0d319b4be5e03750d004dde40a39a0f26f7e635f9007a3600802f53ecd85a759b86f109e80a5f705e01846 - languageName: node - linkType: hard - "gloogle@workspace:.": version: 0.0.0-use.local resolution: "gloogle@workspace:." @@ -2688,29 +1842,13 @@ __metadata: languageName: unknown linkType: soft -"gopd@npm:^1.0.1": - version: 1.0.1 - resolution: "gopd@npm:1.0.1" - dependencies: - get-intrinsic: "npm:^1.1.3" - checksum: 10c0/505c05487f7944c552cee72087bf1567debb470d4355b1335f2c262d218ebbff805cd3715448fe29b4b380bae6912561d0467233e4165830efd28da241418c63 - languageName: node - linkType: hard - -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 languageName: node linkType: hard -"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": - version: 1.0.2 - resolution: "has-bigints@npm:1.0.2" - checksum: 10c0/724eb1485bfa3cdff6f18d95130aa190561f00b3fcf9f19dc640baf8176b5917c143b81ec2123f8cddb6c05164a198c94b13e1377c497705ccc8e1a80306e83b - languageName: node - linkType: hard - "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" @@ -2725,47 +1863,6 @@ __metadata: languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": - version: 1.0.2 - resolution: "has-property-descriptors@npm:1.0.2" - dependencies: - es-define-property: "npm:^1.0.0" - checksum: 10c0/253c1f59e80bb476cf0dde8ff5284505d90c3bdb762983c3514d36414290475fe3fd6f574929d84de2a8eec00d35cf07cb6776205ff32efd7c50719125f00236 - languageName: node - linkType: hard - -"has-proto@npm:^1.0.1, has-proto@npm:^1.0.3": - version: 1.0.3 - resolution: "has-proto@npm:1.0.3" - checksum: 10c0/35a6989f81e9f8022c2f4027f8b48a552de714938765d019dbea6bb547bd49ce5010a3c7c32ec6ddac6e48fc546166a3583b128f5a7add8b058a6d8b4afec205 - languageName: node - linkType: hard - -"has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3": - version: 1.0.3 - resolution: "has-symbols@npm:1.0.3" - checksum: 10c0/e6922b4345a3f37069cdfe8600febbca791c94988c01af3394d86ca3360b4b93928bbf395859158f88099cb10b19d98e3bbab7c9ff2c1bd09cf665ee90afa2c3 - languageName: node - linkType: hard - -"has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.2": - version: 1.0.2 - resolution: "has-tostringtag@npm:1.0.2" - dependencies: - has-symbols: "npm:^1.0.3" - checksum: 10c0/a8b166462192bafe3d9b6e420a1d581d93dd867adb61be223a17a8d6dad147aa77a8be32c961bb2f27b3ef893cae8d36f564ab651f5e9b7938ae86f74027c48c - languageName: node - linkType: hard - -"hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2": - version: 2.0.2 - resolution: "hasown@npm:2.0.2" - dependencies: - function-bind: "npm:^1.1.2" - checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9 - languageName: node - linkType: hard - "highlight.js@npm:^11.9.0": version: 11.10.0 resolution: "highlight.js@npm:11.10.0" @@ -2773,32 +1870,6 @@ __metadata: languageName: node linkType: hard -"hosted-git-info@npm:^2.1.4": - version: 2.8.9 - resolution: "hosted-git-info@npm:2.8.9" - checksum: 10c0/317cbc6b1bbbe23c2a40ae23f3dafe9fa349ce42a89a36f930e3f9c0530c179a3882d2ef1e4141a4c3674d6faaea862138ec55b43ad6f75e387fda2483a13c70 - languageName: node - linkType: hard - -"html-escaper@npm:^3.0.3": - version: 3.0.3 - resolution: "html-escaper@npm:3.0.3" - checksum: 10c0/a042fa4139127ff7546513e90ea39cc9161a1938ce90122dbc4260d4b7252c9aa8452f4509c0c2889901b8ae9a8699179150f1f99d3f80bcf7317573c5f08f4e - languageName: node - linkType: hard - -"htmlparser2@npm:^9.1.0": - version: 9.1.0 - resolution: "htmlparser2@npm:9.1.0" - dependencies: - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.3" - domutils: "npm:^3.1.0" - entities: "npm:^4.5.0" - checksum: 10c0/394f6323efc265bbc791d8c0d96bfe95984e0407565248521ab92e2dc7668e5ceeca7bc6ed18d408b9ee3b25032c5743368a4280d280332d782821d5d467ad8f - languageName: node - linkType: hard - "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -2836,13 +1907,6 @@ __metadata: languageName: node linkType: hard -"human-signals@npm:^5.0.0": - version: 5.0.0 - resolution: "human-signals@npm:5.0.0" - checksum: 10c0/5a9359073fe17a8b58e5a085e9a39a950366d9f00217c4ff5878bd312e09d80f460536ea6a3f260b5943a01fe55c158d1cea3fc7bee3d0520aeef04f6d915c82 - languageName: node - linkType: hard - "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -2866,17 +1930,6 @@ __metadata: languageName: node linkType: hard -"internal-slot@npm:^1.0.7": - version: 1.0.7 - resolution: "internal-slot@npm:1.0.7" - dependencies: - es-errors: "npm:^1.3.0" - hasown: "npm:^2.0.0" - side-channel: "npm:^1.0.4" - checksum: 10c0/f8b294a4e6ea3855fc59551bbf35f2b832cf01fd5e6e2a97f5c201a071cc09b49048f856e484b67a6c721da5e55736c5b6ddafaf19e2dbeb4a3ff1821680de6c - languageName: node - linkType: hard - "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -2887,32 +1940,6 @@ __metadata: languageName: node linkType: hard -"is-array-buffer@npm:^3.0.4": - version: 3.0.4 - resolution: "is-array-buffer@npm:3.0.4" - dependencies: - call-bind: "npm:^1.0.2" - get-intrinsic: "npm:^1.2.1" - checksum: 10c0/42a49d006cc6130bc5424eae113e948c146f31f9d24460fc0958f855d9d810e6fd2e4519bf19aab75179af9c298ea6092459d8cafdec523cd19e529b26eab860 - languageName: node - linkType: hard - -"is-arrayish@npm:^0.2.1": - version: 0.2.1 - resolution: "is-arrayish@npm:0.2.1" - checksum: 10c0/e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729 - languageName: node - linkType: hard - -"is-bigint@npm:^1.0.1": - version: 1.0.4 - resolution: "is-bigint@npm:1.0.4" - dependencies: - has-bigints: "npm:^1.0.1" - checksum: 10c0/eb9c88e418a0d195ca545aff2b715c9903d9b0a5033bc5922fec600eb0c3d7b1ee7f882dbf2e0d5a6e694e42391be3683e4368737bd3c4a77f8ac293e7773696 - languageName: node - linkType: hard - "is-binary-path@npm:~2.1.0": version: 2.1.0 resolution: "is-binary-path@npm:2.1.0" @@ -2922,50 +1949,6 @@ __metadata: languageName: node linkType: hard -"is-boolean-object@npm:^1.1.0": - version: 1.1.2 - resolution: "is-boolean-object@npm:1.1.2" - dependencies: - call-bind: "npm:^1.0.2" - has-tostringtag: "npm:^1.0.0" - checksum: 10c0/6090587f8a8a8534c0f816da868bc94f32810f08807aa72fa7e79f7e11c466d281486ffe7a788178809c2aa71fe3e700b167fe80dd96dad68026bfff8ebf39f7 - languageName: node - linkType: hard - -"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": - version: 1.2.7 - resolution: "is-callable@npm:1.2.7" - checksum: 10c0/ceebaeb9d92e8adee604076971dd6000d38d6afc40bb843ea8e45c5579b57671c3f3b50d7f04869618242c6cee08d1b67806a8cb8edaaaf7c0748b3720d6066f - languageName: node - linkType: hard - -"is-core-module@npm:^2.13.0": - version: 2.14.0 - resolution: "is-core-module@npm:2.14.0" - dependencies: - hasown: "npm:^2.0.2" - checksum: 10c0/ae8dbc82bd20426558bc8d20ce290ce301c1cfd6ae4446266d10cacff4c63c67ab16440ade1d72ced9ec41c569fbacbcee01e293782ce568527c4cdf35936e4c - languageName: node - linkType: hard - -"is-data-view@npm:^1.0.1": - version: 1.0.1 - resolution: "is-data-view@npm:1.0.1" - dependencies: - is-typed-array: "npm:^1.1.13" - checksum: 10c0/a3e6ec84efe303da859107aed9b970e018e2bee7ffcb48e2f8096921a493608134240e672a2072577e5f23a729846241d9634806e8a0e51d9129c56d5f65442d - languageName: node - linkType: hard - -"is-date-object@npm:^1.0.1": - version: 1.0.5 - resolution: "is-date-object@npm:1.0.5" - dependencies: - has-tostringtag: "npm:^1.0.0" - checksum: 10c0/eed21e5dcc619c48ccef804dfc83a739dbb2abee6ca202838ee1bd5f760fe8d8a93444f0d49012ad19bb7c006186e2884a1b92f6e1c056da7fd23d0a9ad5992e - languageName: node - linkType: hard - "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -2996,22 +1979,6 @@ __metadata: languageName: node linkType: hard -"is-negative-zero@npm:^2.0.3": - version: 2.0.3 - resolution: "is-negative-zero@npm:2.0.3" - checksum: 10c0/bcdcf6b8b9714063ffcfa9929c575ac69bfdabb8f4574ff557dfc086df2836cf07e3906f5bbc4f2a5c12f8f3ba56af640c843cdfc74da8caed86c7c7d66fd08e - languageName: node - linkType: hard - -"is-number-object@npm:^1.0.4": - version: 1.0.7 - resolution: "is-number-object@npm:1.0.7" - dependencies: - has-tostringtag: "npm:^1.0.0" - checksum: 10c0/aad266da1e530f1804a2b7bd2e874b4869f71c98590b3964f9d06cc9869b18f8d1f4778f838ecd2a11011bce20aeecb53cb269ba916209b79c24580416b74b1b - languageName: node - linkType: hard - "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -3019,75 +1986,6 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.1.4": - version: 1.1.4 - resolution: "is-regex@npm:1.1.4" - dependencies: - call-bind: "npm:^1.0.2" - has-tostringtag: "npm:^1.0.0" - checksum: 10c0/bb72aae604a69eafd4a82a93002058c416ace8cde95873589a97fc5dac96a6c6c78a9977d487b7b95426a8f5073969124dd228f043f9f604f041f32fcc465fc1 - languageName: node - linkType: hard - -"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.3": - version: 1.0.3 - resolution: "is-shared-array-buffer@npm:1.0.3" - dependencies: - call-bind: "npm:^1.0.7" - checksum: 10c0/adc11ab0acbc934a7b9e5e9d6c588d4ec6682f6fea8cda5180721704fa32927582ede5b123349e32517fdadd07958973d24716c80e7ab198970c47acc09e59c7 - languageName: node - linkType: hard - -"is-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "is-stream@npm:3.0.0" - checksum: 10c0/eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8 - languageName: node - linkType: hard - -"is-string@npm:^1.0.5, is-string@npm:^1.0.7": - version: 1.0.7 - resolution: "is-string@npm:1.0.7" - dependencies: - has-tostringtag: "npm:^1.0.0" - checksum: 10c0/905f805cbc6eedfa678aaa103ab7f626aac9ebbdc8737abb5243acaa61d9820f8edc5819106b8fcd1839e33db21de9f0116ae20de380c8382d16dc2a601921f6 - languageName: node - linkType: hard - -"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3": - version: 1.0.4 - resolution: "is-symbol@npm:1.0.4" - dependencies: - has-symbols: "npm:^1.0.2" - checksum: 10c0/9381dd015f7c8906154dbcbf93fad769de16b4b961edc94f88d26eb8c555935caa23af88bda0c93a18e65560f6d7cca0fd5a3f8a8e1df6f1abbb9bead4502ef7 - languageName: node - linkType: hard - -"is-typed-array@npm:^1.1.13": - version: 1.1.13 - resolution: "is-typed-array@npm:1.1.13" - dependencies: - which-typed-array: "npm:^1.1.14" - checksum: 10c0/fa5cb97d4a80e52c2cc8ed3778e39f175a1a2ae4ddf3adae3187d69586a1fd57cfa0b095db31f66aa90331e9e3da79184cea9c6abdcd1abc722dc3c3edd51cca - languageName: node - linkType: hard - -"is-weakref@npm:^1.0.2": - version: 1.0.2 - resolution: "is-weakref@npm:1.0.2" - dependencies: - call-bind: "npm:^1.0.2" - checksum: 10c0/1545c5d172cb690c392f2136c23eec07d8d78a7f57d0e41f10078aa4f5daf5d7f57b6513a67514ab4f073275ad00c9822fc8935e00229d0a2089e1c02685d4b1 - languageName: node - linkType: hard - -"isarray@npm:^2.0.5": - version: 2.0.5 - resolution: "isarray@npm:2.0.5" - checksum: 10c0/4199f14a7a13da2177c66c31080008b7124331956f47bca57dd0b6ea9f11687aa25e565a2c7a2b519bc86988d10398e3049a1f5df13c9f6b7664154690ae79fd - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -3129,13 +2027,6 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^9.0.0": - version: 9.0.0 - resolution: "js-tokens@npm:9.0.0" - checksum: 10c0/4ad1c12f47b8c8b2a3a99e29ef338c1385c7b7442198a425f3463f3537384dab6032012791bfc2f056ea5ecdb06b1ed4f70e11a3ab3f388d3dcebfe16a52b27d - languageName: node - linkType: hard - "jsbn@npm:1.1.0": version: 1.1.0 resolution: "jsbn@npm:1.1.0" @@ -3152,13 +2043,6 @@ __metadata: languageName: node linkType: hard -"json-parse-better-errors@npm:^1.0.1": - version: 1.0.2 - resolution: "json-parse-better-errors@npm:1.0.2" - checksum: 10c0/2f1287a7c833e397c9ddd361a78638e828fc523038bb3441fd4fc144cfd2c6cd4963ffb9e207e648cf7b692600f1e1e524e965c32df5152120910e4903a47dcb - languageName: node - linkType: hard - "json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" @@ -3168,41 +2052,6 @@ __metadata: languageName: node linkType: hard -"linkedom@npm:^0.16.11": - version: 0.16.11 - resolution: "linkedom@npm:0.16.11" - dependencies: - css-select: "npm:^5.1.0" - cssom: "npm:^0.5.0" - html-escaper: "npm:^3.0.3" - htmlparser2: "npm:^9.1.0" - uhyphen: "npm:^0.2.0" - checksum: 10c0/6a8928ac91f84caa2c4ba6253989f42521139a5c5a3e386df5fc259ccd256cfd3eb1a6009afd74dbef64133358dc1c99ab181f909567f9f04d00eb260c9fea84 - languageName: node - linkType: hard - -"load-json-file@npm:^4.0.0": - version: 4.0.0 - resolution: "load-json-file@npm:4.0.0" - dependencies: - graceful-fs: "npm:^4.1.2" - parse-json: "npm:^4.0.0" - pify: "npm:^3.0.0" - strip-bom: "npm:^3.0.0" - checksum: 10c0/6b48f6a0256bdfcc8970be2c57f68f10acb2ee7e63709b386b2febb6ad3c86198f840889cdbe71d28f741cbaa2f23a7771206b138cd1bdd159564511ca37c1d5 - languageName: node - linkType: hard - -"local-pkg@npm:^0.5.0": - version: 0.5.0 - resolution: "local-pkg@npm:0.5.0" - dependencies: - mlly: "npm:^1.4.2" - pkg-types: "npm:^1.0.3" - checksum: 10c0/f61cbd00d7689f275558b1a45c7ff2a3ddf8472654123ed880215677b9adfa729f1081e50c27ffb415cdb9fa706fb755fec5e23cdd965be375c8059e87ff1cc9 - languageName: node - linkType: hard - "locate-path@npm:^6.0.0": version: 6.0.0 resolution: "locate-path@npm:6.0.0" @@ -3219,15 +2068,6 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^2.3.6, loupe@npm:^2.3.7": - version: 2.3.7 - resolution: "loupe@npm:2.3.7" - dependencies: - get-func-name: "npm:^2.0.1" - checksum: 10c0/71a781c8fc21527b99ed1062043f1f2bb30bdaf54fa4cf92463427e1718bc6567af2988300bc243c1f276e4f0876f29e3cbf7b58106fdc186915687456ce5bf4 - languageName: node - linkType: hard - "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -3244,18 +2084,6 @@ __metadata: languageName: node linkType: hard -"lustre-client-test@workspace:packages/lustre": - version: 0.0.0-use.local - resolution: "lustre-client-test@workspace:packages/lustre" - dependencies: - "@chouqueth/gleam": "npm:^1.3.2" - esbuild: "npm:^0.20.2" - linkedom: "npm:^0.16.11" - npm-run-all: "npm:^4.1.5" - vitest: "npm:^1.5.0" - languageName: unknown - linkType: soft - "magic-string@npm:0.30.8": version: 0.30.8 resolution: "magic-string@npm:0.30.8" @@ -3265,7 +2093,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.2, magic-string@npm:^0.30.5": +"magic-string@npm:^0.30.2": version: 0.30.10 resolution: "magic-string@npm:0.30.10" dependencies: @@ -3312,36 +2140,6 @@ __metadata: languageName: node linkType: hard -"memorystream@npm:^0.3.1": - version: 0.3.1 - resolution: "memorystream@npm:0.3.1" - checksum: 10c0/4bd164657711d9747ff5edb0508b2944414da3464b7fe21ac5c67cf35bba975c4b446a0124bd0f9a8be54cfc18faf92e92bd77563a20328b1ccf2ff04e9f39b9 - languageName: node - linkType: hard - -"merge-stream@npm:^2.0.0": - version: 2.0.0 - resolution: "merge-stream@npm:2.0.0" - checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5 - languageName: node - linkType: hard - -"mimic-fn@npm:^4.0.0": - version: 4.0.0 - resolution: "mimic-fn@npm:4.0.0" - checksum: 10c0/de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf - languageName: node - linkType: hard - -"minimatch@npm:^3.0.4": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 - languageName: node - linkType: hard - "minimatch@npm:^8.0.2": version: 8.0.4 resolution: "minimatch@npm:8.0.4" @@ -3479,18 +2277,6 @@ __metadata: languageName: node linkType: hard -"mlly@npm:^1.4.2, mlly@npm:^1.7.1": - version: 1.7.1 - resolution: "mlly@npm:1.7.1" - dependencies: - acorn: "npm:^8.11.3" - pathe: "npm:^1.1.2" - pkg-types: "npm:^1.1.1" - ufo: "npm:^1.5.3" - checksum: 10c0/d836a7b0adff4d118af41fb93ad4d9e57f80e694a681185280ba220a4607603c19e86c80f9a6c57512b04280567f2599e3386081705c5b5fd74c9ddfd571d0fa - languageName: node - linkType: hard - "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -3514,13 +2300,6 @@ __metadata: languageName: node linkType: hard -"nice-try@npm:^1.0.4": - version: 1.0.5 - resolution: "nice-try@npm:1.0.5" - checksum: 10c0/95568c1b73e1d0d4069a3e3061a2102d854513d37bcfda73300015b7ba4868d3b27c198d1dbbd8ebdef4112fc2ed9e895d4a0f2e1cce0bd334f2a1346dc9205f - languageName: node - linkType: hard - "node-fetch@npm:^2.6.7": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -3573,18 +2352,6 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^2.3.2": - version: 2.5.0 - resolution: "normalize-package-data@npm:2.5.0" - dependencies: - hosted-git-info: "npm:^2.1.4" - resolve: "npm:^1.10.0" - semver: "npm:2 || 3 || 4 || 5" - validate-npm-package-license: "npm:^3.0.1" - checksum: 10c0/357cb1646deb42f8eb4c7d42c4edf0eec312f3628c2ef98501963cc4bbe7277021b2b1d977f982b2edce78f5a1014613ce9cf38085c3df2d76730481357ca504 - languageName: node - linkType: hard - "normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" @@ -3592,80 +2359,6 @@ __metadata: languageName: node linkType: hard -"npm-run-all@npm:^4.1.5": - version: 4.1.5 - resolution: "npm-run-all@npm:4.1.5" - dependencies: - ansi-styles: "npm:^3.2.1" - chalk: "npm:^2.4.1" - cross-spawn: "npm:^6.0.5" - memorystream: "npm:^0.3.1" - minimatch: "npm:^3.0.4" - pidtree: "npm:^0.3.0" - read-pkg: "npm:^3.0.0" - shell-quote: "npm:^1.6.1" - string.prototype.padend: "npm:^3.0.0" - bin: - npm-run-all: bin/npm-run-all/index.js - run-p: bin/run-p/index.js - run-s: bin/run-s/index.js - checksum: 10c0/736ee39bd35454d3efaa4a2e53eba6c523e2e17fba21a18edcce6b221f5cab62000bef16bb6ae8aff9e615831e6b0eb25ab51d52d60e6fa6f4ea880e4c6d31f4 - languageName: node - linkType: hard - -"npm-run-path@npm:^5.1.0": - version: 5.3.0 - resolution: "npm-run-path@npm:5.3.0" - dependencies: - path-key: "npm:^4.0.0" - checksum: 10c0/124df74820c40c2eb9a8612a254ea1d557ddfab1581c3e751f825e3e366d9f00b0d76a3c94ecd8398e7f3eee193018622677e95816e8491f0797b21e30b2deba - languageName: node - linkType: hard - -"nth-check@npm:^2.0.1": - version: 2.1.1 - resolution: "nth-check@npm:2.1.1" - dependencies: - boolbase: "npm:^1.0.0" - checksum: 10c0/5fee7ff309727763689cfad844d979aedd2204a817fbaaf0e1603794a7c20db28548d7b024692f953557df6ce4a0ee4ae46cd8ebd9b36cfb300b9226b567c479 - languageName: node - linkType: hard - -"object-inspect@npm:^1.13.1": - version: 1.13.2 - resolution: "object-inspect@npm:1.13.2" - checksum: 10c0/b97835b4c91ec37b5fd71add84f21c3f1047d1d155d00c0fcd6699516c256d4fcc6ff17a1aced873197fe447f91a3964178fd2a67a1ee2120cdaf60e81a050b4 - languageName: node - linkType: hard - -"object-keys@npm:^1.1.1": - version: 1.1.1 - resolution: "object-keys@npm:1.1.1" - checksum: 10c0/b11f7ccdbc6d406d1f186cdadb9d54738e347b2692a14439ca5ac70c225fa6db46db809711b78589866d47b25fc3e8dee0b4c722ac751e11180f9380e3d8601d - languageName: node - linkType: hard - -"object.assign@npm:^4.1.5": - version: 4.1.5 - resolution: "object.assign@npm:4.1.5" - dependencies: - call-bind: "npm:^1.0.5" - define-properties: "npm:^1.2.1" - has-symbols: "npm:^1.0.3" - object-keys: "npm:^1.1.1" - checksum: 10c0/60108e1fa2706f22554a4648299b0955236c62b3685c52abf4988d14fffb0e7731e00aa8c6448397e3eb63d087dcc124a9f21e1980f36d0b2667f3c18bacd469 - languageName: node - linkType: hard - -"onetime@npm:^6.0.0": - version: 6.0.0 - resolution: "onetime@npm:6.0.0" - dependencies: - mimic-fn: "npm:^4.0.0" - checksum: 10c0/4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c - languageName: node - linkType: hard - "p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" @@ -3675,15 +2368,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^5.0.0": - version: 5.0.0 - resolution: "p-limit@npm:5.0.0" - dependencies: - yocto-queue: "npm:^1.0.0" - checksum: 10c0/574e93b8895a26e8485eb1df7c4b58a1a6e8d8ae41b1750cc2cc440922b3d306044fc6e9a7f74578a883d46802d9db72b30f2e612690fcef838c173261b1ed83 - languageName: node - linkType: hard - "p-locate@npm:^5.0.0": version: 5.0.0 resolution: "p-locate@npm:5.0.0" @@ -3709,16 +2393,6 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^4.0.0": - version: 4.0.0 - resolution: "parse-json@npm:4.0.0" - dependencies: - error-ex: "npm:^1.3.1" - json-parse-better-errors: "npm:^1.0.1" - checksum: 10c0/8d80790b772ccb1bcea4e09e2697555e519d83d04a77c2b4237389b813f82898943a93ffff7d0d2406203bdd0c30dcf95b1661e3a53f83d0e417f053957bef32 - languageName: node - linkType: hard - "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -3726,13 +2400,6 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^2.0.1": - version: 2.0.1 - resolution: "path-key@npm:2.0.1" - checksum: 10c0/dd2044f029a8e58ac31d2bf34c34b93c3095c1481942960e84dd2faa95bbb71b9b762a106aead0646695330936414b31ca0bd862bf488a937ad17c8c5d73b32b - languageName: node - linkType: hard - "path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -3740,50 +2407,13 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^4.0.0": - version: 4.0.0 - resolution: "path-key@npm:4.0.0" - checksum: 10c0/794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3 - languageName: node - linkType: hard - -"path-parse@npm:^1.0.7": - version: 1.0.7 - resolution: "path-parse@npm:1.0.7" - checksum: 10c0/11ce261f9d294cc7a58d6a574b7f1b935842355ec66fba3c3fd79e0f036462eaf07d0aa95bb74ff432f9afef97ce1926c720988c6a7451d8a584930ae7de86e1 - languageName: node - linkType: hard - "path-scurry@npm:^1.11.1, path-scurry@npm:^1.6.1": version: 1.11.1 resolution: "path-scurry@npm:1.11.1" dependencies: lru-cache: "npm:^10.2.0" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d - languageName: node - linkType: hard - -"path-type@npm:^3.0.0": - version: 3.0.0 - resolution: "path-type@npm:3.0.0" - dependencies: - pify: "npm:^3.0.0" - checksum: 10c0/1332c632f1cac15790ebab8dd729b67ba04fc96f81647496feb1c2975d862d046f41e4b975dbd893048999b2cc90721f72924ad820acc58c78507ba7141a8e56 - languageName: node - linkType: hard - -"pathe@npm:^1.1.1, pathe@npm:^1.1.2": - version: 1.1.2 - resolution: "pathe@npm:1.1.2" - checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897 - languageName: node - linkType: hard - -"pathval@npm:^1.1.1": - version: 1.1.1 - resolution: "pathval@npm:1.1.1" - checksum: 10c0/f63e1bc1b33593cdf094ed6ff5c49c1c0dc5dc20a646ca9725cc7fe7cd9995002d51d5685b9b2ec6814342935748b711bafa840f84c0bb04e38ff40a335c94dc + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d languageName: node linkType: hard @@ -3801,40 +2431,6 @@ __metadata: languageName: node linkType: hard -"pidtree@npm:^0.3.0": - version: 0.3.1 - resolution: "pidtree@npm:0.3.1" - bin: - pidtree: bin/pidtree.js - checksum: 10c0/cd69b0182f749f45ab48584e3442c48c5dc4512502c18d5b0147a33b042c41a4db4269b9ce2f7c48f11833ee5e79d81f5ebc6f7bf8372d4ea55726f60dc505a1 - languageName: node - linkType: hard - -"pify@npm:^3.0.0": - version: 3.0.0 - resolution: "pify@npm:3.0.0" - checksum: 10c0/fead19ed9d801f1b1fcd0638a1ac53eabbb0945bf615f2f8806a8b646565a04a1b0e7ef115c951d225f042cca388fdc1cd3add46d10d1ed6951c20bd2998af10 - languageName: node - linkType: hard - -"pkg-types@npm:^1.0.3, pkg-types@npm:^1.1.1": - version: 1.1.3 - resolution: "pkg-types@npm:1.1.3" - dependencies: - confbox: "npm:^0.1.7" - mlly: "npm:^1.7.1" - pathe: "npm:^1.1.2" - checksum: 10c0/4cd2c9442dd5e4ae0c61cbd8fdaa92a273939749b081f78150ce9a3f4e625cca0375607386f49f103f0720b239d02369bf181c3ea6c80cf1028a633df03706ad - languageName: node - linkType: hard - -"possible-typed-array-names@npm:^1.0.0": - version: 1.0.0 - resolution: "possible-typed-array-names@npm:1.0.0" - checksum: 10c0/d9aa22d31f4f7680e20269db76791b41c3a32c01a373e25f8a4813b4d45f7456bfc2b6d68f752dc4aab0e0bb0721cb3d76fb678c9101cb7a16316664bc2c73fd - languageName: node - linkType: hard - "postcss@npm:^8.4.27, postcss@npm:^8.4.39": version: 8.4.39 resolution: "postcss@npm:8.4.39" @@ -3855,17 +2451,6 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.7.0": - version: 29.7.0 - resolution: "pretty-format@npm:29.7.0" - dependencies: - "@jest/schemas": "npm:^29.6.3" - ansi-styles: "npm:^5.0.0" - react-is: "npm:^18.0.0" - checksum: 10c0/edc5ff89f51916f036c62ed433506b55446ff739358de77207e63e88a28ca2894caac6e73dcb68166a606e51c8087d32d400473e6a9fdd2dbe743f46c9c0276f - languageName: node - linkType: hard - "proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": version: 4.2.0 resolution: "proc-log@npm:4.2.0" @@ -3897,24 +2482,6 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.0.0": - version: 18.3.1 - resolution: "react-is@npm:18.3.1" - checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072 - languageName: node - linkType: hard - -"read-pkg@npm:^3.0.0": - version: 3.0.0 - resolution: "read-pkg@npm:3.0.0" - dependencies: - load-json-file: "npm:^4.0.0" - normalize-package-data: "npm:^2.3.2" - path-type: "npm:^3.0.0" - checksum: 10c0/65acf2df89fbcd506b48b7ced56a255ba00adf7ecaa2db759c86cc58212f6fd80f1f0b7a85c848551a5d0685232e9b64f45c1fd5b48d85df2761a160767eeb93 - languageName: node - linkType: hard - "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -3931,18 +2498,6 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.2": - version: 1.5.2 - resolution: "regexp.prototype.flags@npm:1.5.2" - dependencies: - call-bind: "npm:^1.0.6" - define-properties: "npm:^1.2.1" - es-errors: "npm:^1.3.0" - set-function-name: "npm:^2.0.1" - checksum: 10c0/0f3fc4f580d9c349f8b560b012725eb9c002f36daa0041b3fbf6f4238cb05932191a4d7d5db3b5e2caa336d5150ad0402ed2be81f711f9308fe7e1a9bf9bd552 - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -3950,32 +2505,6 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.10.0": - version: 1.22.8 - resolution: "resolve@npm:1.22.8" - dependencies: - is-core-module: "npm:^2.13.0" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10c0/07e179f4375e1fd072cfb72ad66d78547f86e6196c4014b31cb0b8bb1db5f7ca871f922d08da0fbc05b94e9fd42206f819648fa3b5b873ebbc8e1dc68fec433a - languageName: node - linkType: hard - -"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin": - version: 1.22.8 - resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" - dependencies: - is-core-module: "npm:^2.13.0" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10c0/0446f024439cd2e50c6c8fa8ba77eaa8370b4180f401a96abf3d1ebc770ac51c1955e12764cde449fde3fff480a61f84388e3505ecdbab778f4bef5f8212c729 - languageName: node - linkType: hard - "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -4080,29 +2609,6 @@ __metadata: languageName: node linkType: hard -"safe-array-concat@npm:^1.1.2": - version: 1.1.2 - resolution: "safe-array-concat@npm:1.1.2" - dependencies: - call-bind: "npm:^1.0.7" - get-intrinsic: "npm:^1.2.4" - has-symbols: "npm:^1.0.3" - isarray: "npm:^2.0.5" - checksum: 10c0/12f9fdb01c8585e199a347eacc3bae7b5164ae805cdc8c6707199dbad5b9e30001a50a43c4ee24dc9ea32dbb7279397850e9208a7e217f4d8b1cf5d90129dec9 - languageName: node - linkType: hard - -"safe-regex-test@npm:^1.0.3": - version: 1.0.3 - resolution: "safe-regex-test@npm:1.0.3" - dependencies: - call-bind: "npm:^1.0.6" - es-errors: "npm:^1.3.0" - is-regex: "npm:^1.1.4" - checksum: 10c0/900bf7c98dc58f08d8523b7012b468e4eb757afa624f198902c0643d7008ba777b0bdc35810ba0b758671ce887617295fb742b3f3968991b178ceca54cb07603 - languageName: node - linkType: hard - "safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -4110,15 +2616,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0": - version: 5.7.2 - resolution: "semver@npm:5.7.2" - bin: - semver: bin/semver - checksum: 10c0/e4cf10f86f168db772ae95d86ba65b3fd6c5967c94d97c708ccb463b778c2ee53b914cd7167620950fc07faf5a564e6efe903836639e512a1aa15fbc9667fa25 - languageName: node - linkType: hard - "semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -4137,41 +2634,6 @@ __metadata: languageName: node linkType: hard -"set-function-length@npm:^1.2.1": - version: 1.2.2 - resolution: "set-function-length@npm:1.2.2" - dependencies: - define-data-property: "npm:^1.1.4" - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.4" - gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.2" - checksum: 10c0/82850e62f412a258b71e123d4ed3873fa9377c216809551192bb6769329340176f109c2eeae8c22a8d386c76739855f78e8716515c818bcaef384b51110f0f3c - languageName: node - linkType: hard - -"set-function-name@npm:^2.0.1": - version: 2.0.2 - resolution: "set-function-name@npm:2.0.2" - dependencies: - define-data-property: "npm:^1.1.4" - es-errors: "npm:^1.3.0" - functions-have-names: "npm:^1.2.3" - has-property-descriptors: "npm:^1.0.2" - checksum: 10c0/fce59f90696c450a8523e754abb305e2b8c73586452619c2bad5f7bf38c7b6b4651895c9db895679c5bef9554339cf3ef1c329b66ece3eda7255785fbe299316 - languageName: node - linkType: hard - -"shebang-command@npm:^1.2.0": - version: 1.2.0 - resolution: "shebang-command@npm:1.2.0" - dependencies: - shebang-regex: "npm:^1.0.0" - checksum: 10c0/7b20dbf04112c456b7fc258622dafd566553184ac9b6938dd30b943b065b21dabd3776460df534cc02480db5e1b6aec44700d985153a3da46e7db7f9bd21326d - languageName: node - linkType: hard - "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -4181,13 +2643,6 @@ __metadata: languageName: node linkType: hard -"shebang-regex@npm:^1.0.0": - version: 1.0.0 - resolution: "shebang-regex@npm:1.0.0" - checksum: 10c0/9abc45dee35f554ae9453098a13fdc2f1730e525a5eb33c51f096cc31f6f10a4b38074c1ebf354ae7bffa7229506083844008dfc3bb7818228568c0b2dc1fff2 - languageName: node - linkType: hard - "shebang-regex@npm:^3.0.0": version: 3.0.0 resolution: "shebang-regex@npm:3.0.0" @@ -4195,39 +2650,28 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.6.1, shell-quote@npm:^1.8.1": +"shell-quote@npm:^1.8.1": version: 1.8.1 resolution: "shell-quote@npm:1.8.1" checksum: 10c0/8cec6fd827bad74d0a49347057d40dfea1e01f12a6123bf82c4649f3ef152fc2bc6d6176e6376bffcd205d9d0ccb4f1f9acae889384d20baff92186f01ea455a languageName: node linkType: hard -"side-channel@npm:^1.0.4": - version: 1.0.6 - resolution: "side-channel@npm:1.0.6" - dependencies: - call-bind: "npm:^1.0.7" - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.4" - object-inspect: "npm:^1.13.1" - checksum: 10c0/d2afd163dc733cc0a39aa6f7e39bf0c436293510dbccbff446733daeaf295857dbccf94297092ec8c53e2503acac30f0b78830876f0485991d62a90e9cad305f - languageName: node - linkType: hard - -"siginfo@npm:^2.0.0": - version: 2.0.0 - resolution: "siginfo@npm:2.0.0" - checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 - languageName: node - linkType: hard - -"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": +"signal-exit@npm:^4.0.1": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 languageName: node linkType: hard +"sketch_magic@workspace:packages/sketch_magic": + version: 0.0.0-use.local + resolution: "sketch_magic@workspace:packages/sketch_magic" + dependencies: + "@chouqueth/gleam": "npm:^1.6.2" + languageName: unknown + linkType: soft + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -4277,40 +2721,6 @@ __metadata: languageName: node linkType: hard -"spdx-correct@npm:^3.0.0": - version: 3.2.0 - resolution: "spdx-correct@npm:3.2.0" - dependencies: - spdx-expression-parse: "npm:^3.0.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10c0/49208f008618b9119208b0dadc9208a3a55053f4fd6a0ae8116861bd22696fc50f4142a35ebfdb389e05ccf2de8ad142573fefc9e26f670522d899f7b2fe7386 - languageName: node - linkType: hard - -"spdx-exceptions@npm:^2.1.0": - version: 2.5.0 - resolution: "spdx-exceptions@npm:2.5.0" - checksum: 10c0/37217b7762ee0ea0d8b7d0c29fd48b7e4dfb94096b109d6255b589c561f57da93bf4e328c0290046115961b9209a8051ad9f525e48d433082fc79f496a4ea940 - languageName: node - linkType: hard - -"spdx-expression-parse@npm:^3.0.0": - version: 3.0.1 - resolution: "spdx-expression-parse@npm:3.0.1" - dependencies: - spdx-exceptions: "npm:^2.1.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10c0/6f8a41c87759fa184a58713b86c6a8b028250f158159f1d03ed9d1b6ee4d9eefdc74181c8ddc581a341aa971c3e7b79e30b59c23b05d2436d5de1c30bdef7171 - languageName: node - linkType: hard - -"spdx-license-ids@npm:^3.0.0": - version: 3.0.18 - resolution: "spdx-license-ids@npm:3.0.18" - checksum: 10c0/c64ba03d4727191c8fdbd001f137d6ab51386c350d5516be8a4576c2e74044cb27bc8a758f6a04809da986cc0b14213f069b04de72caccecbc9f733753ccde32 - languageName: node - linkType: hard - "sprintf-js@npm:^1.1.3": version: 1.1.3 resolution: "sprintf-js@npm:1.1.3" @@ -4327,20 +2737,6 @@ __metadata: languageName: node linkType: hard -"stackback@npm:0.0.2": - version: 0.0.2 - resolution: "stackback@npm:0.0.2" - checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 - languageName: node - linkType: hard - -"std-env@npm:^3.5.0": - version: 3.7.0 - resolution: "std-env@npm:3.7.0" - checksum: 10c0/60edf2d130a4feb7002974af3d5a5f3343558d1ccf8d9b9934d225c638606884db4a20d2fe6440a09605bca282af6b042ae8070a10490c0800d69e82e478f41e - languageName: node - linkType: hard - "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -4363,52 +2759,6 @@ __metadata: languageName: node linkType: hard -"string.prototype.padend@npm:^3.0.0": - version: 3.1.6 - resolution: "string.prototype.padend@npm:3.1.6" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - es-object-atoms: "npm:^1.0.0" - checksum: 10c0/8f2c8c1f3db1efcdc210668c80c87f2cea1253d6029ff296a172b5e13edc9adebeed4942d023de8d31f9b13b69f3f5d73de7141959b1f09817fba5f527e83be1 - languageName: node - linkType: hard - -"string.prototype.trim@npm:^1.2.9": - version: 1.2.9 - resolution: "string.prototype.trim@npm:1.2.9" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.0" - es-object-atoms: "npm:^1.0.0" - checksum: 10c0/dcef1a0fb61d255778155006b372dff8cc6c4394bc39869117e4241f41a2c52899c0d263ffc7738a1f9e61488c490b05c0427faa15151efad721e1a9fb2663c2 - languageName: node - linkType: hard - -"string.prototype.trimend@npm:^1.0.8": - version: 1.0.8 - resolution: "string.prototype.trimend@npm:1.0.8" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10c0/0a0b54c17c070551b38e756ae271865ac6cc5f60dabf2e7e343cceae7d9b02e1a1120a824e090e79da1b041a74464e8477e2da43e2775c85392be30a6f60963c - languageName: node - linkType: hard - -"string.prototype.trimstart@npm:^1.0.8": - version: 1.0.8 - resolution: "string.prototype.trimstart@npm:1.0.8" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10c0/d53af1899959e53c83b64a5fd120be93e067da740e7e75acb433849aa640782fb6c7d4cd5b84c954c84413745a3764df135a8afeb22908b86a835290788d8366 - languageName: node - linkType: hard - "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -4427,29 +2777,6 @@ __metadata: languageName: node linkType: hard -"strip-bom@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-bom@npm:3.0.0" - checksum: 10c0/51201f50e021ef16672593d7434ca239441b7b760e905d9f33df6e4f3954ff54ec0e0a06f100d028af0982d6f25c35cd5cda2ce34eaebccd0250b8befb90d8f1 - languageName: node - linkType: hard - -"strip-final-newline@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-final-newline@npm:3.0.0" - checksum: 10c0/a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce - languageName: node - linkType: hard - -"strip-literal@npm:^2.0.0": - version: 2.1.0 - resolution: "strip-literal@npm:2.1.0" - dependencies: - js-tokens: "npm:^9.0.0" - checksum: 10c0/bc8b8c8346125ae3c20fcdaf12e10a498ff85baf6f69597b4ab2b5fbf2e58cfd2827f1a44f83606b852da99a5f6c8279770046ddea974c510c17c98934c9cc24 - languageName: node - linkType: hard - "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -4477,13 +2804,6 @@ __metadata: languageName: node linkType: hard -"supports-preserve-symlinks-flag@npm:^1.0.0": - version: 1.0.0 - resolution: "supports-preserve-symlinks-flag@npm:1.0.0" - checksum: 10c0/6c4032340701a9950865f7ae8ef38578d8d7053f5e10518076e6554a9381fa91bd9c6850193695c141f32b21f979c985db07265a758867bac95de05f7d8aeb39 - languageName: node - linkType: hard - "tar@npm:^6.1.11, tar@npm:^6.2.1": version: 6.2.1 resolution: "tar@npm:6.2.1" @@ -4512,27 +2832,6 @@ __metadata: languageName: node linkType: hard -"tinybench@npm:^2.5.1": - version: 2.8.0 - resolution: "tinybench@npm:2.8.0" - checksum: 10c0/5a9a642351fa3e4955e0cbf38f5674be5f3ba6730fd872fd23a5c953ad6c914234d5aba6ea41ef88820180a81829ceece5bd8d3967c490c5171bca1141c2f24d - languageName: node - linkType: hard - -"tinypool@npm:^0.8.3": - version: 0.8.4 - resolution: "tinypool@npm:0.8.4" - checksum: 10c0/779c790adcb0316a45359652f4b025958c1dff5a82460fe49f553c864309b12ad732c8288be52f852973bc76317f5e7b3598878aee0beb8a33322c0e72c4a66c - languageName: node - linkType: hard - -"tinyspy@npm:^2.2.0": - version: 2.2.1 - resolution: "tinyspy@npm:2.2.1" - checksum: 10c0/0b4cfd07c09871e12c592dfa7b91528124dc49a4766a0b23350638c62e6a483d5a2a667de7e6282246c0d4f09996482ddaacbd01f0c05b7ed7e0f79d32409bdc - languageName: node - linkType: hard - "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -4572,16 +2871,6 @@ __metadata: languageName: node linkType: hard -"ts-gleam@npm:^1.0.1": - version: 1.0.1 - resolution: "ts-gleam@npm:1.0.1" - dependencies: - "@iarna/toml": "npm:^2.2.5" - typescript: "npm:^5.2.2" - checksum: 10c0/c0f07828cbcb5793a25d974f233b2470d4f2cddebf0e6201beb38838a24a77272e98c2dcdb29a21b4796fb069e9f77224dae95c19a2a2b7df295f9703f597bfb - languageName: node - linkType: hard - "tslib@npm:^2.1.0": version: 2.6.3 resolution: "tslib@npm:2.6.3" @@ -4589,111 +2878,6 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:^4.0.0, type-detect@npm:^4.0.8": - version: 4.0.8 - resolution: "type-detect@npm:4.0.8" - checksum: 10c0/8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd - languageName: node - linkType: hard - -"typed-array-buffer@npm:^1.0.2": - version: 1.0.2 - resolution: "typed-array-buffer@npm:1.0.2" - dependencies: - call-bind: "npm:^1.0.7" - es-errors: "npm:^1.3.0" - is-typed-array: "npm:^1.1.13" - checksum: 10c0/9e043eb38e1b4df4ddf9dde1aa64919ae8bb909571c1cc4490ba777d55d23a0c74c7d73afcdd29ec98616d91bb3ae0f705fad4421ea147e1daf9528200b562da - languageName: node - linkType: hard - -"typed-array-byte-length@npm:^1.0.1": - version: 1.0.1 - resolution: "typed-array-byte-length@npm:1.0.1" - dependencies: - call-bind: "npm:^1.0.7" - for-each: "npm:^0.3.3" - gopd: "npm:^1.0.1" - has-proto: "npm:^1.0.3" - is-typed-array: "npm:^1.1.13" - checksum: 10c0/fcebeffb2436c9f355e91bd19e2368273b88c11d1acc0948a2a306792f1ab672bce4cfe524ab9f51a0505c9d7cd1c98eff4235c4f6bfef6a198f6cfc4ff3d4f3 - languageName: node - linkType: hard - -"typed-array-byte-offset@npm:^1.0.2": - version: 1.0.2 - resolution: "typed-array-byte-offset@npm:1.0.2" - dependencies: - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.7" - for-each: "npm:^0.3.3" - gopd: "npm:^1.0.1" - has-proto: "npm:^1.0.3" - is-typed-array: "npm:^1.1.13" - checksum: 10c0/d2628bc739732072e39269389a758025f75339de2ed40c4f91357023c5512d237f255b633e3106c461ced41907c1bf9a533c7e8578066b0163690ca8bc61b22f - languageName: node - linkType: hard - -"typed-array-length@npm:^1.0.6": - version: 1.0.6 - resolution: "typed-array-length@npm:1.0.6" - dependencies: - call-bind: "npm:^1.0.7" - for-each: "npm:^0.3.3" - gopd: "npm:^1.0.1" - has-proto: "npm:^1.0.3" - is-typed-array: "npm:^1.1.13" - possible-typed-array-names: "npm:^1.0.0" - checksum: 10c0/74253d7dc488eb28b6b2711cf31f5a9dcefc9c41b0681fd1c178ed0a1681b4468581a3626d39cd4df7aee3d3927ab62be06aa9ca74e5baf81827f61641445b77 - languageName: node - linkType: hard - -"typescript@npm:^5.2.2, typescript@npm:^5.4.2": - version: 5.5.3 - resolution: "typescript@npm:5.5.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/f52c71ccbc7080b034b9d3b72051d563601a4815bf3e39ded188e6ce60813f75dbedf11ad15dd4d32a12996a9ed8c7155b46c93a9b9c9bad1049766fe614bbdd - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A^5.2.2#optional!builtin, typescript@patch:typescript@npm%3A^5.4.2#optional!builtin": - version: 5.5.3 - resolution: "typescript@patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=b45daf" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/5a437c416251334deeaf29897157032311f3f126547cfdc4b133768b606cb0e62bcee733bb97cf74c42fe7268801aea1392d8e40988cdef112e9546eba4c03c5 - languageName: node - linkType: hard - -"ufo@npm:^1.5.3": - version: 1.5.3 - resolution: "ufo@npm:1.5.3" - checksum: 10c0/1df10702582aa74f4deac4486ecdfd660e74be057355f1afb6adfa14243476cf3d3acff734ccc3d0b74e9bfdefe91d578f3edbbb0a5b2430fe93cd672370e024 - languageName: node - linkType: hard - -"uhyphen@npm:^0.2.0": - version: 0.2.0 - resolution: "uhyphen@npm:0.2.0" - checksum: 10c0/1e7129fe7a5c86445d1adf04d5c58913b5992e4899ea5553d9ddf6e7ef88af0f807a47f1bf9673b92f705276e5cf1b2c1d3852f1ab5d08ecac3382bcc3a642f9 - languageName: node - linkType: hard - -"unbox-primitive@npm:^1.0.2": - version: 1.0.2 - resolution: "unbox-primitive@npm:1.0.2" - dependencies: - call-bind: "npm:^1.0.2" - has-bigints: "npm:^1.0.2" - has-symbols: "npm:^1.0.3" - which-boxed-primitive: "npm:^1.0.2" - checksum: 10c0/81ca2e81134167cc8f75fa79fbcc8a94379d6c61de67090986a2273850989dd3bae8440c163121b77434b68263e34787a675cbdcb34bb2f764c6b9c843a11b66 - languageName: node - linkType: hard - "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -4738,16 +2922,6 @@ __metadata: languageName: node linkType: hard -"validate-npm-package-license@npm:^3.0.1": - version: 3.0.4 - resolution: "validate-npm-package-license@npm:3.0.4" - dependencies: - spdx-correct: "npm:^3.0.0" - spdx-expression-parse: "npm:^3.0.0" - checksum: 10c0/7b91e455a8de9a0beaa9fe961e536b677da7f48c9a493edf4d4d4a87fd80a7a10267d438723364e432c2fcd00b5650b5378275cded362383ef570276e6312f4f - languageName: node - linkType: hard - "vite-gleam@npm:^0.4.0": version: 0.4.3 resolution: "vite-gleam@npm:0.4.3" @@ -4760,21 +2934,6 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:1.6.0": - version: 1.6.0 - resolution: "vite-node@npm:1.6.0" - dependencies: - cac: "npm:^6.7.14" - debug: "npm:^4.3.4" - pathe: "npm:^1.1.1" - picocolors: "npm:^1.0.0" - vite: "npm:^5.0.0" - bin: - vite-node: vite-node.mjs - checksum: 10c0/0807e6501ac7763e0efa2b4bd484ce99fb207e92c98624c9f8999d1f6727ac026e457994260fa7fdb7060d87546d197081e46a705d05b0136a38b6f03715cbc2 - languageName: node - linkType: hard - "vite@npm:^4.4.5": version: 4.5.3 resolution: "vite@npm:4.5.3" @@ -4815,7 +2974,7 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.0.0, vite@npm:^5.1.6": +"vite@npm:^5.1.6": version: 5.3.3 resolution: "vite@npm:5.3.3" dependencies: @@ -4855,61 +3014,11 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^1.5.0": - version: 1.6.0 - resolution: "vitest@npm:1.6.0" - dependencies: - "@vitest/expect": "npm:1.6.0" - "@vitest/runner": "npm:1.6.0" - "@vitest/snapshot": "npm:1.6.0" - "@vitest/spy": "npm:1.6.0" - "@vitest/utils": "npm:1.6.0" - acorn-walk: "npm:^8.3.2" - chai: "npm:^4.3.10" - debug: "npm:^4.3.4" - execa: "npm:^8.0.1" - local-pkg: "npm:^0.5.0" - magic-string: "npm:^0.30.5" - pathe: "npm:^1.1.1" - picocolors: "npm:^1.0.0" - std-env: "npm:^3.5.0" - strip-literal: "npm:^2.0.0" - tinybench: "npm:^2.5.1" - tinypool: "npm:^0.8.3" - vite: "npm:^5.0.0" - vite-node: "npm:1.6.0" - why-is-node-running: "npm:^2.2.2" - peerDependencies: - "@edge-runtime/vm": "*" - "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 1.6.0 - "@vitest/ui": 1.6.0 - happy-dom: "*" - jsdom: "*" - peerDependenciesMeta: - "@edge-runtime/vm": - optional: true - "@types/node": - optional: true - "@vitest/browser": - optional: true - "@vitest/ui": - optional: true - happy-dom: - optional: true - jsdom: - optional: true - bin: - vitest: vitest.mjs - checksum: 10c0/065da5b8ead51eb174d93dac0cd50042ca9539856dc25e340ea905d668c41961f7e00df3e388e6c76125b2c22091db2e8465f993d0f6944daf9598d549e562e7 - languageName: node - linkType: hard - "vitools@workspace:packages/vitools": version: 0.0.0-use.local resolution: "vitools@workspace:packages/vitools" dependencies: - "@chouqueth/gleam": "npm:^1.3.2" + "@chouqueth/gleam": "npm:^1.6.2" languageName: unknown linkType: soft @@ -4944,43 +3053,6 @@ __metadata: languageName: node linkType: hard -"which-boxed-primitive@npm:^1.0.2": - version: 1.0.2 - resolution: "which-boxed-primitive@npm:1.0.2" - dependencies: - is-bigint: "npm:^1.0.1" - is-boolean-object: "npm:^1.1.0" - is-number-object: "npm:^1.0.4" - is-string: "npm:^1.0.5" - is-symbol: "npm:^1.0.3" - checksum: 10c0/0a62a03c00c91dd4fb1035b2f0733c341d805753b027eebd3a304b9cb70e8ce33e25317add2fe9b5fea6f53a175c0633ae701ff812e604410ddd049777cd435e - languageName: node - linkType: hard - -"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15": - version: 1.1.15 - resolution: "which-typed-array@npm:1.1.15" - dependencies: - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.7" - for-each: "npm:^0.3.3" - gopd: "npm:^1.0.1" - has-tostringtag: "npm:^1.0.2" - checksum: 10c0/4465d5348c044032032251be54d8988270e69c6b7154f8fcb2a47ff706fe36f7624b3a24246b8d9089435a8f4ec48c1c1025c5d6b499456b9e5eff4f48212983 - languageName: node - linkType: hard - -"which@npm:^1.2.9": - version: 1.3.1 - resolution: "which@npm:1.3.1" - dependencies: - isexe: "npm:^2.0.0" - bin: - which: ./bin/which - checksum: 10c0/e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59 - languageName: node - linkType: hard - "which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -5003,18 +3075,6 @@ __metadata: languageName: node linkType: hard -"why-is-node-running@npm:^2.2.2": - version: 2.3.0 - resolution: "why-is-node-running@npm:2.3.0" - dependencies: - siginfo: "npm:^2.0.0" - stackback: "npm:0.0.2" - bin: - why-is-node-running: cli.js - checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 - languageName: node - linkType: hard - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -5093,10 +3153,3 @@ __metadata: checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f languageName: node linkType: hard - -"yocto-queue@npm:^1.0.0": - version: 1.1.1 - resolution: "yocto-queue@npm:1.1.1" - checksum: 10c0/cb287fe5e6acfa82690acb43c283de34e945c571a78a939774f6eaba7c285bacdf6c90fbc16ce530060863984c906d2b4c6ceb069c94d1e0a06d5f2b458e2a92 - languageName: node - linkType: hard