.dockerignore π
@@ -0,0 +1,3 @@
+/target
+/manifest.yml
+/migrate.yml
Nathan Sobo and Max Brunsfeld created
We're going full monorepo.
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
.dockerignore | 3
.gitignore | 3
Cargo.lock | 734 ++
Cargo.toml | 11
Dockerfile | 33
Dockerfile.migrator | 15
gpui/Cargo.toml | 12
gpui/grammars/context-predicate/Cargo.toml | 8
gpui_macros/Cargo.toml | 2
script/build-css | 10
script/deploy | 17
script/deploy-migration | 11
script/package-lock.json | 2452 +++++++++
script/package.json | 6
script/server | 6
script/sqlx | 12
script/tailwind.config.js | 44
server/.env.template.toml | 11
server/Cargo.toml | 50
server/Procfile | 2
server/README.md | 17
server/basic.conf | 12
server/manifest.yml | 71
server/migrate.yml | 17
server/migrations/20210527024318_initial_schema.sql | 26
server/migrations/20210607190313_create_access_tokens.sql | 7
server/src/admin.rs | 160
server/src/assets.rs | 31
server/src/auth.rs | 336 +
server/src/bin/dotenv.rs | 20
server/src/env.rs | 20
server/src/errors.rs | 73
server/src/expiring.rs | 43
server/src/github.rs | 265
server/src/home.rs | 112
server/src/main.rs | 197
server/src/rpc.rs | 652 ++
server/src/team.rs | 15
server/src/tests.rs | 538 +
server/static/fonts/VisbyCF-Bold.eot | 0
server/static/fonts/VisbyCF-Bold.woff | 0
server/static/fonts/VisbyCF-Bold.woff2 | 0
server/static/fonts/VisbyCF-BoldOblique.eot | 0
server/static/fonts/VisbyCF-BoldOblique.woff | 0
server/static/fonts/VisbyCF-BoldOblique.woff2 | 0
server/static/fonts/VisbyCF-DemiBold.eot | 0
server/static/fonts/VisbyCF-DemiBold.woff | 0
server/static/fonts/VisbyCF-DemiBold.woff2 | 0
server/static/fonts/VisbyCF-DemiBoldOblique.eot | 0
server/static/fonts/VisbyCF-DemiBoldOblique.woff | 0
server/static/fonts/VisbyCF-DemiBoldOblique.woff2 | 0
server/static/fonts/VisbyCF-ExtraBold.eot | 0
server/static/fonts/VisbyCF-ExtraBold.woff | 0
server/static/fonts/VisbyCF-ExtraBold.woff2 | 0
server/static/fonts/VisbyCF-ExtraBoldOblique.eot | 0
server/static/fonts/VisbyCF-ExtraBoldOblique.woff | 0
server/static/fonts/VisbyCF-ExtraBoldOblique.woff2 | 0
server/static/fonts/VisbyCF-Heavy.eot | 0
server/static/fonts/VisbyCF-Heavy.woff | 0
server/static/fonts/VisbyCF-Heavy.woff2 | 0
server/static/fonts/VisbyCF-HeavyOblique.eot | 0
server/static/fonts/VisbyCF-HeavyOblique.woff | 0
server/static/fonts/VisbyCF-HeavyOblique.woff2 | 0
server/static/fonts/VisbyCF-Light.eot | 0
server/static/fonts/VisbyCF-Light.woff | 0
server/static/fonts/VisbyCF-Light.woff2 | 0
server/static/fonts/VisbyCF-LightOblique.eot | 0
server/static/fonts/VisbyCF-LightOblique.woff | 0
server/static/fonts/VisbyCF-LightOblique.woff2 | 0
server/static/fonts/VisbyCF-Medium.eot | 0
server/static/fonts/VisbyCF-Medium.woff | 0
server/static/fonts/VisbyCF-Medium.woff2 | 0
server/static/fonts/VisbyCF-MediumOblique.eot | 0
server/static/fonts/VisbyCF-MediumOblique.woff | 0
server/static/fonts/VisbyCF-MediumOblique.woff2 | 0
server/static/fonts/VisbyCF-Regular.eot | 0
server/static/fonts/VisbyCF-Regular.woff | 0
server/static/fonts/VisbyCF-Regular.woff2 | 0
server/static/fonts/VisbyCF-RegularOblique.eot | 0
server/static/fonts/VisbyCF-RegularOblique.woff | 0
server/static/fonts/VisbyCF-RegularOblique.woff2 | 0
server/static/fonts/VisbyCF-Thin.eot | 0
server/static/fonts/VisbyCF-Thin.woff | 0
server/static/fonts/VisbyCF-Thin.woff2 | 0
server/static/fonts/VisbyCF-ThinOblique.eot | 0
server/static/fonts/VisbyCF-ThinOblique.woff | 0
server/static/fonts/VisbyCF-ThinOblique.woff2 | 0
server/static/images/favicon.png | 0
server/static/svg/hero.svg | 4
server/styles.css | 108
server/templates/admin.hbs | 81
server/templates/docs.hbs | 41
server/templates/error.hbs | 7
server/templates/home.hbs | 69
server/templates/partials/layout.hbs | 62
server/templates/signup.hbs | 19
server/templates/team.hbs | 62
zed-rpc/Cargo.toml | 6
zed/Cargo.toml | 24
99 files changed, 6,466 insertions(+), 71 deletions(-)
@@ -0,0 +1,3 @@
+/target
+/manifest.yml
+/migrate.yml
@@ -1,3 +1,6 @@
/target
/zed.xcworkspace
.DS_Store
+/script/node_modules
+/server/.env.toml
+/server/static/styles.css
@@ -29,7 +29,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331"
dependencies = [
- "generic-array",
+ "generic-array 0.14.4",
]
[[package]]
@@ -40,7 +40,7 @@ checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561"
dependencies = [
"aes-soft",
"aesni",
- "cipher",
+ "cipher 0.2.5",
]
[[package]]
@@ -51,7 +51,7 @@ checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da"
dependencies = [
"aead",
"aes",
- "cipher",
+ "cipher 0.2.5",
"ctr",
"ghash",
"subtle",
@@ -63,8 +63,8 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072"
dependencies = [
- "cipher",
- "opaque-debug",
+ "cipher 0.2.5",
+ "opaque-debug 0.3.0",
]
[[package]]
@@ -73,8 +73,36 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce"
dependencies = [
- "cipher",
- "opaque-debug",
+ "cipher 0.2.5",
+ "opaque-debug 0.3.0",
+]
+
+[[package]]
+name = "ahash"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
+
+[[package]]
+name = "ahash"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "796540673305a66d127804eef19ad696f1f204b8c1025aaca4958c17eab32877"
+dependencies = [
+ "getrandom 0.2.2",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
+dependencies = [
+ "getrandom 0.2.2",
+ "once_cell",
+ "version_check",
]
[[package]]
@@ -86,6 +114,21 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192ec435945d87bc2f70992b4d818154b5feede43c09fb7592146374eac90a6"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
[[package]]
name = "ansi_term"
version = "0.11.0"
@@ -97,9 +140,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.38"
+version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
+checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
[[package]]
name = "ar"
@@ -125,6 +168,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote",
+ "syn",
+]
+
[[package]]
name = "async-channel"
version = "1.6.1"
@@ -136,6 +189,30 @@ dependencies = [
"futures-core",
]
+[[package]]
+name = "async-compression"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443ccbb270374a2b1055fc72da40e1f237809cd6bb0e97e66d264cd138473a6"
+dependencies = [
+ "brotli",
+ "flate2",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "pin-project-lite 0.2.4",
+]
+
+[[package]]
+name = "async-dup"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7427a12b8dc09291528cfb1da2447059adb4a257388c2acd6497a79d55cf6f7c"
+dependencies = [
+ "futures-io",
+ "simple-mutex",
+]
+
[[package]]
name = "async-executor"
version = "1.4.0"
@@ -177,6 +254,24 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "async-h1"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc5142de15b549749cce62923a50714b0d7b77f5090ced141599e78899865451"
+dependencies = [
+ "async-channel",
+ "async-dup",
+ "async-std",
+ "byte-pool",
+ "futures-core",
+ "http-types",
+ "httparse",
+ "lazy_static",
+ "log",
+ "pin-project",
+]
+
[[package]]
name = "async-io"
version = "1.3.1"
@@ -243,16 +338,86 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "async-rustls"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f38092e8f467f47aadaff680903c7cbfeee7926b058d7f40af2dd4c878fbdee"
+dependencies = [
+ "futures-lite",
+ "rustls 0.18.1",
+ "webpki",
+]
+
+[[package]]
+name = "async-rustls"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c86f33abd5a4f3e2d6d9251a9e0c6a7e52eb1113caf893dae8429bf4a53f378"
+dependencies = [
+ "futures-lite",
+ "rustls 0.19.1",
+ "webpki",
+]
+
+[[package]]
+name = "async-session"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f"
+dependencies = [
+ "anyhow",
+ "async-std",
+ "async-trait",
+ "base64 0.12.3",
+ "bincode",
+ "blake3",
+ "chrono",
+ "hmac 0.8.1",
+ "kv-log-macro",
+ "rand 0.7.3",
+ "serde 1.0.125",
+ "serde_json 1.0.64",
+ "sha2",
+]
+
+[[package]]
+name = "async-sqlx-session"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b66fb8c6ffbf26cdba6705c36f086b6f02f0b4778b6a348134302a2a00730bc"
+dependencies = [
+ "async-session",
+ "async-std",
+ "sqlx 0.4.2",
+]
+
+[[package]]
+name = "async-sse"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10"
+dependencies = [
+ "async-channel",
+ "async-std",
+ "http-types",
+ "log",
+ "memchr",
+ "pin-project-lite 0.1.12",
+]
+
[[package]]
name = "async-std"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9f06685bad74e0570f5213741bea82158279a4103d988e57bfada11ad230341"
dependencies = [
+ "async-attributes",
"async-channel",
"async-global-executor",
"async-io",
"async-lock",
+ "async-process",
"crossbeam-utils",
"futures-channel",
"futures-core",
@@ -264,7 +429,7 @@ dependencies = [
"memchr",
"num_cpus",
"once_cell",
- "pin-project-lite",
+ "pin-project-lite 0.2.4",
"pin-utils",
"slab",
"wasm-bindgen-futures",
@@ -283,7 +448,7 @@ checksum = "2f23d769dbf1838d5df5156e7b1ad404f4c463d1ac2c6aeb6cd943630f8a8400"
dependencies = [
"futures-core",
"futures-io",
- "rustls",
+ "rustls 0.19.1",
"webpki",
"webpki-roots",
]
@@ -309,10 +474,19 @@ dependencies = [
"futures-io",
"futures-util",
"log",
- "pin-project-lite",
+ "pin-project-lite 0.2.4",
"tungstenite",
]
+[[package]]
+name = "atoi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5"
+dependencies = [
+ "num-traits 0.2.14",
+]
+
[[package]]
name = "atomic"
version = "0.5.0"
@@ -383,6 +557,21 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+[[package]]
+name = "base64ct"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0d27fb6b6f1e43147af148af49d49329413ba781aa0d5e10979831c210173b5"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde 1.0.125",
+]
+
[[package]]
name = "bindgen"
version = "0.58.1"
@@ -392,7 +581,7 @@ dependencies = [
"bitflags 1.2.1",
"cexpr",
"clang-sys",
- "clap",
+ "clap 2.33.3",
"env_logger",
"lazy_static",
"lazycell",
@@ -418,6 +607,18 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+[[package]]
+name = "bitvec"
+version = "0.19.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
[[package]]
name = "blake2b_simd"
version = "0.5.11"
@@ -429,19 +630,55 @@ dependencies = [
"constant_time_eq",
]
+[[package]]
+name = "blake3"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "cc",
+ "cfg-if 0.1.10",
+ "constant_time_eq",
+ "crypto-mac 0.8.0",
+ "digest 0.9.0",
+]
+
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+[[package]]
+name = "block-buffer"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
+dependencies = [
+ "block-padding",
+ "byte-tools",
+ "byteorder",
+ "generic-array 0.12.4",
+]
+
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
- "generic-array",
+ "generic-array 0.14.4",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
+dependencies = [
+ "byte-tools",
]
[[package]]
@@ -458,6 +695,27 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "brotli"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f29919120f08613aadcd4383764e00526fc9f18b6c0895814faeed0dd78613e"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1052e1c3b8d4d80eb84a8b94f0a1498797b5fb96314c001156a1c761940ef4ec"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
[[package]]
name = "bstr"
version = "0.2.15"
@@ -467,12 +725,34 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "build_const"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7"
+
[[package]]
name = "bumpalo"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
+[[package]]
+name = "byte-pool"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca"
+dependencies = [
+ "crossbeam-queue",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "byte-tools"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
+
[[package]]
name = "bytemuck"
version = "1.5.1"
@@ -523,7 +803,7 @@ dependencies = [
"ar",
"cab",
"chrono",
- "clap",
+ "clap 2.33.3",
"dirs 1.0.5",
"error-chain",
"glob 0.2.11",
@@ -543,6 +823,28 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "cargo-platform"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0226944a63d1bf35a3b5f948dd7c59e263db83695c9e8bffc4037de02e30f1d7"
+dependencies = [
+ "serde 1.0.125",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714a157da7991e23d90686b9524b9e12e0407a108647f52e9328f4b3d51ac7f"
+dependencies = [
+ "cargo-platform",
+ "semver 0.11.0",
+ "semver-parser 0.10.2",
+ "serde 1.0.125",
+ "serde_json 1.0.64",
+]
+
[[package]]
name = "cc"
version = "1.0.67"
@@ -555,7 +857,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
dependencies = [
- "nom",
+ "nom 5.1.2",
]
[[package]]
@@ -589,6 +891,7 @@ dependencies = [
"libc",
"num-integer",
"num-traits 0.2.14",
+ "serde 1.0.125",
"time 0.1.44",
"winapi 0.3.9",
]
@@ -605,7 +908,16 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
dependencies = [
- "generic-array",
+ "generic-array 0.14.4",
+]
+
+[[package]]
+name = "cipher"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
+dependencies = [
+ "generic-array 0.14.4",
]
[[package]]
@@ -629,11 +941,43 @@ dependencies = [
"atty",
"bitflags 1.2.1",
"strsim 0.8.0",
- "textwrap",
+ "textwrap 0.11.0",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "clap"
+version = "3.0.0-beta.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
+dependencies = [
+ "atty",
+ "bitflags 1.2.1",
+ "clap_derive",
+ "indexmap",
+ "lazy_static",
+ "os_str_bytes",
+ "strsim 0.10.0",
+ "termcolor",
+ "textwrap 0.12.1",
"unicode-width",
"vec_map",
]
+[[package]]
+name = "clap_derive"
+version = "3.0.0-beta.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "cloudabi"
version = "0.0.3"
@@ -652,6 +996,18 @@ dependencies = [
"cc",
]
+[[package]]
+name = "coarsetime"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2918e2ffa91a49dabbba4965fe38a37a1ba0b6953a29e32cc250a8d59cd42232"
+dependencies = [
+ "libc",
+ "once_cell",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
[[package]]
name = "cocoa"
version = "0.24.0"
@@ -687,6 +1043,25 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+[[package]]
+name = "comrak"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b423acba50d5016684beaf643f9991e622633a4c858be6885653071c2da2b0c6"
+dependencies = [
+ "clap 2.33.3",
+ "entities",
+ "lazy_static",
+ "pest",
+ "pest_derive",
+ "regex",
+ "shell-words",
+ "twoway",
+ "typed-arena",
+ "unicode_categories",
+ "xdg",
+]
+
[[package]]
name = "concurrent-queue"
version = "1.2.2"
@@ -696,6 +1071,12 @@ dependencies = [
"cache-padded",
]
+[[package]]
+name = "const-oid"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22"
+
[[package]]
name = "const_fn"
version = "0.4.8"
@@ -717,7 +1098,7 @@ dependencies = [
"aes-gcm",
"base64 0.13.0",
"hkdf",
- "hmac",
+ "hmac 0.10.1",
"percent-encoding",
"rand 0.8.3",
"sha2",
@@ -789,6 +1170,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba"
+[[package]]
+name = "crc"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
+dependencies = [
+ "build_const",
+]
+
[[package]]
name = "crc32fast"
version = "1.2.1"
@@ -855,16 +1245,54 @@ dependencies = [
"loom",
]
+[[package]]
+name = "crypto-bigint"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b32a398eb1ccfbe7e4f452bc749c44d38dd732e9a253f19da224c416f00ee7f4"
+dependencies = [
+ "generic-array 0.14.4",
+ "rand_core 0.6.2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
+dependencies = [
+ "generic-array 0.14.4",
+ "subtle",
+]
+
[[package]]
name = "crypto-mac"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6"
dependencies = [
- "generic-array",
+ "generic-array 0.14.4",
"subtle",
]
+[[package]]
+name = "crypto-mac"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e"
+dependencies = [
+ "generic-array 0.14.4",
+ "subtle",
+]
+
+[[package]]
+name = "ct-codecs"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df"
+
[[package]]
name = "ctor"
version = "0.1.20"
@@ -881,7 +1309,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f"
dependencies = [
- "cipher",
+ "cipher 0.2.5",
]
[[package]]
@@ -960,13 +1388,31 @@ dependencies = [
"byteorder",
]
+[[package]]
+name = "der"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f215f706081a44cb702c71c39a52c05da637822e9c1645a50b7202689e982d"
+dependencies = [
+ "const-oid",
+]
+
+[[package]]
+name = "digest"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
+dependencies = [
+ "generic-array 0.12.4",
+]
+
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
- "generic-array",
+ "generic-array 0.14.4",
]
[[package]]
@@ -1027,6 +1473,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
[[package]]
name = "dtoa"
version = "0.4.8"
@@ -1051,12 +1503,49 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd4afd79212583ff429b913ad6605242ed7eec277e950b1438f300748f948f4"
+[[package]]
+name = "ecdsa"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05cb0ed2d2ce37766ac86c05f66973ace8c51f7f1533bedce8fb79e2b54b3f14"
+dependencies = [
+ "der",
+ "elliptic-curve",
+ "hmac 0.11.0",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-compact"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aaf396058cc7285b342f9a10ed7a377f088942396c46c4c9a7eb4f0782cb1171"
+dependencies = [
+ "getrandom 0.2.2",
+]
+
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+[[package]]
+name = "elliptic-curve"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83e5c176479da93a0983f0a6fdc3c1b8e7d5be0d7fe3fe05a99f15b96582b9a8"
+dependencies = [
+ "crypto-bigint",
+ "ff",
+ "generic-array 0.14.4",
+ "group",
+ "pkcs8",
+ "rand_core 0.6.2",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "encoding"
version = "0.2.33"
@@ -1130,6 +1619,12 @@ dependencies = [
"cfg-if 1.0.0",
]
+[[package]]
+name = "entities"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+
[[package]]
name = "enum_primitive"
version = "0.1.1"
@@ -1152,6 +1647,15 @@ dependencies = [
"termcolor",
]
+[[package]]
+name = "envy"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965"
+dependencies = [
+ "serde 1.0.125",
+]
+
[[package]]
name = "error-chain"
version = "0.12.4"
@@ -1197,6 +1701,12 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "fake-simd"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+
[[package]]
name = "fastrand"
version = "1.4.0"
@@ -1206,6 +1716,32 @@ dependencies = [
"instant",
]
+[[package]]
+name = "femme"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af1a24f391a5a94d756db5092c6576aad494b88a71a5a36b20c67b63e0df034"
+dependencies = [
+ "cfg-if 0.1.10",
+ "js-sys",
+ "log",
+ "serde 1.0.125",
+ "serde_derive",
+ "serde_json 1.0.64",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "ff"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63eec06c61e487eecf0f7e6e6372e596a81922c28d33e645d6983ca6493a1af0"
+dependencies = [
+ "rand_core 0.6.2",
+ "subtle",
+]
+
[[package]]
name = "filetime"
version = "0.2.14"
@@ -1371,6 +1907,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+[[package]]
+name = "funty"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+
[[package]]
name = "futures"
version = "0.3.12"
@@ -1430,7 +1972,7 @@ dependencies = [
"futures-io",
"memchr",
"parking",
- "pin-project-lite",
+ "pin-project-lite 0.2.4",
"waker-fn",
]
@@ -1471,7 +2013,7 @@ dependencies = [
"futures-sink",
"futures-task",
"memchr",
- "pin-project-lite",
+ "pin-project-lite 0.2.4",
"pin-utils",
"proc-macro-hack",
"proc-macro-nested",
@@ -1491,6 +2033,15 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "generic-array"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
+dependencies = [
+ "typenum",
+]
+
[[package]]
name = "generic-array"
version = "0.14.4"
@@ -1519,8 +2070,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [
"cfg-if 1.0.0",
+ "js-sys",
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
]
[[package]]
@@ -1529,7 +2082,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375"
dependencies = [
- "opaque-debug",
+ "opaque-debug 0.3.0",
"polyval",
]
@@ -1640,48 +2193,147 @@ dependencies = [
"syn",
]
+[[package]]
+name = "group"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912"
+dependencies = [
+ "ff",
+ "rand_core 0.6.2",
+ "subtle",
+]
+
+[[package]]
+name = "handlebars"
+version = "3.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3"
+dependencies = [
+ "log",
+ "pest",
+ "pest_derive",
+ "quick-error",
+ "serde 1.0.125",
+ "serde_json 1.0.64",
+]
+
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
+dependencies = [
+ "ahash 0.4.7",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash 0.7.4",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8"
+dependencies = [
+ "hashbrown 0.9.1",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
+dependencies = [
+ "hashbrown 0.11.2",
+]
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
+dependencies = [
+ "digest 0.9.0",
+ "hmac 0.10.1",
+]
+
+[[package]]
+name = "hmac"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
+dependencies = [
+ "crypto-mac 0.8.0",
+ "digest 0.9.0",
+]
+
+[[package]]
+name = "hmac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [
- "unicode-segmentation",
+ "crypto-mac 0.10.0",
+ "digest 0.9.0",
]
[[package]]
-name = "hermit-abi"
-version = "0.1.18"
+name = "hmac"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
dependencies = [
- "libc",
+ "crypto-mac 0.11.0",
+ "digest 0.9.0",
]
[[package]]
-name = "hkdf"
-version = "0.10.0"
+name = "hmac-sha256"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
+checksum = "bcdc571e566521512579aab40bf807c5066e1765fb36857f16ed7595c13567c6"
dependencies = [
- "digest",
- "hmac",
+ "digest 0.9.0",
]
[[package]]
-name = "hmac"
-version = "0.10.1"
+name = "hmac-sha512"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
+checksum = "77e806677ce663d0a199541030c816847b36e8dc095f70dae4a4f4ad63da5383"
dependencies = [
- "crypto-mac",
- "digest",
+ "digest 0.9.0",
]
[[package]]
@@ -1,10 +1,17 @@
[workspace]
-members = ["zed", "zed-rpc", "gpui", "gpui_macros", "fsevent", "scoped_pool"]
+members = [
+ "fsevent",
+ "gpui",
+ "gpui_macros",
+ "scoped_pool",
+ "server",
+ "zed",
+ "zed-rpc"
+]
[patch.crates-io]
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "d72771a19f4143530b1cfd23808e344f1276e176" }
-
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" }
cocoa-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" }
@@ -0,0 +1,33 @@
+# syntax = docker/dockerfile:1.2
+
+FROM rust as builder
+WORKDIR app
+RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
+RUN apt-get install -y nodejs
+COPY . .
+
+# Install script dependencies
+RUN --mount=type=cache,target=./script/node_modules \
+ cd ./script && npm install --quiet
+
+# Build CSS
+RUN --mount=type=cache,target=./script/node_modules \
+ script/build-css --release
+
+# Compile server
+RUN --mount=type=cache,target=./script/node_modules \
+ --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=./target \
+ cargo build --release --bin zed-server
+
+# Copy server binary out of cached directory
+RUN --mount=type=cache,target=./target \
+ cp /app/target/release/zed-server /app/zed-server
+
+# Copy server binary to the runtime image
+FROM debian:buster-slim as runtime
+RUN apt-get update; \
+ apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates
+WORKDIR app
+COPY --from=builder /app/zed-server /app
+ENTRYPOINT ["/app/zed-server"]
@@ -0,0 +1,15 @@
+# syntax = docker/dockerfile:1.2
+
+FROM rust as builder
+WORKDIR app
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=./target \
+ cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.5
+
+FROM debian:buster-slim as runtime
+RUN apt-get update; \
+ apt-get install -y --no-install-recommends libssl1.1
+WORKDIR app
+COPY --from=builder /app/bin/sqlx /app
+COPY ./server/migrations /app/migrations
+ENTRYPOINT ["/app/sqlx", "migrate", "run"]
@@ -8,22 +8,22 @@ version = "0.1.0"
async-task = "4.0.3"
ctor = "0.1"
etagere = "0.2"
-gpui_macros = {path = "../gpui_macros"}
+gpui_macros = { path = "../gpui_macros" }
log = "0.4"
num_cpus = "1.13"
ordered-float = "2.1.1"
parking_lot = "0.11.1"
pathfinder_color = "0.5"
pathfinder_geometry = "0.5"
-postage = {version = "0.4.1", features = ["futures-traits"]}
+postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8.3"
replace_with = "0.1.7"
resvg = "0.14"
-scoped-pool = {path = "../scoped_pool"}
+scoped-pool = { path = "../scoped_pool" }
seahash = "4.1"
-serde = {version = "1.0.125", features = ["derive"]}
+serde = { version = "1.0.125", features = ["derive"] }
serde_json = "1.0.64"
-smallvec = {version = "1.6", features = ["union"]}
+smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
tiny-skia = "0.5"
tree-sitter = "0.19"
@@ -45,7 +45,7 @@ cocoa = "0.24"
core-foundation = "0.9"
core-graphics = "0.22.2"
core-text = "19.2"
-font-kit = {git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1"}
+font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" }
foreign-types = "0.3"
log = "0.4"
metal = "0.21.0"
@@ -7,14 +7,8 @@ categories = ["parsing", "text-editors"]
repository = "https://github.com/tree-sitter/tree-sitter-javascript"
edition = "2018"
license = "MIT"
-
build = "bindings/rust/build.rs"
-include = [
- "bindings/rust/*",
- "grammar.js",
- "queries/*",
- "src/*",
-]
+include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"]
[lib]
path = "bindings/rust/lib.rs"
@@ -9,4 +9,4 @@ proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
-proc-macro2 = "1.0"
+proc-macro2 = "1.0"
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+set -e
+
+cd ./script
+[ -d node_modules ] || npm install
+if [[ $1 == --release ]]; then
+ export NODE_ENV=production # Purge unused styles in --release mode
+fi
+npx tailwindcss build ../server/styles.css --output ../server/static/styles.css
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Prerequisites:
+#
+# - Log in to the DigitalOcean docker registry
+# doctl registry login
+#
+# - Set the default K8s context to production
+# doctl kubernetes cluster kubeconfig save zed-1
+
+set -e
+
+IMAGE_ID=registry.digitalocean.com/zed/zed-server
+
+docker build . --tag $IMAGE_ID
+docker push $IMAGE_ID
+kubectl rollout restart deployment zed
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+set -e
+
+IMAGE_ID=registry.digitalocean.com/zed/zed-migrator
+
+docker build . \
+ --file ./Dockerfile.migrator \
+ --tag $IMAGE_ID
+docker push $IMAGE_ID
+kubectl apply -f ./server/migrate.yml
@@ -0,0 +1,2452 @@
+{
+ "name": "script",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "@tailwindcss/typography": "^0.4.0",
+ "tailwindcss-cli": "^0.1.2"
+ }
+ },
+ "node_modules/@fullhuman/postcss-purgecss": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz",
+ "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==",
+ "dev": true,
+ "dependencies": {
+ "purgecss": "^3.1.3"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
+ "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.4",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz",
+ "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz",
+ "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.4",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.4.0.tgz",
+ "integrity": "sha512-3BfOYT5MYNEq81Ism3L2qu/HRP2Q5vWqZtZRQqQrthHuaTK9qpuPfbMT5WATjAM5J1OePKBaI5pLoX4S1JGNMQ==",
+ "dev": true,
+ "dependencies": {
+ "lodash.castarray": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.merge": "^4.6.2",
+ "lodash.uniq": "^4.5.0"
+ },
+ "peerDependencies": {
+ "tailwindcss": "2.0.0-alpha.24 || ^2.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-node": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
+ "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^7.0.0",
+ "acorn-walk": "^7.0.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.5.tgz",
+ "integrity": "sha512-7H4AJZXvSsn62SqZyJCP+1AWwOuoYpUfK6ot9vm0e87XD6mT8lDywc9D9OTJPMULyGcvmIxzTAMeG2Cc+YX+fA==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.16.3",
+ "caniuse-lite": "^1.0.30001196",
+ "colorette": "^1.2.2",
+ "fraction.js": "^4.0.13",
+ "normalize-range": "^0.1.2",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "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/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.16.6",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
+ "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
+ "dev": true,
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001219",
+ "colorette": "^1.2.2",
+ "electron-to-chromium": "^1.3.723",
+ "escalade": "^3.1.1",
+ "node-releases": "^1.1.71"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001228",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz",
+ "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+ "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
+ "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.1",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.5.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.1"
+ }
+ },
+ "node_modules/color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
+ "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.1",
+ "color-string": "^1.5.4"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/color-string": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
+ "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/color/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/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "node_modules/colorette": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
+ "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "node_modules/css-unit-converter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
+ "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==",
+ "dev": true
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+ "dev": true
+ },
+ "node_modules/detective": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
+ "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==",
+ "dev": true,
+ "dependencies": {
+ "acorn-node": "^1.6.1",
+ "defined": "^1.0.0",
+ "minimist": "^1.1.1"
+ },
+ "bin": {
+ "detective": "bin/detective.js"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz",
+ "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=",
+ "dev": true
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.3.728",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.728.tgz",
+ "integrity": "sha512-SHv4ziXruBpb1Nz4aTuqEHBYi/9GNCJMYIJgDEXrp/2V01nFXMNFUTli5Z85f5ivSkioLilQatqBYFB44wNJrA==",
+ "dev": true
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz",
+ "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.0",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.2",
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz",
+ "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.0.tgz",
+ "integrity": "sha512-o9lSKpK0TDqDwTL24Hxqi6I99s942l6TYkfl6WvGWgLOIFz/YonSGKfiSeMadoiNvTfqnfOa9mjb5SGVbBK9/w==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-base": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
+ "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+ "dev": true,
+ "dependencies": {
+ "glob-parent": "^2.0.0",
+ "is-glob": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/glob-base/node_modules/glob-parent": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+ "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^2.0.0"
+ }
+ },
+ "node_modules/glob-base/node_modules/is-extglob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/glob-base/node_modules/is-glob": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
+ "dev": true
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/html-tags": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
+ "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+ "dev": true
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+ "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-dotfile": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
+ "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/lodash.castarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
+ "integrity": "sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU=",
+ "dev": true
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
+ "dev": true
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/lodash.toarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
+ "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=",
+ "dev": true
+ },
+ "node_modules/lodash.topath": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
+ "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
+ "dev": true
+ },
+ "node_modules/lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
+ "dev": true
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+ "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true
+ },
+ "node_modules/modern-normalize": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz",
+ "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.1.23",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
+ "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==",
+ "dev": true,
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-emoji": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
+ "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==",
+ "dev": true,
+ "dependencies": {
+ "lodash.toarray": "^4.4.0"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "1.1.72",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz",
+ "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==",
+ "dev": true
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz",
+ "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parse-glob": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
+ "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+ "dev": true,
+ "dependencies": {
+ "glob-base": "^0.3.0",
+ "is-dotfile": "^1.0.0",
+ "is-extglob": "^1.0.0",
+ "is-glob": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/parse-glob/node_modules/is-extglob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/parse-glob/node_modules/is-glob": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz",
+ "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.2.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz",
+ "integrity": "sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==",
+ "dev": true,
+ "dependencies": {
+ "colorette": "^1.2.2",
+ "nanoid": "^3.1.23",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/postcss-functions": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz",
+ "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.2",
+ "object-assign": "^4.1.1",
+ "postcss": "^6.0.9",
+ "postcss-value-parser": "^3.3.0"
+ }
+ },
+ "node_modules/postcss-functions/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/postcss-functions/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/postcss-functions/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/postcss-functions/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "node_modules/postcss-functions/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-functions/node_modules/postcss": {
+ "version": "6.0.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+ "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^2.4.1",
+ "source-map": "^0.6.1",
+ "supports-color": "^5.4.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/postcss-functions/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/postcss-functions/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/postcss-js": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz",
+ "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==",
+ "dev": true,
+ "dependencies": {
+ "camelcase-css": "^2.0.1",
+ "postcss": "^8.1.6"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.5.tgz",
+ "integrity": "sha512-GSRXYz5bccobpTzLQZXOnSOfKl6TwVr5CyAQJUPub4nuRJSOECK5AqurxVgmtxP48p0Kc/ndY/YyS1yqldX0Ew==",
+ "dev": true,
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.4"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.13"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz",
+ "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==",
+ "dev": true,
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
+ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
+ "dev": true
+ },
+ "node_modules/pretty-hrtime": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
+ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/purgecss": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz",
+ "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==",
+ "dev": true,
+ "dependencies": {
+ "commander": "^6.0.0",
+ "glob": "^7.0.0",
+ "postcss": "^8.2.1",
+ "postcss-selector-parser": "^6.0.2"
+ },
+ "bin": {
+ "purgecss": "bin/purgecss.js"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
+ "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/reduce-css-calc": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
+ "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
+ "dev": true,
+ "dependencies": {
+ "css-unit-converter": "^1.1.1",
+ "postcss-value-parser": "^3.3.0"
+ }
+ },
+ "node_modules/reduce-css-calc/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "node_modules/resolve": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.1.2.tgz",
+ "integrity": "sha512-T5t+wwd+/hsOyRw2HJuFuv0LTUm3MUdHm2DJ94GPVgzqwPPFa9XxX0KlwLWupUuiOUj6uiKURCzYPHFcuPch/w==",
+ "dev": true,
+ "dependencies": {
+ "@fullhuman/postcss-purgecss": "^3.1.3",
+ "bytes": "^3.0.0",
+ "chalk": "^4.1.0",
+ "chokidar": "^3.5.1",
+ "color": "^3.1.3",
+ "detective": "^5.2.0",
+ "didyoumean": "^1.2.1",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.2.5",
+ "fs-extra": "^9.1.0",
+ "html-tags": "^3.1.0",
+ "lodash": "^4.17.21",
+ "lodash.topath": "^4.5.2",
+ "modern-normalize": "^1.0.0",
+ "node-emoji": "^1.8.1",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^2.1.1",
+ "parse-glob": "^3.0.4",
+ "postcss-functions": "^3",
+ "postcss-js": "^3.0.3",
+ "postcss-nested": "5.0.5",
+ "postcss-selector-parser": "^6.0.4",
+ "postcss-value-parser": "^4.1.0",
+ "pretty-hrtime": "^1.0.3",
+ "quick-lru": "^5.1.1",
+ "reduce-css-calc": "^2.1.8",
+ "resolve": "^1.20.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=12.13.0"
+ },
+ "peerDependencies": {
+ "autoprefixer": "^10.0.2",
+ "postcss": "^8.0.9"
+ }
+ },
+ "node_modules/tailwindcss-cli": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss-cli/-/tailwindcss-cli-0.1.2.tgz",
+ "integrity": "sha512-17NuGSHKTr4twN1BFxuoTArMcBQH+7YL6x4PHFnmWsGNOX45O4Roc8EdMVhSSH2rQoSDoLvR4TmlfddMon3yKg==",
+ "dev": true,
+ "dependencies": {
+ "autoprefixer": "^10.0.2",
+ "postcss": "^8.1.8",
+ "tailwindcss": "^2.0.1"
+ },
+ "bin": {
+ "tailwindcss-cli": "index.js"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "dev": true
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ },
+ "dependencies": {
+ "@fullhuman/postcss-purgecss": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz",
+ "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==",
+ "dev": true,
+ "requires": {
+ "purgecss": "^3.1.3"
+ }
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
+ "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.4",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz",
+ "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==",
+ "dev": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz",
+ "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.4",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@tailwindcss/typography": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.4.0.tgz",
+ "integrity": "sha512-3BfOYT5MYNEq81Ism3L2qu/HRP2Q5vWqZtZRQqQrthHuaTK9qpuPfbMT5WATjAM5J1OePKBaI5pLoX4S1JGNMQ==",
+ "dev": true,
+ "requires": {
+ "lodash.castarray": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.merge": "^4.6.2",
+ "lodash.uniq": "^4.5.0"
+ }
+ },
+ "acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true
+ },
+ "acorn-node": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
+ "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.0.0",
+ "acorn-walk": "^7.0.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true
+ },
+ "autoprefixer": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.5.tgz",
+ "integrity": "sha512-7H4AJZXvSsn62SqZyJCP+1AWwOuoYpUfK6ot9vm0e87XD6mT8lDywc9D9OTJPMULyGcvmIxzTAMeG2Cc+YX+fA==",
+ "dev": true,
+ "requires": {
+ "browserslist": "^4.16.3",
+ "caniuse-lite": "^1.0.30001196",
+ "colorette": "^1.2.2",
+ "fraction.js": "^4.0.13",
+ "normalize-range": "^0.1.2",
+ "postcss-value-parser": "^4.1.0"
+ }
+ },
+ "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
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true
+ },
+ "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,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browserslist": {
+ "version": "4.16.6",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
+ "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
+ "dev": true,
+ "requires": {
+ "caniuse-lite": "^1.0.30001219",
+ "colorette": "^1.2.2",
+ "electron-to-chromium": "^1.3.723",
+ "escalade": "^3.1.1",
+ "node-releases": "^1.1.71"
+ }
+ },
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
+ "dev": true
+ },
+ "camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001228",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz",
+ "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+ "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "chokidar": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
+ "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.1",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.5.0"
+ }
+ },
+ "color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
+ "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.1",
+ "color-string": "^1.5.4"
+ },
+ "dependencies": {
+ "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,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ }
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "color-string": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
+ "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
+ "dev": true,
+ "requires": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "colorette": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
+ "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
+ "dev": true
+ },
+ "commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "css-unit-converter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
+ "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==",
+ "dev": true
+ },
+ "cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true
+ },
+ "defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+ "dev": true
+ },
+ "detective": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
+ "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==",
+ "dev": true,
+ "requires": {
+ "acorn-node": "^1.6.1",
+ "defined": "^1.0.0",
+ "minimist": "^1.1.1"
+ }
+ },
+ "didyoumean": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz",
+ "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=",
+ "dev": true
+ },
+ "dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true
+ },
+ "electron-to-chromium": {
+ "version": "1.3.728",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.728.tgz",
+ "integrity": "sha512-SHv4ziXruBpb1Nz4aTuqEHBYi/9GNCJMYIJgDEXrp/2V01nFXMNFUTli5Z85f5ivSkioLilQatqBYFB44wNJrA==",
+ "dev": true
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "fast-glob": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz",
+ "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.0",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.2",
+ "picomatch": "^2.2.1"
+ }
+ },
+ "fastq": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz",
+ "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==",
+ "dev": true,
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "fraction.js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.0.tgz",
+ "integrity": "sha512-o9lSKpK0TDqDwTL24Hxqi6I99s942l6TYkfl6WvGWgLOIFz/YonSGKfiSeMadoiNvTfqnfOa9mjb5SGVbBK9/w==",
+ "dev": true
+ },
+ "fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "requires": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-base": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
+ "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+ "dev": true,
+ "requires": {
+ "glob-parent": "^2.0.0",
+ "is-glob": "^2.0.0"
+ },
+ "dependencies": {
+ "glob-parent": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+ "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+ "dev": true,
+ "requires": {
+ "is-glob": "^2.0.0"
+ }
+ },
+ "is-extglob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^1.0.0"
+ }
+ }
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "html-tags": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
+ "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-core-module": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
+ "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-dotfile": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
+ "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
+ "dev": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "lodash.castarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
+ "integrity": "sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU=",
+ "dev": true
+ },
+ "lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
+ "dev": true
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "lodash.toarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
+ "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=",
+ "dev": true
+ },
+ "lodash.topath": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
+ "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
+ "dev": true
+ },
+ "lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
+ "dev": true
+ },
+ "merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+ "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true
+ },
+ "modern-normalize": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz",
+ "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==",
+ "dev": true
+ },
+ "nanoid": {
+ "version": "3.1.23",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
+ "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==",
+ "dev": true
+ },
+ "node-emoji": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
+ "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==",
+ "dev": true,
+ "requires": {
+ "lodash.toarray": "^4.4.0"
+ }
+ },
+ "node-releases": {
+ "version": "1.1.72",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz",
+ "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==",
+ "dev": true
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+ "dev": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "dev": true
+ },
+ "object-hash": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz",
+ "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "parse-glob": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
+ "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+ "dev": true,
+ "requires": {
+ "glob-base": "^0.3.0",
+ "is-dotfile": "^1.0.0",
+ "is-extglob": "^1.0.0",
+ "is-glob": "^2.0.0"
+ },
+ "dependencies": {
+ "is-extglob": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+ "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^1.0.0"
+ }
+ }
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz",
+ "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==",
+ "dev": true
+ },
+ "postcss": {
+ "version": "8.2.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz",
+ "integrity": "sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==",
+ "dev": true,
+ "requires": {
+ "colorette": "^1.2.2",
+ "nanoid": "^3.1.23",
+ "source-map": "^0.6.1"
+ }
+ },
+ "postcss-functions": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz",
+ "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.2",
+ "object-assign": "^4.1.1",
+ "postcss": "^6.0.9",
+ "postcss-value-parser": "^3.3.0"
+ },
+ "dependencies": {
+ "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,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "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,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "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,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "postcss": {
+ "version": "6.0.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+ "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.1",
+ "source-map": "^0.6.1",
+ "supports-color": "^5.4.0"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ },
+ "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,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "postcss-js": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz",
+ "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==",
+ "dev": true,
+ "requires": {
+ "camelcase-css": "^2.0.1",
+ "postcss": "^8.1.6"
+ }
+ },
+ "postcss-nested": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.5.tgz",
+ "integrity": "sha512-GSRXYz5bccobpTzLQZXOnSOfKl6TwVr5CyAQJUPub4nuRJSOECK5AqurxVgmtxP48p0Kc/ndY/YyS1yqldX0Ew==",
+ "dev": true,
+ "requires": {
+ "postcss-selector-parser": "^6.0.4"
+ }
+ },
+ "postcss-selector-parser": {
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz",
+ "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==",
+ "dev": true,
+ "requires": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
+ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
+ "dev": true
+ },
+ "pretty-hrtime": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
+ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
+ "dev": true
+ },
+ "purgecss": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz",
+ "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==",
+ "dev": true,
+ "requires": {
+ "commander": "^6.0.0",
+ "glob": "^7.0.0",
+ "postcss": "^8.2.1",
+ "postcss-selector-parser": "^6.0.2"
+ }
+ },
+ "queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true
+ },
+ "quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "dev": true
+ },
+ "readdirp": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
+ "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "reduce-css-calc": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
+ "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
+ "dev": true,
+ "requires": {
+ "css-unit-converter": "^1.1.1",
+ "postcss-value-parser": "^3.3.0"
+ },
+ "dependencies": {
+ "postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ }
+ }
+ },
+ "resolve": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ }
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true
+ },
+ "run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "tailwindcss": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.1.2.tgz",
+ "integrity": "sha512-T5t+wwd+/hsOyRw2HJuFuv0LTUm3MUdHm2DJ94GPVgzqwPPFa9XxX0KlwLWupUuiOUj6uiKURCzYPHFcuPch/w==",
+ "dev": true,
+ "requires": {
+ "@fullhuman/postcss-purgecss": "^3.1.3",
+ "bytes": "^3.0.0",
+ "chalk": "^4.1.0",
+ "chokidar": "^3.5.1",
+ "color": "^3.1.3",
+ "detective": "^5.2.0",
+ "didyoumean": "^1.2.1",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.2.5",
+ "fs-extra": "^9.1.0",
+ "html-tags": "^3.1.0",
+ "lodash": "^4.17.21",
+ "lodash.topath": "^4.5.2",
+ "modern-normalize": "^1.0.0",
+ "node-emoji": "^1.8.1",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^2.1.1",
+ "parse-glob": "^3.0.4",
+ "postcss-functions": "^3",
+ "postcss-js": "^3.0.3",
+ "postcss-nested": "5.0.5",
+ "postcss-selector-parser": "^6.0.4",
+ "postcss-value-parser": "^4.1.0",
+ "pretty-hrtime": "^1.0.3",
+ "quick-lru": "^5.1.1",
+ "reduce-css-calc": "^2.1.8",
+ "resolve": "^1.20.0"
+ }
+ },
+ "tailwindcss-cli": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss-cli/-/tailwindcss-cli-0.1.2.tgz",
+ "integrity": "sha512-17NuGSHKTr4twN1BFxuoTArMcBQH+7YL6x4PHFnmWsGNOX45O4Roc8EdMVhSSH2rQoSDoLvR4TmlfddMon3yKg==",
+ "dev": true,
+ "requires": {
+ "autoprefixer": "^10.0.2",
+ "postcss": "^8.1.8",
+ "tailwindcss": "^2.0.1"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "dev": true
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "dev": true
+ }
+ }
+}
@@ -0,0 +1,6 @@
+{
+ "devDependencies": {
+ "@tailwindcss/typography": "^0.4.0",
+ "tailwindcss-cli": "^0.1.2"
+ }
+}
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+set -e
+
+cd server
+cargo run
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+set -e
+
+# Install sqlx-cli if needed
+[[ "$(sqlx --version)" == "sqlx-cli 0.5.5" ]] || cargo install sqlx-cli --version 0.5.5
+
+# Export contents of .env.toml
+eval "$(cargo run --bin dotenv)"
+
+# Run sqlx command
+sqlx $@
@@ -0,0 +1,44 @@
+module.exports = {
+ theme: {
+ fontFamily: {
+ display: [
+ "Visby CF", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto",
+ "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji"
+ ],
+ body: [
+ "Open Sans", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto",
+ "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji"
+ ],
+ },
+ extend: {
+ typography: (theme) => ({
+ DEFAULT: {
+ css: {
+ h1: {
+ fontFamily: theme("fontFamily.display").join(", ")
+ },
+ h2: {
+ fontFamily: theme("fontFamily.display").join(", ")
+ },
+ h3: {
+ fontFamily: theme("fontFamily.display").join(", ")
+ },
+ h4: {
+ fontFamily: theme("fontFamily.display").join(", ")
+ }
+ }
+ }
+ })
+ }
+ },
+ variants: {
+ },
+ plugins: [
+ require('@tailwindcss/typography'),
+ ],
+ purge: [
+ "../templates/**/*.hbs"
+ ]
+}
@@ -0,0 +1,11 @@
+DATABASE_URL = "postgres://postgres@localhost/zed"
+SESSION_SECRET = "6E1GS6IQNOLIBKWMEVWF1AFO4H78KNU8"
+
+HTTP_PORT = 8080
+
+# Available at https://github.com/organizations/zed-industries/settings/apps/zed-local-development
+GITHUB_APP_ID = 115633
+GITHUB_CLIENT_ID = "Iv1.768076c9becc75c4"
+GITHUB_CLIENT_SECRET = ""
+GITHUB_PRIVATE_KEY = """\
+"""
@@ -0,0 +1,50 @@
+[package]
+authors = ["Nathan Sobo <nathan@warp.dev>"]
+default-run = "zed-server"
+edition = "2018"
+name = "zed-server"
+version = "0.1.0"
+
+[dependencies]
+anyhow = "1.0.40"
+async-std = { version = "1.8.0", features = ["attributes"] }
+async-trait = "0.1.50"
+async-tungstenite = "0.14"
+base64 = "0.13"
+clap = "=3.0.0-beta.2"
+comrak = "0.10"
+either = "1.6"
+envy = "0.4.2"
+futures = "0.3"
+handlebars = "3.5"
+http-auth-basic = "0.1.3"
+jwt-simple = "0.10.0"
+oauth2 = { version = "4.0.0", default_features = false }
+oauth2-surf = "0.1.1"
+parking_lot = "0.11.1"
+postage = { version = "0.4.1", features = ["futures-traits"] }
+rand = "0.8"
+rust-embed = "5.9.0"
+scrypt = "0.7"
+serde = { version = "1.0", features = ["derive"] }
+sha-1 = "0.9"
+surf = "2.2.0"
+tide = "0.16.0"
+tide-compress = "0.9.0"
+toml = "0.5.8"
+zed-rpc = { path = "../zed-rpc" }
+
+[dependencies.async-sqlx-session]
+version = "0.3.0"
+features = ["pg", "rustls"]
+default-features = false
+
+[dependencies.sqlx]
+version = "0.5.2"
+features = ["runtime-async-std-rustls", "postgres"]
+
+[dev-dependencies]
+gpui = { path = "../gpui" }
+zed = { path = "../zed", features = ["test-support"] }
+lazy_static = "1.4"
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
@@ -0,0 +1,2 @@
+web: ./target/release/zed-server
+release: ./target/release/sqlx migrate run
@@ -0,0 +1,17 @@
+# Zed Server
+
+This crate is what we run at https://zed.dev.
+
+It contains our web presence as well as the backend logic for collaboration, to which we connect from the Zed client via a websocket.
+
+## Templates
+
+We use handlebars templates that are interpreted at runtime. When running in debug mode, you can change templates and see the latest content without restarting the server. This is enabled by the `rust-embed` crate, which we use to access the contents of the `/templates` folder at runtime. In debug mode it reads contents from the file system, but in release the templates will be embedded in the server binary.
+
+## Static assets
+
+We also use `rust-embed` to access the contents of the `/static` folder via the `/static/*` route. The app will pick up changes to the contents of this folder when running in debug mode.
+
+## CSS
+
+This site uses Tailwind CSS, which means our stylesheets don't need to change very frequently. We check `static/styles.css` into the repository, but it's actually compiled from `/styles.css` via `script/build-css`. This script runs the Tailwind compilation flow to regenerate `static/styles.css` via PostCSS.
@@ -0,0 +1,12 @@
+
+[Interface]
+PrivateKey = B5Fp/yVfP0QYlb+YJv9ea+EMI1mWODPD3akh91cVjvc=
+Address = fdaa:0:2ce3:a7b:bea:0:a:2/120
+DNS = fdaa:0:2ce3::3
+
+[Peer]
+PublicKey = RKAYPljEJiuaELNDdQIEJmQienT9+LRISfIHwH45HAw=
+AllowedIPs = fdaa:0:2ce3::/48
+Endpoint = ord1.gateway.6pn.dev:51820
+PersistentKeepalive = 15
+
@@ -0,0 +1,71 @@
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: zed
+ annotations:
+ service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
+ service.beta.kubernetes.io/do-loadbalancer-certificate-id: "606e2db9-2b58-4ae7-b12c-a0c7d56af49b"
+spec:
+ type: LoadBalancer
+ selector:
+ app: zed
+ ports:
+ - name: web
+ protocol: TCP
+ port: 443
+ targetPort: 8080
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: zed
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: zed
+ template:
+ metadata:
+ labels:
+ app: zed
+ spec:
+ containers:
+ - name: zed
+ image: registry.digitalocean.com/zed/zed-server
+ ports:
+ - containerPort: 8080
+ protocol: TCP
+ env:
+ - name: HTTP_PORT
+ value: "8080"
+ - name: DATABASE_URL
+ valueFrom:
+ secretKeyRef:
+ name: database
+ key: url
+ - name: SESSION_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: session
+ key: secret
+ - name: GITHUB_APP_ID
+ valueFrom:
+ secretKeyRef:
+ name: github
+ key: appId
+ - name: GITHUB_CLIENT_ID
+ valueFrom:
+ secretKeyRef:
+ name: github
+ key: clientId
+ - name: GITHUB_CLIENT_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: github
+ key: clientSecret
+ - name: GITHUB_PRIVATE_KEY
+ valueFrom:
+ secretKeyRef:
+ name: github
+ key: privateKey
@@ -0,0 +1,17 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: migrate
+spec:
+ template:
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: migrator
+ image: registry.digitalocean.com/zed/zed-migrator
+ env:
+ - name: DATABASE_URL
+ valueFrom:
+ secretKeyRef:
+ name: database
+ key: url
@@ -0,0 +1,26 @@
+CREATE TABLE IF NOT EXISTS "sessions" (
+ "id" VARCHAR NOT NULL PRIMARY KEY,
+ "expires" TIMESTAMP WITH TIME ZONE NULL,
+ "session" TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS "users" (
+ "id" SERIAL PRIMARY KEY,
+ "github_login" VARCHAR,
+ "admin" BOOLEAN
+);
+
+CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
+
+CREATE TABLE IF NOT EXISTS "signups" (
+ "id" SERIAL PRIMARY KEY,
+ "github_login" VARCHAR,
+ "email_address" VARCHAR,
+ "about" TEXT
+);
+
+INSERT INTO users (github_login, admin)
+VALUES
+ ('nathansobo', true),
+ ('maxbrunsfeld', true),
+ ('as-cii', true);
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS "access_tokens" (
+ "id" SERIAL PRIMARY KEY,
+ "user_id" INTEGER REFERENCES users (id),
+ "hash" VARCHAR(128)
+);
+
+CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id");
@@ -0,0 +1,160 @@
+use crate::{auth::RequestExt as _, AppState, DbPool, LayoutData, Request, RequestExt as _};
+use async_trait::async_trait;
+use serde::{Deserialize, Serialize};
+use sqlx::{Executor, FromRow};
+use std::sync::Arc;
+use surf::http::mime;
+
+#[async_trait]
+pub trait RequestExt {
+ async fn require_admin(&self) -> tide::Result<()>;
+}
+
+#[async_trait]
+impl RequestExt for Request {
+ async fn require_admin(&self) -> tide::Result<()> {
+ let current_user = self
+ .current_user()
+ .await?
+ .ok_or_else(|| tide::Error::from_str(401, "not logged in"))?;
+
+ if current_user.is_admin {
+ Ok(())
+ } else {
+ Err(tide::Error::from_str(
+ 403,
+ "authenticated user is not an admin",
+ ))
+ }
+ }
+}
+
+pub fn add_routes(app: &mut tide::Server<Arc<AppState>>) {
+ app.at("/admin").get(get_admin_page);
+ app.at("/users").post(post_user);
+ app.at("/users/:id").put(put_user);
+ app.at("/users/:id/delete").post(delete_user);
+ app.at("/signups/:id/delete").post(delete_signup);
+}
+
+#[derive(Serialize)]
+struct AdminData {
+ #[serde(flatten)]
+ layout: Arc<LayoutData>,
+ users: Vec<User>,
+ signups: Vec<Signup>,
+}
+
+#[derive(Debug, FromRow, Serialize)]
+pub struct User {
+ pub id: i32,
+ pub github_login: String,
+ pub admin: bool,
+}
+
+#[derive(Debug, FromRow, Serialize)]
+pub struct Signup {
+ pub id: i32,
+ pub github_login: String,
+ pub email_address: String,
+ pub about: String,
+}
+
+async fn get_admin_page(mut request: Request) -> tide::Result {
+ request.require_admin().await?;
+
+ let data = AdminData {
+ layout: request.layout_data().await?,
+ users: sqlx::query_as("SELECT * FROM users ORDER BY github_login ASC")
+ .fetch_all(request.db())
+ .await?,
+ signups: sqlx::query_as("SELECT * FROM signups ORDER BY id DESC")
+ .fetch_all(request.db())
+ .await?,
+ };
+
+ Ok(tide::Response::builder(200)
+ .body(request.state().render_template("admin.hbs", &data)?)
+ .content_type(mime::HTML)
+ .build())
+}
+
+async fn post_user(mut request: Request) -> tide::Result {
+ request.require_admin().await?;
+
+ #[derive(Deserialize)]
+ struct Form {
+ github_login: String,
+ #[serde(default)]
+ admin: bool,
+ }
+
+ let form = request.body_form::<Form>().await?;
+ let github_login = form
+ .github_login
+ .strip_prefix("@")
+ .unwrap_or(&form.github_login);
+
+ if !github_login.is_empty() {
+ create_user(request.db(), github_login, form.admin).await?;
+ }
+
+ Ok(tide::Redirect::new("/admin").into())
+}
+
+async fn put_user(mut request: Request) -> tide::Result {
+ request.require_admin().await?;
+
+ let user_id = request.param("id")?.parse::<i32>()?;
+
+ #[derive(Deserialize)]
+ struct Body {
+ admin: bool,
+ }
+
+ let body: Body = request.body_json().await?;
+
+ request
+ .db()
+ .execute(
+ sqlx::query("UPDATE users SET admin = $1 WHERE id = $2;")
+ .bind(body.admin)
+ .bind(user_id),
+ )
+ .await?;
+
+ Ok(tide::Response::builder(200).build())
+}
+
+async fn delete_user(request: Request) -> tide::Result {
+ request.require_admin().await?;
+
+ let user_id = request.param("id")?.parse::<i32>()?;
+ request
+ .db()
+ .execute(sqlx::query("DELETE FROM users WHERE id = $1;").bind(user_id))
+ .await?;
+
+ Ok(tide::Redirect::new("/admin").into())
+}
+
+pub async fn create_user(db: &DbPool, github_login: &str, admin: bool) -> tide::Result<i32> {
+ let id: i32 =
+ sqlx::query_scalar("INSERT INTO users (github_login, admin) VALUES ($1, $2) RETURNING id;")
+ .bind(github_login)
+ .bind(admin)
+ .fetch_one(db)
+ .await?;
+ Ok(id)
+}
+
+async fn delete_signup(request: Request) -> tide::Result {
+ request.require_admin().await?;
+ let signup_id = request.param("id")?.parse::<i32>()?;
+ request
+ .db()
+ .execute(sqlx::query("DELETE FROM signups WHERE id = $1;").bind(signup_id))
+ .await?;
+
+ Ok(tide::Redirect::new("/admin").into())
+}
@@ -0,0 +1,31 @@
+use crate::{AppState, Request};
+use anyhow::anyhow;
+use rust_embed::RustEmbed;
+use std::sync::Arc;
+use tide::{http::mime, Server};
+
+#[derive(RustEmbed)]
+#[folder = "static"]
+struct Static;
+
+pub fn add_routes(app: &mut Server<Arc<AppState>>) {
+ app.at("/static/*path").get(get_static_asset);
+}
+
+async fn get_static_asset(request: Request) -> tide::Result {
+ let path = request.param("path").unwrap();
+ let content = Static::get(path).ok_or_else(|| anyhow!("asset not found at {}", path))?;
+
+ let content_type = if path.starts_with("svg") {
+ mime::SVG
+ } else if path.starts_with("styles") {
+ mime::CSS
+ } else {
+ mime::BYTE_STREAM
+ };
+
+ Ok(tide::Response::builder(200)
+ .content_type(content_type)
+ .body(content.as_ref())
+ .build())
+}
@@ -0,0 +1,336 @@
+use super::errors::TideResultExt;
+use crate::{github, rpc, AppState, DbPool, Request, RequestExt as _};
+use anyhow::{anyhow, Context};
+use async_std::stream::StreamExt;
+use async_trait::async_trait;
+pub use oauth2::basic::BasicClient as Client;
+use oauth2::{
+ AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, RedirectUrl,
+ TokenResponse as _, TokenUrl,
+};
+use rand::thread_rng;
+use scrypt::{
+ password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
+ Scrypt,
+};
+use serde::{Deserialize, Serialize};
+use sqlx::FromRow;
+use std::{borrow::Cow, convert::TryFrom, sync::Arc};
+use surf::Url;
+use tide::Server;
+use zed_rpc::{auth as zed_auth, proto, Peer};
+
+static CURRENT_GITHUB_USER: &'static str = "current_github_user";
+static GITHUB_AUTH_URL: &'static str = "https://github.com/login/oauth/authorize";
+static GITHUB_TOKEN_URL: &'static str = "https://github.com/login/oauth/access_token";
+
+#[derive(Serialize)]
+pub struct User {
+ pub github_login: String,
+ pub avatar_url: String,
+ pub is_insider: bool,
+ pub is_admin: bool,
+}
+
+pub struct VerifyToken;
+
+#[derive(Clone, Copy)]
+pub struct UserId(pub i32);
+
+#[async_trait]
+impl tide::Middleware<Arc<AppState>> for VerifyToken {
+ async fn handle(
+ &self,
+ mut request: Request,
+ next: tide::Next<'_, Arc<AppState>>,
+ ) -> tide::Result {
+ let mut auth_header = request
+ .header("Authorization")
+ .ok_or_else(|| anyhow!("no authorization header"))?
+ .last()
+ .as_str()
+ .split_whitespace();
+
+ let user_id: i32 = auth_header
+ .next()
+ .ok_or_else(|| anyhow!("missing user id in authorization header"))?
+ .parse()?;
+ let access_token = auth_header
+ .next()
+ .ok_or_else(|| anyhow!("missing access token in authorization header"))?;
+
+ let state = request.state().clone();
+
+ let mut password_hashes =
+ sqlx::query_scalar::<_, String>("SELECT hash FROM access_tokens WHERE user_id = $1")
+ .bind(&user_id)
+ .fetch_many(&state.db);
+
+ let mut credentials_valid = false;
+ while let Some(password_hash) = password_hashes.next().await {
+ if let either::Either::Right(password_hash) = password_hash? {
+ if verify_access_token(&access_token, &password_hash)? {
+ credentials_valid = true;
+ break;
+ }
+ }
+ }
+
+ if credentials_valid {
+ request.set_ext(UserId(user_id));
+ Ok(next.run(request).await)
+ } else {
+ Err(anyhow!("invalid credentials").into())
+ }
+ }
+}
+
+#[async_trait]
+pub trait RequestExt {
+ async fn current_user(&self) -> tide::Result<Option<User>>;
+}
+
+#[async_trait]
+impl RequestExt for Request {
+ async fn current_user(&self) -> tide::Result<Option<User>> {
+ if let Some(details) = self.session().get::<github::User>(CURRENT_GITHUB_USER) {
+ #[derive(FromRow)]
+ struct UserRow {
+ admin: bool,
+ }
+
+ let user_row: Option<UserRow> =
+ sqlx::query_as("SELECT admin FROM users WHERE github_login = $1")
+ .bind(&details.login)
+ .fetch_optional(self.db())
+ .await?;
+
+ let is_insider = user_row.is_some();
+ let is_admin = user_row.map_or(false, |row| row.admin);
+
+ Ok(Some(User {
+ github_login: details.login,
+ avatar_url: details.avatar_url,
+ is_insider,
+ is_admin,
+ }))
+ } else {
+ Ok(None)
+ }
+ }
+}
+
+#[async_trait]
+pub trait PeerExt {
+ async fn sign_out(
+ self: &Arc<Self>,
+ connection_id: zed_rpc::ConnectionId,
+ state: &AppState,
+ ) -> tide::Result<()>;
+}
+
+#[async_trait]
+impl PeerExt for Peer {
+ async fn sign_out(
+ self: &Arc<Self>,
+ connection_id: zed_rpc::ConnectionId,
+ state: &AppState,
+ ) -> tide::Result<()> {
+ self.disconnect(connection_id).await;
+ let worktree_ids = state.rpc.write().await.remove_connection(connection_id);
+ for worktree_id in worktree_ids {
+ let state = state.rpc.read().await;
+ if let Some(worktree) = state.worktrees.get(&worktree_id) {
+ rpc::broadcast(connection_id, worktree.connection_ids(), |conn_id| {
+ self.send(
+ conn_id,
+ proto::RemovePeer {
+ worktree_id,
+ peer_id: connection_id.0,
+ },
+ )
+ })
+ .await?;
+ }
+ }
+ Ok(())
+ }
+}
+
+pub fn build_client(client_id: &str, client_secret: &str) -> Client {
+ Client::new(
+ ClientId::new(client_id.to_string()),
+ Some(oauth2::ClientSecret::new(client_secret.to_string())),
+ AuthUrl::new(GITHUB_AUTH_URL.into()).unwrap(),
+ Some(TokenUrl::new(GITHUB_TOKEN_URL.into()).unwrap()),
+ )
+}
+
+pub fn add_routes(app: &mut Server<Arc<AppState>>) {
+ app.at("/sign_in").get(get_sign_in);
+ app.at("/sign_out").post(post_sign_out);
+ app.at("/auth_callback").get(get_auth_callback);
+}
+
+#[derive(Debug, Deserialize)]
+struct NativeAppSignInParams {
+ native_app_port: String,
+ native_app_public_key: String,
+}
+
+async fn get_sign_in(mut request: Request) -> tide::Result {
+ let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
+
+ request
+ .session_mut()
+ .insert("pkce_verifier", pkce_verifier)?;
+
+ let mut redirect_url = Url::parse(&format!(
+ "{}://{}/auth_callback",
+ request
+ .header("X-Forwarded-Proto")
+ .and_then(|values| values.get(0))
+ .map(|value| value.as_str())
+ .unwrap_or("http"),
+ request.host().unwrap()
+ ))?;
+
+ let app_sign_in_params: Option<NativeAppSignInParams> = request.query().ok();
+ if let Some(query) = app_sign_in_params {
+ redirect_url
+ .query_pairs_mut()
+ .clear()
+ .append_pair("native_app_port", &query.native_app_port)
+ .append_pair("native_app_public_key", &query.native_app_public_key);
+ }
+
+ let (auth_url, csrf_token) = request
+ .state()
+ .auth_client
+ .authorize_url(CsrfToken::new_random)
+ .set_redirect_uri(Cow::Owned(RedirectUrl::from_url(redirect_url)))
+ .set_pkce_challenge(pkce_challenge)
+ .url();
+
+ request
+ .session_mut()
+ .insert("auth_csrf_token", csrf_token)?;
+
+ Ok(tide::Redirect::new(auth_url).into())
+}
+
+async fn get_auth_callback(mut request: Request) -> tide::Result {
+ #[derive(Debug, Deserialize)]
+ struct Query {
+ code: String,
+ state: String,
+
+ #[serde(flatten)]
+ native_app_sign_in_params: Option<NativeAppSignInParams>,
+ }
+
+ let query: Query = request.query()?;
+
+ let pkce_verifier = request
+ .session()
+ .get("pkce_verifier")
+ .ok_or_else(|| anyhow!("could not retrieve pkce_verifier from session"))?;
+
+ let csrf_token = request
+ .session()
+ .get::<CsrfToken>("auth_csrf_token")
+ .ok_or_else(|| anyhow!("could not retrieve auth_csrf_token from session"))?;
+
+ if &query.state != csrf_token.secret() {
+ return Err(anyhow!("csrf token does not match").into());
+ }
+
+ let github_access_token = request
+ .state()
+ .auth_client
+ .exchange_code(AuthorizationCode::new(query.code))
+ .set_pkce_verifier(pkce_verifier)
+ .request_async(oauth2_surf::http_client)
+ .await
+ .context("failed to exchange oauth code")?
+ .access_token()
+ .secret()
+ .clone();
+
+ let user_details = request
+ .state()
+ .github_client
+ .user(github_access_token)
+ .details()
+ .await
+ .context("failed to fetch user")?;
+
+ let user_id: Option<i32> = sqlx::query_scalar("SELECT id from users where github_login = $1")
+ .bind(&user_details.login)
+ .fetch_optional(request.db())
+ .await?;
+
+ request
+ .session_mut()
+ .insert(CURRENT_GITHUB_USER, user_details.clone())?;
+
+ // When signing in from the native app, generate a new access token for the current user. Return
+ // a redirect so that the user's browser sends this access token to the locally-running app.
+ if let Some((user_id, app_sign_in_params)) = user_id.zip(query.native_app_sign_in_params) {
+ let access_token = create_access_token(request.db(), user_id).await?;
+ let native_app_public_key =
+ zed_auth::PublicKey::try_from(app_sign_in_params.native_app_public_key.clone())
+ .context("failed to parse app public key")?;
+ let encrypted_access_token = native_app_public_key
+ .encrypt_string(&access_token)
+ .context("failed to encrypt access token with public key")?;
+
+ return Ok(tide::Redirect::new(&format!(
+ "http://127.0.0.1:{}?user_id={}&access_token={}",
+ app_sign_in_params.native_app_port, user_id, encrypted_access_token,
+ ))
+ .into());
+ }
+
+ Ok(tide::Redirect::new("/").into())
+}
+
+async fn post_sign_out(mut request: Request) -> tide::Result {
+ request.session_mut().remove(CURRENT_GITHUB_USER);
+ Ok(tide::Redirect::new("/").into())
+}
+
+pub async fn create_access_token(db: &DbPool, user_id: i32) -> tide::Result<String> {
+ let access_token = zed_auth::random_token();
+ let access_token_hash =
+ hash_access_token(&access_token).context("failed to hash access token")?;
+ sqlx::query("INSERT INTO access_tokens (user_id, hash) values ($1, $2)")
+ .bind(user_id)
+ .bind(access_token_hash)
+ .fetch_optional(db)
+ .await?;
+ Ok(access_token)
+}
+
+fn hash_access_token(token: &str) -> tide::Result<String> {
+ // Avoid slow hashing in debug mode.
+ let params = if cfg!(debug_assertions) {
+ scrypt::Params::new(1, 1, 1).unwrap()
+ } else {
+ scrypt::Params::recommended()
+ };
+
+ Ok(Scrypt
+ .hash_password(
+ token.as_bytes(),
+ None,
+ params,
+ &SaltString::generate(thread_rng()),
+ )?
+ .to_string())
+}
+
+pub fn verify_access_token(token: &str, hash: &str) -> tide::Result<bool> {
+ let hash = PasswordHash::new(hash)?;
+ Ok(Scrypt.verify_password(token.as_bytes(), &hash).is_ok())
+}
@@ -0,0 +1,20 @@
+use anyhow::anyhow;
+use std::fs;
+
+fn main() -> anyhow::Result<()> {
+ let env: toml::map::Map<String, toml::Value> = toml::de::from_str(
+ &fs::read_to_string("./.env.toml").map_err(|_| anyhow!("no .env.toml file found"))?,
+ )?;
+
+ for (key, value) in env {
+ let value = match value {
+ toml::Value::String(value) => value,
+ toml::Value::Integer(value) => value.to_string(),
+ toml::Value::Float(value) => value.to_string(),
+ _ => panic!("unsupported TOML value in .env.toml for key {}", key),
+ };
+ println!("export {}=\"{}\"", key, value);
+ }
+
+ Ok(())
+}
@@ -0,0 +1,20 @@
+use anyhow::anyhow;
+use std::fs;
+
+pub fn load_dotenv() -> anyhow::Result<()> {
+ let env: toml::map::Map<String, toml::Value> = toml::de::from_str(
+ &fs::read_to_string("./.env.toml").map_err(|_| anyhow!("no .env.toml file found"))?,
+ )?;
+
+ for (key, value) in env {
+ let value = match value {
+ toml::Value::String(value) => value,
+ toml::Value::Integer(value) => value.to_string(),
+ toml::Value::Float(value) => value.to_string(),
+ _ => panic!("unsupported TOML value in .env.toml for key {}", key),
+ };
+ std::env::set_var(key, value);
+ }
+
+ Ok(())
+}
@@ -0,0 +1,73 @@
+use crate::{AppState, LayoutData, Request, RequestExt};
+use async_trait::async_trait;
+use serde::Serialize;
+use std::sync::Arc;
+use tide::http::mime;
+
+pub struct Middleware;
+
+#[async_trait]
+impl tide::Middleware<Arc<AppState>> for Middleware {
+ async fn handle(
+ &self,
+ mut request: Request,
+ next: tide::Next<'_, Arc<AppState>>,
+ ) -> tide::Result {
+ let app = request.state().clone();
+ let layout_data = request.layout_data().await?;
+
+ let mut response = next.run(request).await;
+
+ #[derive(Serialize)]
+ struct ErrorData {
+ #[serde(flatten)]
+ layout: Arc<LayoutData>,
+ status: u16,
+ reason: &'static str,
+ }
+
+ if !response.status().is_success() {
+ response.set_body(app.render_template(
+ "error.hbs",
+ &ErrorData {
+ layout: layout_data,
+ status: response.status().into(),
+ reason: response.status().canonical_reason(),
+ },
+ )?);
+ response.set_content_type(mime::HTML);
+ }
+
+ Ok(response)
+ }
+}
+
+// Allow tide Results to accept context like other Results do when
+// using anyhow.
+pub trait TideResultExt {
+ fn context<C>(self, cx: C) -> Self
+ where
+ C: std::fmt::Display + Send + Sync + 'static;
+
+ fn with_context<C, F>(self, f: F) -> Self
+ where
+ C: std::fmt::Display + Send + Sync + 'static,
+ F: FnOnce() -> C;
+}
+
+impl<T> TideResultExt for tide::Result<T> {
+ fn context<C>(self, cx: C) -> Self
+ where
+ C: std::fmt::Display + Send + Sync + 'static,
+ {
+ self.map_err(|e| tide::Error::new(e.status(), e.into_inner().context(cx)))
+ }
+
+ fn with_context<C, F>(self, f: F) -> Self
+ where
+ C: std::fmt::Display + Send + Sync + 'static,
+ F: FnOnce() -> C,
+ {
+ self.map_err(|e| tide::Error::new(e.status(), e.into_inner().context(f())))
+ }
+}
@@ -0,0 +1,43 @@
+use std::{future::Future, time::Instant};
+
+use async_std::sync::Mutex;
+
+#[derive(Default)]
+pub struct Expiring<T>(Mutex<Option<ExpiringState<T>>>);
+
+pub struct ExpiringState<T> {
+ value: T,
+ expires_at: Instant,
+}
+
+impl<T: Clone> Expiring<T> {
+ pub async fn get_or_refresh<F, G>(&self, f: F) -> tide::Result<T>
+ where
+ F: FnOnce() -> G,
+ G: Future<Output = tide::Result<(T, Instant)>>,
+ {
+ let mut state = self.0.lock().await;
+
+ if let Some(state) = state.as_mut() {
+ if Instant::now() >= state.expires_at {
+ let (value, expires_at) = f().await?;
+ state.value = value.clone();
+ state.expires_at = expires_at;
+ Ok(value)
+ } else {
+ Ok(state.value.clone())
+ }
+ } else {
+ let (value, expires_at) = f().await?;
+ *state = Some(ExpiringState {
+ value: value.clone(),
+ expires_at,
+ });
+ Ok(value)
+ }
+ }
+
+ pub async fn clear(&self) {
+ self.0.lock().await.take();
+ }
+}
@@ -0,0 +1,265 @@
+use crate::expiring::Expiring;
+use anyhow::{anyhow, Context};
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use std::{
+ future::Future,
+ sync::Arc,
+ time::{Duration, Instant},
+};
+use surf::{http::Method, RequestBuilder, Url};
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Release {
+ pub tag_name: String,
+ pub name: String,
+ pub body: String,
+ pub draft: bool,
+ pub assets: Vec<Asset>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct Asset {
+ pub name: String,
+ pub url: String,
+}
+
+pub struct AppClient {
+ id: usize,
+ private_key: String,
+ jwt_bearer_header: Expiring<String>,
+}
+
+#[derive(Deserialize)]
+struct Installation {
+ #[allow(unused)]
+ id: usize,
+}
+
+impl AppClient {
+ #[cfg(test)]
+ pub fn test() -> Arc<Self> {
+ Arc::new(Self {
+ id: Default::default(),
+ private_key: Default::default(),
+ jwt_bearer_header: Default::default(),
+ })
+ }
+
+ pub fn new(id: usize, private_key: String) -> Arc<Self> {
+ Arc::new(Self {
+ id,
+ private_key,
+ jwt_bearer_header: Default::default(),
+ })
+ }
+
+ pub async fn repo(self: &Arc<Self>, nwo: String) -> tide::Result<RepoClient> {
+ let installation: Installation = self
+ .request(
+ Method::Get,
+ &format!("/repos/{}/installation", &nwo),
+ |refresh| self.bearer_header(refresh),
+ )
+ .await?;
+
+ Ok(RepoClient {
+ app: self.clone(),
+ nwo,
+ installation_id: installation.id,
+ installation_token_header: Default::default(),
+ })
+ }
+
+ pub fn user(self: &Arc<Self>, access_token: String) -> UserClient {
+ UserClient {
+ app: self.clone(),
+ access_token,
+ }
+ }
+
+ async fn request<T, F, G>(
+ &self,
+ method: Method,
+ path: &str,
+ get_auth_header: F,
+ ) -> tide::Result<T>
+ where
+ T: DeserializeOwned,
+ F: Fn(bool) -> G,
+ G: Future<Output = tide::Result<String>>,
+ {
+ let mut retried = false;
+
+ loop {
+ let response = RequestBuilder::new(
+ method,
+ Url::parse(&format!("https://api.github.com{}", path))?,
+ )
+ .header("Accept", "application/vnd.github.v3+json")
+ .header("Authorization", get_auth_header(retried).await?)
+ .recv_json()
+ .await;
+
+ if let Err(error) = response.as_ref() {
+ if error.status() == 401 && !retried {
+ retried = true;
+ continue;
+ }
+ }
+
+ return response;
+ }
+ }
+
+ async fn bearer_header(&self, refresh: bool) -> tide::Result<String> {
+ if refresh {
+ self.jwt_bearer_header.clear().await;
+ }
+
+ self.jwt_bearer_header
+ .get_or_refresh(|| async {
+ use jwt_simple::{algorithms::RS256KeyPair, prelude::*};
+ use std::time;
+
+ let key_pair = RS256KeyPair::from_pem(&self.private_key)
+ .with_context(|| format!("invalid private key {:?}", self.private_key))?;
+ let mut claims = Claims::create(Duration::from_mins(10));
+ claims.issued_at = Some(Clock::now_since_epoch() - Duration::from_mins(1));
+ claims.issuer = Some(self.id.to_string());
+ let token = key_pair.sign(claims).context("failed to sign claims")?;
+ let expires_at = time::Instant::now() + time::Duration::from_secs(9 * 60);
+
+ Ok((format!("Bearer {}", token), expires_at))
+ })
+ .await
+ }
+
+ async fn installation_token_header(
+ &self,
+ header: &Expiring<String>,
+ installation_id: usize,
+ refresh: bool,
+ ) -> tide::Result<String> {
+ if refresh {
+ header.clear().await;
+ }
+
+ header
+ .get_or_refresh(|| async {
+ #[derive(Debug, Deserialize)]
+ struct AccessToken {
+ token: String,
+ }
+
+ let access_token: AccessToken = self
+ .request(
+ Method::Post,
+ &format!("/app/installations/{}/access_tokens", installation_id),
+ |refresh| self.bearer_header(refresh),
+ )
+ .await?;
+
+ let header = format!("Token {}", access_token.token);
+ let expires_at = Instant::now() + Duration::from_secs(60 * 30);
+
+ Ok((header, expires_at))
+ })
+ .await
+ }
+}
+
+pub struct RepoClient {
+ app: Arc<AppClient>,
+ nwo: String,
+ installation_id: usize,
+ installation_token_header: Expiring<String>,
+}
+
+impl RepoClient {
+ #[cfg(test)]
+ pub fn test(app_client: &Arc<AppClient>) -> Self {
+ Self {
+ app: app_client.clone(),
+ nwo: String::new(),
+ installation_id: 0,
+ installation_token_header: Default::default(),
+ }
+ }
+
+ pub async fn releases(&self) -> tide::Result<Vec<Release>> {
+ self.get(&format!("/repos/{}/releases?per_page=100", self.nwo))
+ .await
+ }
+
+ pub async fn release_asset(&self, tag: &str, name: &str) -> tide::Result<surf::Body> {
+ let release: Release = self
+ .get(&format!("/repos/{}/releases/tags/{}", self.nwo, tag))
+ .await?;
+
+ let asset = release
+ .assets
+ .iter()
+ .find(|asset| asset.name == name)
+ .ok_or_else(|| anyhow!("no asset found with name {}", name))?;
+
+ let request = surf::get(&asset.url)
+ .header("Accept", "application/octet-stream'")
+ .header(
+ "Authorization",
+ self.installation_token_header(false).await?,
+ );
+ let client = surf::client().with(surf::middleware::Redirect::new(5));
+ let mut response = client.send(request).await?;
+
+ Ok(response.take_body())
+ }
+
+ async fn get<T: DeserializeOwned>(&self, path: &str) -> tide::Result<T> {
+ self.request::<T>(Method::Get, path).await
+ }
+
+ async fn request<T: DeserializeOwned>(&self, method: Method, path: &str) -> tide::Result<T> {
+ Ok(self
+ .app
+ .request(method, path, |refresh| {
+ self.installation_token_header(refresh)
+ })
+ .await?)
+ }
+
+ async fn installation_token_header(&self, refresh: bool) -> tide::Result<String> {
+ self.app
+ .installation_token_header(
+ &self.installation_token_header,
+ self.installation_id,
+ refresh,
+ )
+ .await
+ }
+}
+
+pub struct UserClient {
+ app: Arc<AppClient>,
+ access_token: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct User {
+ pub login: String,
+ pub avatar_url: String,
+}
+
+impl UserClient {
+ pub async fn details(&self) -> tide::Result<User> {
+ Ok(self
+ .app
+ .request(Method::Get, "/user", |_| async {
+ Ok(self.access_token_header())
+ })
+ .await?)
+ }
+
+ fn access_token_header(&self) -> String {
+ format!("Token {}", self.access_token)
+ }
+}
@@ -0,0 +1,112 @@
+use crate::{
+ auth::RequestExt as _, github::Release, AppState, LayoutData, Request, RequestExt as _,
+};
+use comrak::ComrakOptions;
+use serde::{Deserialize, Serialize};
+use sqlx::Executor as _;
+use std::sync::Arc;
+use tide::{http::mime, log, Server};
+
+pub fn add_routes(app: &mut Server<Arc<AppState>>) {
+ app.at("/").get(get_home);
+ app.at("/signups").post(post_signup);
+ app.at("/releases/:tag_name/:name").get(get_release_asset);
+}
+
+async fn get_home(mut request: Request) -> tide::Result {
+ #[derive(Serialize)]
+ struct HomeData {
+ #[serde(flatten)]
+ layout: Arc<LayoutData>,
+ releases: Option<Vec<Release>>,
+ }
+
+ let mut data = HomeData {
+ layout: request.layout_data().await?,
+ releases: None,
+ };
+
+ if let Some(user) = request.current_user().await? {
+ if user.is_insider {
+ data.releases = Some(
+ request
+ .state()
+ .repo_client
+ .releases()
+ .await?
+ .into_iter()
+ .filter_map(|mut release| {
+ if release.draft {
+ None
+ } else {
+ let mut options = ComrakOptions::default();
+ options.render.unsafe_ = true; // Allow raw HTML in the markup. We control these release notes anyway.
+ release.body = comrak::markdown_to_html(&release.body, &options);
+ Some(release)
+ }
+ })
+ .collect(),
+ );
+ }
+ }
+
+ Ok(tide::Response::builder(200)
+ .body(request.state().render_template("home.hbs", &data)?)
+ .content_type(mime::HTML)
+ .build())
+}
+
+async fn post_signup(mut request: Request) -> tide::Result {
+ #[derive(Debug, Deserialize)]
+ struct Form {
+ github_login: String,
+ email_address: String,
+ about: String,
+ }
+
+ let mut form: Form = request.body_form().await?;
+ form.github_login = form
+ .github_login
+ .strip_prefix("@")
+ .map(str::to_string)
+ .unwrap_or(form.github_login);
+
+ log::info!("Signup submitted: {:?}", form);
+
+ // Save signup in the database
+ request
+ .db()
+ .execute(
+ sqlx::query(
+ "INSERT INTO signups (github_login, email_address, about) VALUES ($1, $2, $3);",
+ )
+ .bind(&form.github_login)
+ .bind(&form.email_address)
+ .bind(&form.about),
+ )
+ .await?;
+
+ let layout_data = request.layout_data().await?;
+ Ok(tide::Response::builder(200)
+ .body(
+ request
+ .state()
+ .render_template("signup.hbs", &layout_data)?,
+ )
+ .content_type(mime::HTML)
+ .build())
+}
+
+async fn get_release_asset(request: Request) -> tide::Result {
+ let body = request
+ .state()
+ .repo_client
+ .release_asset(request.param("tag_name")?, request.param("name")?)
+ .await?;
+
+ Ok(tide::Response::builder(200)
+ .header("Cache-Control", "no-transform")
+ .content_type(mime::BYTE_STREAM)
+ .body(body)
+ .build())
+}
@@ -0,0 +1,197 @@
+mod admin;
+mod assets;
+mod auth;
+mod env;
+mod errors;
+mod expiring;
+mod github;
+mod home;
+mod rpc;
+mod team;
+#[cfg(test)]
+mod tests;
+
+use self::errors::TideResultExt as _;
+use anyhow::{Context, Result};
+use async_sqlx_session::PostgresSessionStore;
+use async_std::{net::TcpListener, sync::RwLock as AsyncRwLock};
+use async_trait::async_trait;
+use auth::RequestExt as _;
+use handlebars::{Handlebars, TemplateRenderError};
+use parking_lot::RwLock;
+use rust_embed::RustEmbed;
+use serde::{Deserialize, Serialize};
+use sqlx::postgres::{PgPool, PgPoolOptions};
+use std::sync::Arc;
+use surf::http::cookies::SameSite;
+use tide::{log, sessions::SessionMiddleware};
+use tide_compress::CompressMiddleware;
+use zed_rpc::Peer;
+
+type Request = tide::Request<Arc<AppState>>;
+type DbPool = PgPool;
+
+#[derive(RustEmbed)]
+#[folder = "templates"]
+struct Templates;
+
+#[derive(Default, Deserialize)]
+pub struct Config {
+ pub http_port: u16,
+ pub database_url: String,
+ pub session_secret: String,
+ pub github_app_id: usize,
+ pub github_client_id: String,
+ pub github_client_secret: String,
+ pub github_private_key: String,
+}
+
+pub struct AppState {
+ db: sqlx::PgPool,
+ handlebars: RwLock<Handlebars<'static>>,
+ auth_client: auth::Client,
+ github_client: Arc<github::AppClient>,
+ repo_client: github::RepoClient,
+ rpc: AsyncRwLock<rpc::State>,
+ config: Config,
+}
+
+impl AppState {
+ async fn new(config: Config) -> tide::Result<Arc<Self>> {
+ let db = PgPoolOptions::new()
+ .max_connections(5)
+ .connect(&config.database_url)
+ .await
+ .context("failed to connect to postgres database")?;
+
+ let github_client =
+ github::AppClient::new(config.github_app_id, config.github_private_key.clone());
+ let repo_client = github_client
+ .repo("zed-industries/zed".into())
+ .await
+ .context("failed to initialize github client")?;
+
+ let this = Self {
+ db,
+ handlebars: Default::default(),
+ auth_client: auth::build_client(&config.github_client_id, &config.github_client_secret),
+ github_client,
+ repo_client,
+ rpc: Default::default(),
+ config,
+ };
+ this.register_partials();
+ Ok(Arc::new(this))
+ }
+
+ fn register_partials(&self) {
+ for path in Templates::iter() {
+ if let Some(partial_name) = path
+ .strip_prefix("partials/")
+ .and_then(|path| path.strip_suffix(".hbs"))
+ {
+ let partial = Templates::get(path.as_ref()).unwrap();
+ self.handlebars
+ .write()
+ .register_partial(partial_name, std::str::from_utf8(partial.as_ref()).unwrap())
+ .unwrap()
+ }
+ }
+ }
+
+ fn render_template(
+ &self,
+ path: &'static str,
+ data: &impl Serialize,
+ ) -> Result<String, TemplateRenderError> {
+ #[cfg(debug_assertions)]
+ self.register_partials();
+
+ self.handlebars.read().render_template(
+ std::str::from_utf8(Templates::get(path).unwrap().as_ref()).unwrap(),
+ data,
+ )
+ }
+}
+
+#[async_trait]
+trait RequestExt {
+ async fn layout_data(&mut self) -> tide::Result<Arc<LayoutData>>;
+ fn db(&self) -> &DbPool;
+}
+
+#[async_trait]
+impl RequestExt for Request {
+ async fn layout_data(&mut self) -> tide::Result<Arc<LayoutData>> {
+ if self.ext::<Arc<LayoutData>>().is_none() {
+ self.set_ext(Arc::new(LayoutData {
+ current_user: self.current_user().await?,
+ }));
+ }
+ Ok(self.ext::<Arc<LayoutData>>().unwrap().clone())
+ }
+
+ fn db(&self) -> &DbPool {
+ &self.state().db
+ }
+}
+
+#[derive(Serialize)]
+struct LayoutData {
+ current_user: Option<auth::User>,
+}
+
+#[async_std::main]
+async fn main() -> tide::Result<()> {
+ log::start();
+
+ if let Err(error) = env::load_dotenv() {
+ log::error!(
+ "error loading .env.toml (this is expected in production): {}",
+ error
+ );
+ }
+
+ let config = envy::from_env::<Config>().expect("error loading config");
+ let state = AppState::new(config).await?;
+ let rpc = Peer::new();
+ run_server(
+ state.clone(),
+ rpc,
+ TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port)).await?,
+ )
+ .await?;
+ Ok(())
+}
+
+pub async fn run_server(
+ state: Arc<AppState>,
+ rpc: Arc<Peer>,
+ listener: TcpListener,
+) -> tide::Result<()> {
+ let mut web = tide::with_state(state.clone());
+ web.with(CompressMiddleware::new());
+ web.with(
+ SessionMiddleware::new(
+ PostgresSessionStore::new_with_table_name(&state.config.database_url, "sessions")
+ .await
+ .unwrap(),
+ state.config.session_secret.as_bytes(),
+ )
+ .with_same_site_policy(SameSite::Lax), // Required obtain our session in /auth_callback
+ );
+ web.with(errors::Middleware);
+ home::add_routes(&mut web);
+ team::add_routes(&mut web);
+ admin::add_routes(&mut web);
+ auth::add_routes(&mut web);
+ assets::add_routes(&mut web);
+
+ let mut app = tide::with_state(state.clone());
+ rpc::add_routes(&mut app, &rpc);
+ app.at("/").nest(web);
+
+ app.listen(listener).await?;
+
+ Ok(())
+}
@@ -0,0 +1,652 @@
+use crate::auth::{self, UserId};
+
+use super::{auth::PeerExt as _, AppState};
+use anyhow::anyhow;
+use async_std::task;
+use async_tungstenite::{
+ tungstenite::{protocol::Role, Error as WebSocketError, Message as WebSocketMessage},
+ WebSocketStream,
+};
+use sha1::{Digest as _, Sha1};
+use std::{
+ collections::{HashMap, HashSet},
+ future::Future,
+ mem,
+ sync::Arc,
+ time::Instant,
+};
+use surf::StatusCode;
+use tide::log;
+use tide::{
+ http::headers::{HeaderName, CONNECTION, UPGRADE},
+ Request, Response,
+};
+use zed_rpc::{
+ auth::random_token,
+ proto::{self, EnvelopedMessage},
+ ConnectionId, Peer, Router, TypedEnvelope,
+};
+
+type ReplicaId = u16;
+
+#[derive(Default)]
+pub struct State {
+ connections: HashMap<ConnectionId, ConnectionState>,
+ pub worktrees: HashMap<u64, WorktreeState>,
+ next_worktree_id: u64,
+}
+
+struct ConnectionState {
+ _user_id: i32,
+ worktrees: HashSet<u64>,
+}
+
+pub struct WorktreeState {
+ host_connection_id: Option<ConnectionId>,
+ guest_connection_ids: HashMap<ConnectionId, ReplicaId>,
+ active_replica_ids: HashSet<ReplicaId>,
+ access_token: String,
+ root_name: String,
+ entries: HashMap<u64, proto::Entry>,
+}
+
+impl WorktreeState {
+ pub fn connection_ids(&self) -> Vec<ConnectionId> {
+ self.guest_connection_ids
+ .keys()
+ .copied()
+ .chain(self.host_connection_id)
+ .collect()
+ }
+
+ fn host_connection_id(&self) -> tide::Result<ConnectionId> {
+ Ok(self
+ .host_connection_id
+ .ok_or_else(|| anyhow!("host disconnected from worktree"))?)
+ }
+}
+
+impl State {
+ // Add a new connection associated with a given user.
+ pub fn add_connection(&mut self, connection_id: ConnectionId, _user_id: i32) {
+ self.connections.insert(
+ connection_id,
+ ConnectionState {
+ _user_id,
+ worktrees: Default::default(),
+ },
+ );
+ }
+
+ // Remove the given connection and its association with any worktrees.
+ pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Vec<u64> {
+ let mut worktree_ids = Vec::new();
+ if let Some(connection_state) = self.connections.remove(&connection_id) {
+ for worktree_id in connection_state.worktrees {
+ if let Some(worktree) = self.worktrees.get_mut(&worktree_id) {
+ if worktree.host_connection_id == Some(connection_id) {
+ worktree_ids.push(worktree_id);
+ } else if let Some(replica_id) =
+ worktree.guest_connection_ids.remove(&connection_id)
+ {
+ worktree.active_replica_ids.remove(&replica_id);
+ worktree_ids.push(worktree_id);
+ }
+ }
+ }
+ }
+ worktree_ids
+ }
+
+ // Add the given connection as a guest of the given worktree
+ pub fn join_worktree(
+ &mut self,
+ connection_id: ConnectionId,
+ worktree_id: u64,
+ access_token: &str,
+ ) -> Option<(ReplicaId, &WorktreeState)> {
+ if let Some(worktree_state) = self.worktrees.get_mut(&worktree_id) {
+ if access_token == worktree_state.access_token {
+ if let Some(connection_state) = self.connections.get_mut(&connection_id) {
+ connection_state.worktrees.insert(worktree_id);
+ }
+
+ let mut replica_id = 1;
+ while worktree_state.active_replica_ids.contains(&replica_id) {
+ replica_id += 1;
+ }
+ worktree_state.active_replica_ids.insert(replica_id);
+ worktree_state
+ .guest_connection_ids
+ .insert(connection_id, replica_id);
+ Some((replica_id, worktree_state))
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ }
+
+ fn read_worktree(
+ &self,
+ worktree_id: u64,
+ connection_id: ConnectionId,
+ ) -> tide::Result<&WorktreeState> {
+ let worktree = self
+ .worktrees
+ .get(&worktree_id)
+ .ok_or_else(|| anyhow!("worktree not found"))?;
+
+ if worktree.host_connection_id == Some(connection_id)
+ || worktree.guest_connection_ids.contains_key(&connection_id)
+ {
+ Ok(worktree)
+ } else {
+ Err(anyhow!(
+ "{} is not a member of worktree {}",
+ connection_id,
+ worktree_id
+ ))?
+ }
+ }
+
+ fn write_worktree(
+ &mut self,
+ worktree_id: u64,
+ connection_id: ConnectionId,
+ ) -> tide::Result<&mut WorktreeState> {
+ let worktree = self
+ .worktrees
+ .get_mut(&worktree_id)
+ .ok_or_else(|| anyhow!("worktree not found"))?;
+
+ if worktree.host_connection_id == Some(connection_id)
+ || worktree.guest_connection_ids.contains_key(&connection_id)
+ {
+ Ok(worktree)
+ } else {
+ Err(anyhow!(
+ "{} is not a member of worktree {}",
+ connection_id,
+ worktree_id
+ ))?
+ }
+ }
+}
+
+trait MessageHandler<'a, M: proto::EnvelopedMessage> {
+ type Output: 'a + Send + Future<Output = tide::Result<()>>;
+
+ fn handle(
+ &self,
+ message: TypedEnvelope<M>,
+ rpc: &'a Arc<Peer>,
+ app_state: &'a Arc<AppState>,
+ ) -> Self::Output;
+}
+
+impl<'a, M, F, Fut> MessageHandler<'a, M> for F
+where
+ M: proto::EnvelopedMessage,
+ F: Fn(TypedEnvelope<M>, &'a Arc<Peer>, &'a Arc<AppState>) -> Fut,
+ Fut: 'a + Send + Future<Output = tide::Result<()>>,
+{
+ type Output = Fut;
+
+ fn handle(
+ &self,
+ message: TypedEnvelope<M>,
+ rpc: &'a Arc<Peer>,
+ app_state: &'a Arc<AppState>,
+ ) -> Self::Output {
+ (self)(message, rpc, app_state)
+ }
+}
+
+fn on_message<M, H>(router: &mut Router, rpc: &Arc<Peer>, app_state: &Arc<AppState>, handler: H)
+where
+ M: EnvelopedMessage,
+ H: 'static + Clone + Send + Sync + for<'a> MessageHandler<'a, M>,
+{
+ let rpc = rpc.clone();
+ let handler = handler.clone();
+ let app_state = app_state.clone();
+ router.add_message_handler(move |message| {
+ let rpc = rpc.clone();
+ let handler = handler.clone();
+ let app_state = app_state.clone();
+ async move {
+ let sender_id = message.sender_id;
+ let message_id = message.message_id;
+ let start_time = Instant::now();
+ log::info!(
+ "RPC message received. id: {}.{}, type:{}",
+ sender_id,
+ message_id,
+ M::NAME
+ );
+ if let Err(err) = handler.handle(message, &rpc, &app_state).await {
+ log::error!("error handling message: {:?}", err);
+ } else {
+ log::info!(
+ "RPC message handled. id:{}.{}, duration:{:?}",
+ sender_id,
+ message_id,
+ start_time.elapsed()
+ );
+ }
+
+ Ok(())
+ }
+ });
+}
+
+pub fn add_rpc_routes(router: &mut Router, state: &Arc<AppState>, rpc: &Arc<Peer>) {
+ on_message(router, rpc, state, share_worktree);
+ on_message(router, rpc, state, join_worktree);
+ on_message(router, rpc, state, update_worktree);
+ on_message(router, rpc, state, close_worktree);
+ on_message(router, rpc, state, open_buffer);
+ on_message(router, rpc, state, close_buffer);
+ on_message(router, rpc, state, update_buffer);
+ on_message(router, rpc, state, buffer_saved);
+ on_message(router, rpc, state, save_buffer);
+}
+
+pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
+ let mut router = Router::new();
+ add_rpc_routes(&mut router, app.state(), rpc);
+ let router = Arc::new(router);
+
+ let rpc = rpc.clone();
+ app.at("/rpc").with(auth::VerifyToken).get(move |request: Request<Arc<AppState>>| {
+ let user_id = request.ext::<UserId>().copied();
+ let rpc = rpc.clone();
+ let router = router.clone();
+ async move {
+ const WEBSOCKET_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+ let connection_upgrade = header_contains_ignore_case(&request, CONNECTION, "upgrade");
+ let upgrade_to_websocket = header_contains_ignore_case(&request, UPGRADE, "websocket");
+ let upgrade_requested = connection_upgrade && upgrade_to_websocket;
+
+ if !upgrade_requested {
+ return Ok(Response::new(StatusCode::UpgradeRequired));
+ }
+
+ let header = match request.header("Sec-Websocket-Key") {
+ Some(h) => h.as_str(),
+ None => return Err(anyhow!("expected sec-websocket-key"))?,
+ };
+
+ let mut response = Response::new(StatusCode::SwitchingProtocols);
+ response.insert_header(UPGRADE, "websocket");
+ response.insert_header(CONNECTION, "Upgrade");
+ let hash = Sha1::new().chain(header).chain(WEBSOCKET_GUID).finalize();
+ response.insert_header("Sec-Websocket-Accept", base64::encode(&hash[..]));
+ response.insert_header("Sec-Websocket-Version", "13");
+
+ let http_res: &mut tide::http::Response = response.as_mut();
+ let upgrade_receiver = http_res.recv_upgrade().await;
+ let addr = request.remote().unwrap_or("unknown").to_string();
+ let state = request.state().clone();
+ let user_id = user_id.ok_or_else(|| anyhow!("user_id is not present on request. ensure auth::VerifyToken middleware is present"))?.0;
+ task::spawn(async move {
+ if let Some(stream) = upgrade_receiver.await {
+ let stream = WebSocketStream::from_raw_socket(stream, Role::Server, None).await;
+ handle_connection(rpc, router, state, addr, stream, user_id).await;
+ }
+ });
+
+ Ok(response)
+ }
+ });
+}
+
+pub async fn handle_connection<Conn>(
+ rpc: Arc<Peer>,
+ router: Arc<Router>,
+ state: Arc<AppState>,
+ addr: String,
+ stream: Conn,
+ user_id: i32,
+) where
+ Conn: 'static
+ + futures::Sink<WebSocketMessage, Error = WebSocketError>
+ + futures::Stream<Item = Result<WebSocketMessage, WebSocketError>>
+ + Send
+ + Unpin,
+{
+ log::info!("accepted rpc connection: {:?}", addr);
+ let (connection_id, handle_io, handle_messages) = rpc.add_connection(stream, router).await;
+ state
+ .rpc
+ .write()
+ .await
+ .add_connection(connection_id, user_id);
+
+ let handle_messages = async move {
+ handle_messages.await;
+ Ok(())
+ };
+
+ if let Err(e) = futures::try_join!(handle_messages, handle_io) {
+ log::error!("error handling rpc connection {:?} - {:?}", addr, e);
+ }
+
+ log::info!("closing connection to {:?}", addr);
+ if let Err(e) = rpc.sign_out(connection_id, &state).await {
+ log::error!("error signing out connection {:?} - {:?}", addr, e);
+ }
+}
+
+async fn share_worktree(
+ mut request: TypedEnvelope<proto::ShareWorktree>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ let mut state = state.rpc.write().await;
+ let worktree_id = state.next_worktree_id;
+ state.next_worktree_id += 1;
+ let access_token = random_token();
+ let worktree = request
+ .payload
+ .worktree
+ .as_mut()
+ .ok_or_else(|| anyhow!("missing worktree"))?;
+ let entries = mem::take(&mut worktree.entries)
+ .into_iter()
+ .map(|entry| (entry.id, entry))
+ .collect();
+ state.worktrees.insert(
+ worktree_id,
+ WorktreeState {
+ host_connection_id: Some(request.sender_id),
+ guest_connection_ids: Default::default(),
+ active_replica_ids: Default::default(),
+ access_token: access_token.clone(),
+ root_name: mem::take(&mut worktree.root_name),
+ entries,
+ },
+ );
+
+ rpc.respond(
+ request.receipt(),
+ proto::ShareWorktreeResponse {
+ worktree_id,
+ access_token,
+ },
+ )
+ .await?;
+ Ok(())
+}
+
+async fn join_worktree(
+ request: TypedEnvelope<proto::OpenWorktree>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ let worktree_id = request.payload.worktree_id;
+ let access_token = &request.payload.access_token;
+
+ let mut state = state.rpc.write().await;
+ if let Some((peer_replica_id, worktree)) =
+ state.join_worktree(request.sender_id, worktree_id, access_token)
+ {
+ let mut peers = Vec::new();
+ if let Some(host_connection_id) = worktree.host_connection_id {
+ peers.push(proto::Peer {
+ peer_id: host_connection_id.0,
+ replica_id: 0,
+ });
+ }
+ for (peer_conn_id, peer_replica_id) in &worktree.guest_connection_ids {
+ if *peer_conn_id != request.sender_id {
+ peers.push(proto::Peer {
+ peer_id: peer_conn_id.0,
+ replica_id: *peer_replica_id as u32,
+ });
+ }
+ }
+
+ broadcast(request.sender_id, worktree.connection_ids(), |conn_id| {
+ rpc.send(
+ conn_id,
+ proto::AddPeer {
+ worktree_id,
+ peer: Some(proto::Peer {
+ peer_id: request.sender_id.0,
+ replica_id: peer_replica_id as u32,
+ }),
+ },
+ )
+ })
+ .await?;
+ rpc.respond(
+ request.receipt(),
+ proto::OpenWorktreeResponse {
+ worktree_id,
+ worktree: Some(proto::Worktree {
+ root_name: worktree.root_name.clone(),
+ entries: worktree.entries.values().cloned().collect(),
+ }),
+ replica_id: peer_replica_id as u32,
+ peers,
+ },
+ )
+ .await?;
+ } else {
+ rpc.respond(
+ request.receipt(),
+ proto::OpenWorktreeResponse {
+ worktree_id,
+ worktree: None,
+ replica_id: 0,
+ peers: Vec::new(),
+ },
+ )
+ .await?;
+ }
+
+ Ok(())
+}
+
+async fn update_worktree(
+ request: TypedEnvelope<proto::UpdateWorktree>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ {
+ let mut state = state.rpc.write().await;
+ let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?;
+ for entry_id in &request.payload.removed_entries {
+ worktree.entries.remove(&entry_id);
+ }
+
+ for entry in &request.payload.updated_entries {
+ worktree.entries.insert(entry.id, entry.clone());
+ }
+ }
+
+ broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await?;
+ Ok(())
+}
+
+async fn close_worktree(
+ request: TypedEnvelope<proto::CloseWorktree>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ let connection_ids;
+ {
+ let mut state = state.rpc.write().await;
+ let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?;
+ connection_ids = worktree.connection_ids();
+ if worktree.host_connection_id == Some(request.sender_id) {
+ worktree.host_connection_id = None;
+ } else if let Some(replica_id) = worktree.guest_connection_ids.remove(&request.sender_id) {
+ worktree.active_replica_ids.remove(&replica_id);
+ }
+ }
+
+ broadcast(request.sender_id, connection_ids, |conn_id| {
+ rpc.send(
+ conn_id,
+ proto::RemovePeer {
+ worktree_id: request.payload.worktree_id,
+ peer_id: request.sender_id.0,
+ },
+ )
+ })
+ .await?;
+
+ Ok(())
+}
+
+async fn open_buffer(
+ request: TypedEnvelope<proto::OpenBuffer>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ let receipt = request.receipt();
+ let worktree_id = request.payload.worktree_id;
+ let host_connection_id = state
+ .rpc
+ .read()
+ .await
+ .read_worktree(worktree_id, request.sender_id)?
+ .host_connection_id()?;
+
+ let response = rpc
+ .forward_request(request.sender_id, host_connection_id, request.payload)
+ .await?;
+ rpc.respond(receipt, response).await?;
+ Ok(())
+}
+
+async fn close_buffer(
+ request: TypedEnvelope<proto::CloseBuffer>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ let host_connection_id = state
+ .rpc
+ .read()
+ .await
+ .read_worktree(request.payload.worktree_id, request.sender_id)?
+ .host_connection_id()?;
+
+ rpc.forward_send(request.sender_id, host_connection_id, request.payload)
+ .await?;
+
+ Ok(())
+}
+
+async fn save_buffer(
+ request: TypedEnvelope<proto::SaveBuffer>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ let host;
+ let guests;
+ {
+ let state = state.rpc.read().await;
+ let worktree = state.read_worktree(request.payload.worktree_id, request.sender_id)?;
+ host = worktree.host_connection_id()?;
+ guests = worktree
+ .guest_connection_ids
+ .keys()
+ .copied()
+ .collect::<Vec<_>>();
+ }
+
+ let sender = request.sender_id;
+ let receipt = request.receipt();
+ let response = rpc
+ .forward_request(sender, host, request.payload.clone())
+ .await?;
+
+ broadcast(host, guests, |conn_id| {
+ let response = response.clone();
+ async move {
+ if conn_id == sender {
+ rpc.respond(receipt, response).await
+ } else {
+ rpc.forward_send(host, conn_id, response).await
+ }
+ }
+ })
+ .await?;
+
+ Ok(())
+}
+
+async fn update_buffer(
+ request: TypedEnvelope<proto::UpdateBuffer>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await
+}
+
+async fn buffer_saved(
+ request: TypedEnvelope<proto::BufferSaved>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ broadcast_in_worktree(request.payload.worktree_id, request, rpc, state).await
+}
+
+async fn broadcast_in_worktree<T: proto::EnvelopedMessage>(
+ worktree_id: u64,
+ request: TypedEnvelope<T>,
+ rpc: &Arc<Peer>,
+ state: &Arc<AppState>,
+) -> tide::Result<()> {
+ let connection_ids = state
+ .rpc
+ .read()
+ .await
+ .read_worktree(worktree_id, request.sender_id)?
+ .connection_ids();
+
+ broadcast(request.sender_id, connection_ids, |conn_id| {
+ rpc.forward_send(request.sender_id, conn_id, request.payload.clone())
+ })
+ .await?;
+
+ Ok(())
+}
+
+pub async fn broadcast<F, T>(
+ sender_id: ConnectionId,
+ receiver_ids: Vec<ConnectionId>,
+ mut f: F,
+) -> anyhow::Result<()>
+where
+ F: FnMut(ConnectionId) -> T,
+ T: Future<Output = anyhow::Result<()>>,
+{
+ let futures = receiver_ids
+ .into_iter()
+ .filter(|id| *id != sender_id)
+ .map(|id| f(id));
+ futures::future::try_join_all(futures).await?;
+ Ok(())
+}
+
+fn header_contains_ignore_case<T>(
+ request: &tide::Request<T>,
+ header_name: HeaderName,
+ value: &str,
+) -> bool {
+ request
+ .header(header_name)
+ .map(|h| {
+ h.as_str()
+ .split(',')
+ .any(|s| s.trim().eq_ignore_ascii_case(value.trim()))
+ })
+ .unwrap_or(false)
+}
@@ -0,0 +1,15 @@
+use crate::{AppState, Request, RequestExt};
+use std::sync::Arc;
+use tide::http::mime;
+
+pub fn add_routes(app: &mut tide::Server<Arc<AppState>>) {
+ app.at("/team").get(get_team);
+}
+
+async fn get_team(mut request: Request) -> tide::Result {
+ let data = request.layout_data().await?;
+ Ok(tide::Response::builder(200)
+ .body(request.state().render_template("team.hbs", &data)?)
+ .content_type(mime::HTML)
+ .build())
+}
@@ -0,0 +1,538 @@
+use crate::{
+ admin, auth, github,
+ rpc::{self, add_rpc_routes},
+ AppState, Config,
+};
+use async_std::task;
+use gpui::TestAppContext;
+use rand::prelude::*;
+use serde_json::json;
+use sqlx::{
+ migrate::{MigrateDatabase, Migrator},
+ postgres::PgPoolOptions,
+ Executor as _, Postgres,
+};
+use std::{fs, path::Path, sync::Arc};
+use zed::{
+ editor::Editor,
+ language::LanguageRegistry,
+ rpc::Client,
+ settings,
+ test::{temp_tree, Channel},
+ worktree::{Fs, InMemoryFs, Worktree},
+};
+use zed_rpc::{ForegroundRouter, Peer, Router};
+
+#[gpui::test]
+async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+ let (window_b, _) = cx_b.add_window(|_| EmptyView);
+ let settings = settings::channel(&cx_b.font_cache()).unwrap().1;
+ let lang_registry = Arc::new(LanguageRegistry::new());
+
+ // Connect to a server as 2 clients.
+ let mut server = TestServer::start().await;
+ let client_a = server.create_client(&mut cx_a, "user_a").await;
+ let client_b = server.create_client(&mut cx_b, "user_b").await;
+
+ // Share a local worktree as client A
+ let dir = temp_tree(json!({
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ }));
+ let worktree_a = cx_a.add_model(|cx| Worktree::local(dir.path(), lang_registry.clone(), cx));
+ worktree_a
+ .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+ .await;
+ let (worktree_id, worktree_token) = worktree_a
+ .update(&mut cx_a, |tree, cx| {
+ tree.as_local_mut().unwrap().share(client_a.clone(), cx)
+ })
+ .await
+ .unwrap();
+
+ // Join that worktree as client B, and see that a guest has joined as client A.
+ let worktree_b = Worktree::open_remote(
+ client_b.clone(),
+ worktree_id,
+ worktree_token,
+ lang_registry.clone(),
+ &mut cx_b.to_async(),
+ )
+ .await
+ .unwrap();
+ let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| tree.replica_id());
+ worktree_a
+ .condition(&cx_a, |tree, _| {
+ tree.peers()
+ .values()
+ .any(|replica_id| *replica_id == replica_id_b)
+ })
+ .await;
+
+ // Open the same file as client B and client A.
+ let buffer_b = worktree_b
+ .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx))
+ .await
+ .unwrap();
+ buffer_b.read_with(&cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
+ worktree_a.read_with(&cx_a, |tree, cx| assert!(tree.has_open_buffer("b.txt", cx)));
+ let buffer_a = worktree_a
+ .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx))
+ .await
+ .unwrap();
+
+ // Create a selection set as client B and see that selection set as client A.
+ let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, settings, cx));
+ buffer_a
+ .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
+ .await;
+
+ // Edit the buffer as client B and see that edit as client A.
+ editor_b.update(&mut cx_b, |editor, cx| {
+ editor.insert(&"ok, ".to_string(), cx)
+ });
+ buffer_a
+ .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")
+ .await;
+
+ // Remove the selection set as client B, see those selections disappear as client A.
+ cx_b.update(move |_| drop(editor_b));
+ buffer_a
+ .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
+ .await;
+
+ // Close the buffer as client A, see that the buffer is closed.
+ drop(buffer_a);
+ worktree_a
+ .condition(&cx_a, |tree, cx| !tree.has_open_buffer("b.txt", cx))
+ .await;
+
+ // Dropping the worktree removes client B from client A's peers.
+ cx_b.update(move |_| drop(worktree_b));
+ worktree_a
+ .condition(&cx_a, |tree, _| tree.peers().is_empty())
+ .await;
+}
+
+#[gpui::test]
+async fn test_propagate_saves_and_fs_changes_in_shared_worktree(
+ mut cx_a: TestAppContext,
+ mut cx_b: TestAppContext,
+ mut cx_c: TestAppContext,
+) {
+ let lang_registry = Arc::new(LanguageRegistry::new());
+
+ // Connect to a server as 3 clients.
+ let mut server = TestServer::start().await;
+ let client_a = server.create_client(&mut cx_a, "user_a").await;
+ let client_b = server.create_client(&mut cx_b, "user_b").await;
+ let client_c = server.create_client(&mut cx_c, "user_c").await;
+
+ // Share a worktree as client A.
+ let dir = temp_tree(json!({
+ "file1": "",
+ "file2": ""
+ }));
+ let worktree_a = cx_a.add_model(|cx| Worktree::local(dir.path(), lang_registry.clone(), cx));
+ worktree_a
+ .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+ .await;
+ let (worktree_id, worktree_token) = worktree_a
+ .update(&mut cx_a, |tree, cx| {
+ tree.as_local_mut().unwrap().share(client_a.clone(), cx)
+ })
+ .await
+ .unwrap();
+
+ // Join that worktree as clients B and C.
+ let worktree_b = Worktree::open_remote(
+ client_b.clone(),
+ worktree_id,
+ worktree_token.clone(),
+ lang_registry.clone(),
+ &mut cx_b.to_async(),
+ )
+ .await
+ .unwrap();
+ let worktree_c = Worktree::open_remote(
+ client_c.clone(),
+ worktree_id,
+ worktree_token,
+ lang_registry.clone(),
+ &mut cx_c.to_async(),
+ )
+ .await
+ .unwrap();
+
+ // Open and edit a buffer as both guests B and C.
+ let buffer_b = worktree_b
+ .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx))
+ .await
+ .unwrap();
+ let buffer_c = worktree_c
+ .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx))
+ .await
+ .unwrap();
+ buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx));
+ buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx));
+
+ // Open and edit that buffer as the host.
+ let buffer_a = worktree_a
+ .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx))
+ .await
+ .unwrap();
+ buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "i-am-a", cx));
+
+ // Wait for edits to propagate
+ buffer_a
+ .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
+ .await;
+ buffer_b
+ .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
+ .await;
+ buffer_c
+ .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
+ .await;
+
+ // Edit the buffer as the host and concurrently save as guest B.
+ let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap());
+ buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx));
+ save_b.await.unwrap();
+ assert_eq!(
+ fs::read_to_string(dir.path().join("file1")).unwrap(),
+ "hi-a, i-am-c, i-am-b, i-am-a"
+ );
+ buffer_a.read_with(&cx_a, |buf, _| assert!(!buf.is_dirty()));
+ buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty()));
+ buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await;
+
+ // Make changes on host's file system, see those changes on the guests.
+ fs::rename(dir.path().join("file2"), dir.path().join("file3")).unwrap();
+ fs::write(dir.path().join("file4"), "4").unwrap();
+ worktree_b
+ .condition(&cx_b, |tree, _| tree.file_count() == 3)
+ .await;
+ worktree_c
+ .condition(&cx_c, |tree, _| tree.file_count() == 3)
+ .await;
+ worktree_b.read_with(&cx_b, |tree, _| {
+ assert_eq!(
+ tree.paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ &["file1", "file3", "file4"]
+ )
+ });
+ worktree_c.read_with(&cx_c, |tree, _| {
+ assert_eq!(
+ tree.paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ &["file1", "file3", "file4"]
+ )
+ });
+}
+
+#[gpui::test]
+async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+ let lang_registry = Arc::new(LanguageRegistry::new());
+
+ // Connect to a server as 2 clients.
+ let mut server = TestServer::start().await;
+ let client_a = server.create_client(&mut cx_a, "user_a").await;
+ let client_b = server.create_client(&mut cx_b, "user_b").await;
+
+ // Share a local worktree as client A
+ let fs = Arc::new(InMemoryFs::new());
+ fs.save(Path::new("/a.txt"), &"a-contents".into())
+ .await
+ .unwrap();
+ let worktree_a =
+ cx_a.add_model(|cx| Worktree::test(Path::new("/"), lang_registry.clone(), fs.clone(), cx));
+ worktree_a
+ .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+ .await;
+ let (worktree_id, worktree_token) = worktree_a
+ .update(&mut cx_a, |tree, cx| {
+ tree.as_local_mut().unwrap().share(client_a.clone(), cx)
+ })
+ .await
+ .unwrap();
+
+ // Join that worktree as client B, and see that a guest has joined as client A.
+ let worktree_b = Worktree::open_remote(
+ client_b.clone(),
+ worktree_id,
+ worktree_token,
+ lang_registry.clone(),
+ &mut cx_b.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let buffer_b = worktree_b
+ .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))
+ .await
+ .unwrap();
+ let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime);
+
+ buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx));
+ buffer_b.read_with(&cx_b, |buf, _| {
+ assert!(buf.is_dirty());
+ assert!(!buf.has_conflict());
+ });
+
+ buffer_b
+ .update(&mut cx_b, |buf, cx| buf.save(cx))
+ .unwrap()
+ .await
+ .unwrap();
+ worktree_b
+ .condition(&cx_b, |_, cx| {
+ buffer_b.read(cx).file().unwrap().mtime != mtime
+ })
+ .await;
+ buffer_b.read_with(&cx_b, |buf, _| {
+ assert!(!buf.is_dirty());
+ assert!(!buf.has_conflict());
+ });
+
+ buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "hello ", cx));
+ buffer_b.read_with(&cx_b, |buf, _| {
+ assert!(buf.is_dirty());
+ assert!(!buf.has_conflict());
+ });
+}
+
+#[gpui::test]
+async fn test_editing_while_guest_opens_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+ let lang_registry = Arc::new(LanguageRegistry::new());
+
+ // Connect to a server as 2 clients.
+ let mut server = TestServer::start().await;
+ let client_a = server.create_client(&mut cx_a, "user_a").await;
+ let client_b = server.create_client(&mut cx_b, "user_b").await;
+
+ // Share a local worktree as client A
+ let fs = Arc::new(InMemoryFs::new());
+ fs.save(Path::new("/a.txt"), &"a-contents".into())
+ .await
+ .unwrap();
+ let worktree_a =
+ cx_a.add_model(|cx| Worktree::test(Path::new("/"), lang_registry.clone(), fs.clone(), cx));
+ worktree_a
+ .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+ .await;
+ let (worktree_id, worktree_token) = worktree_a
+ .update(&mut cx_a, |tree, cx| {
+ tree.as_local_mut().unwrap().share(client_a.clone(), cx)
+ })
+ .await
+ .unwrap();
+
+ // Join that worktree as client B, and see that a guest has joined as client A.
+ let worktree_b = Worktree::open_remote(
+ client_b.clone(),
+ worktree_id,
+ worktree_token,
+ lang_registry.clone(),
+ &mut cx_b.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let buffer_a = worktree_a
+ .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx))
+ .await
+ .unwrap();
+ let buffer_b = cx_b
+ .background()
+ .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)));
+
+ task::yield_now().await;
+ buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx));
+
+ let text = buffer_a.read_with(&cx_a, |buf, _| buf.text());
+ let buffer_b = buffer_b.await.unwrap();
+ buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await;
+}
+
+#[gpui::test]
+async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) {
+ let lang_registry = Arc::new(LanguageRegistry::new());
+
+ // Connect to a server as 2 clients.
+ let mut server = TestServer::start().await;
+ let client_a = server.create_client(&mut cx_a, "user_a").await;
+ let client_b = server.create_client(&mut cx_a, "user_b").await;
+
+ // Share a local worktree as client A
+ let dir = temp_tree(json!({
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ }));
+ let worktree_a = cx_a.add_model(|cx| Worktree::local(dir.path(), lang_registry.clone(), cx));
+ worktree_a
+ .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+ .await;
+ let (worktree_id, worktree_token) = worktree_a
+ .update(&mut cx_a, |tree, cx| {
+ tree.as_local_mut().unwrap().share(client_a.clone(), cx)
+ })
+ .await
+ .unwrap();
+
+ // Join that worktree as client B, and see that a guest has joined as client A.
+ let _worktree_b = Worktree::open_remote(
+ client_b.clone(),
+ worktree_id,
+ worktree_token,
+ lang_registry.clone(),
+ &mut cx_b.to_async(),
+ )
+ .await
+ .unwrap();
+ worktree_a
+ .condition(&cx_a, |tree, _| tree.peers().len() == 1)
+ .await;
+
+ // Drop client B's connection and ensure client A observes client B leaving the worktree.
+ client_b.disconnect().await.unwrap();
+ worktree_a
+ .condition(&cx_a, |tree, _| tree.peers().len() == 0)
+ .await;
+}
+
+struct TestServer {
+ peer: Arc<Peer>,
+ app_state: Arc<AppState>,
+ db_name: String,
+ router: Arc<Router>,
+}
+
+impl TestServer {
+ async fn start() -> Self {
+ let mut rng = StdRng::from_entropy();
+ let db_name = format!("zed-test-{}", rng.gen::<u128>());
+ let app_state = Self::build_app_state(&db_name).await;
+ let peer = Peer::new();
+ let mut router = Router::new();
+ add_rpc_routes(&mut router, &app_state, &peer);
+ Self {
+ peer,
+ router: Arc::new(router),
+ app_state,
+ db_name,
+ }
+ }
+
+ async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> Client {
+ let user_id = admin::create_user(&self.app_state.db, name, false)
+ .await
+ .unwrap();
+ let lang_registry = Arc::new(LanguageRegistry::new());
+ let client = Client::new(lang_registry.clone());
+ let mut client_router = ForegroundRouter::new();
+ cx.update(|cx| zed::worktree::init(cx, &client, &mut client_router));
+
+ let (client_conn, server_conn) = Channel::bidirectional();
+ cx.background()
+ .spawn(rpc::handle_connection(
+ self.peer.clone(),
+ self.router.clone(),
+ self.app_state.clone(),
+ name.to_string(),
+ server_conn,
+ user_id,
+ ))
+ .detach();
+ client
+ .add_connection(client_conn, Arc::new(client_router), cx.to_async())
+ .await
+ .unwrap();
+
+ // Reset the executor because running SQL queries has a non-deterministic impact on it.
+ cx.foreground().reset();
+ client
+ }
+
+ async fn build_app_state(db_name: &str) -> Arc<AppState> {
+ let mut config = Config::default();
+ config.session_secret = "a".repeat(32);
+ config.database_url = format!("postgres://postgres@localhost/{}", db_name);
+
+ Self::create_db(&config.database_url).await;
+ let db = PgPoolOptions::new()
+ .max_connections(5)
+ .connect(&config.database_url)
+ .await
+ .expect("failed to connect to postgres database");
+ let migrator = Migrator::new(Path::new("./migrations")).await.unwrap();
+ migrator.run(&db).await.unwrap();
+
+ let github_client = github::AppClient::test();
+ Arc::new(AppState {
+ db,
+ handlebars: Default::default(),
+ auth_client: auth::build_client("", ""),
+ repo_client: github::RepoClient::test(&github_client),
+ github_client,
+ rpc: Default::default(),
+ config,
+ })
+ }
+
+ async fn create_db(url: &str) {
+ // Enable tests to run in parallel by serializing the creation of each test database.
+ lazy_static::lazy_static! {
+ static ref DB_CREATION: async_std::sync::Mutex<()> = async_std::sync::Mutex::new(());
+ }
+
+ let _lock = DB_CREATION.lock().await;
+ Postgres::create_database(url)
+ .await
+ .expect("failed to create test database");
+ }
+}
+
+impl Drop for TestServer {
+ fn drop(&mut self) {
+ task::block_on(async {
+ self.peer.reset().await;
+ self.app_state
+ .db
+ .execute(
+ format!(
+ "
+ SELECT pg_terminate_backend(pg_stat_activity.pid)
+ FROM pg_stat_activity
+ WHERE pg_stat_activity.datname = '{}' AND pid <> pg_backend_pid();",
+ self.db_name,
+ )
+ .as_str(),
+ )
+ .await
+ .unwrap();
+ self.app_state.db.close().await;
+ Postgres::drop_database(&self.app_state.config.database_url)
+ .await
+ .unwrap();
+ });
+ }
+}
+
+struct EmptyView;
+
+impl gpui::Entity for EmptyView {
+ type Event = ();
+}
+
+impl gpui::View for EmptyView {
+ fn ui_name() -> &'static str {
+ "empty view"
+ }
+
+ fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox {
+ gpui::Element::boxed(gpui::elements::Empty)
+ }
+}
@@ -0,0 +1,6 @@
+<svg width="1147" height="310" viewBox="0 0 1147 310" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.34388 233H166.2V186.632H82.1039L169.368 28.52H7.79988V74.888H91.6079L4.34388 233ZM233.094 186.632V152.936H291.27V107.144H233.094V74.888H303.078V28.52H182.406V233H305.382V186.632H233.094ZM323.108 233H406.916C460.484 233 504.548 190.376 504.548 130.76C504.548 71.144 460.484 28.52 406.916 28.52H323.108V233ZM373.796 185.192V76.328H405.188C436.292 76.328 451.556 99.08 451.556 130.76C451.556 162.44 436.292 185.192 405.188 185.192H373.796Z" fill="black"/>
+<rect x="372" y="222" width="773" height="86" rx="2" fill="black" stroke="black" stroke-width="4"/>
+<rect x="351" y="205" width="773" height="85" rx="2" fill="#F9FAFB" stroke="black" stroke-width="4"/>
@@ -0,0 +1,108 @@
+/* This file is compiled to /assets/styles/tailwind.css via script/tailwind */
+
+@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap');
+
+@font-face {
+ font-family: 'Visby CF';
+ src:
+ url('/static/fonts/VisbyCF-Thin.woff2') format('woff2'),
+ url('/static/fonts/VisbyCF-Thin.woff') format('woff');
+ font-weight: 100;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: 'Visby CF';
+ src:
+ url('/static/fonts/VisbyCF-Light.woff2') format('woff2'),
+ url('/static/fonts/VisbyCF-Light.woff') format('woff');
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Visby CF';
+ src:
+ url('/static/fonts/VisbyCF-Regular.woff2') format('woff2'),
+ url('/static/fonts/VisbyCF-Regular.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Visby CF';
+ src:
+ url('/static/fonts/VisbyCF-Medium.woff2') format('woff2'),
+ url('/static/fonts/VisbyCF-Medium.woff') format('woff');
+ font-weight: 500;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Visby CF';
+ src:
+ url('/static/fonts/VisbyCF-DemiBold.woff2') format('woff2'),
+ url('/static/fonts/VisbyCF-DemiBold.woff') format('woff');
+ font-weight: 600;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Visby CF';
+ src:
+ url('/static/fonts/VisbyCF-Bold.woff2') format('woff2'),
+ url('/static/fonts/VisbyCF-Bold.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Visby CF';
+ src:
+ url('/static/fonts/VisbyCF-ExtraBold.woff2') format('woff2'),
+ url('/static/fonts/VisbyCF-ExtraBold.woff') format('woff');
+ font-weight: 800;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Visby CF';
+ src:
+ url('/static/fonts/VisbyCF-Heavy.woff2') format('woff2'),
+ url('/static/fonts/VisbyCF-Heavy.woff') format('woff');
+ font-weight: 900;
+ font-style: normal;
+}
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer utilities {
+ @responsive {
+ .bg-dotgrid-sm {
+ background:
+ linear-gradient(90deg, theme('colors.gray.50') 38px, transparent 1%) center,
+ linear-gradient(theme('colors.gray.50') 38px, transparent 1%) center,
+ theme('colors.gray.600');
+ background-size: 40px 40px;
+ }
+
+ .bg-dotgrid-md {
+ background:
+ linear-gradient(90deg, theme('colors.gray.50') 58px, transparent 1%) center,
+ linear-gradient(theme('colors.gray.50') 58px, transparent 1%) center,
+ theme('colors.gray.600');
+ background-size: 60px 60px;
+ }
+
+ .bg-dotgrid-lg {
+ background:
+ linear-gradient(90deg, theme('colors.gray.50') 88px, transparent 1%) center,
+ linear-gradient(theme('colors.gray.50') 88px, transparent 1%) center,
+ theme('colors.gray.600');
+ background-size: 90px 90px;
+ }
+ }
+}
@@ -0,0 +1,81 @@
+{{#> layout }}
+<script>
+ window.addEventListener("DOMContentLoaded", function () {
+ let users = document.getElementById("users");
+ if (users) {
+ users.addEventListener("change", async function (event) {
+ const action = event.target.getAttribute("action");
+ if (action) {
+ console.log(action, event.target.checked);
+ const response = await fetch(action, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ admin: event.target.checked })
+ });
+ }
+ });
+ }
+ });
+</script>
+
+<div class="bg-white">
+ <div class="container py-4 px-8 md:px-12 mx-auto">
+ <h1 class="text-xl font-bold border-b border-gray-300 mb-4">Users</h1>
+ <table class="table" id="users">
+ <tr>
+ <th class="text-left pr-2">GitHub Login</th>
+ <th class="text-left pr-2">Admin</th>
+ <th></th>
+ </tr>
+ <form action="/users" method="post" class="m-0 mb-4">
+ <tr>
+ <td>
+ <input name="github_login" type="text" class="border border-gray-300 p-1 mr-2 w-48"
+ placeholder="@github_handle">
+ </td>
+ <td>
+ <input type="checkbox" id="admin" name="admin" value="true">
+ </td>
+ <td class="text-right">
+ <button class="p-1 w-20 text-white rounded-md bg-gray-600 hover:bg-black">Add</button>
+ </td>
+ </tr>
+ </form>
+
+ {{#each users}}
+ <tr>
+ <form action="/users/{{id}}/delete" method="post">
+ <td class="py-1">
+ {{github_login}}
+ </td>
+ <td>
+ <input action="/users/{{id}}" type="checkbox" {{#if admin}}checked{{/if}}>
+ </td>
+ <td class="text-right">
+ <button class="p-1 w-20 rounded-md bg-gray-600 hover:bg-black text-white">Remove</button>
+ </td>
+ </form>
+ </tr>
+ {{/each}}
+ </table>
+
+ <h1 class="text-xl font-bold border-b border-gray-300 mb-4 mt-8">Signups</h1>
+ <table class="table">
+ {{#each signups}}
+ <tr>
+ <form action="/signups/{{id}}/delete" method="post">
+ <td class="align-top">{{github_login}}</td>
+ <td class="pl-4 align-top">{{email_address}}</td>
+ <td class="pl-4 align-top">{{about}}</td>
+ <td class="text-right">
+ <button class="p-1 w-20 rounded-md bg-gray-600 hover:bg-black text-white">Remove</button>
+ </td>
+ </form>
+ </tr>
+ {{/each}}
+ </table>
+ </div>
+</div>
+{{/layout}}
@@ -0,0 +1,41 @@
+{{#> layout }}
+
+<div class="bg-white">
+ <div class="container mx-auto py-12 px-8 md:px-12">
+ <h1 class="text-4xl font-black font-display mb-8">Bypassing code signing restrictions</h1>
+ <div class="lg:flex lg:flex-row items-start">
+ <div class="prose xl:prose-xl lg:mr-12">
+ <p>
+ We haven't yet applied to Apple for the required certificate to sign our application bundle, which
+ means there's a small speed bump when you run our app.
+ </p>
+ <p>
+ Instead of double-clicking the app, right click it and choose Open.
+ </p>
+ <p>
+ You need to attempt open the app <b>twice</b>. On the second attempt, you should see the option
+ to open the application anyway in the dialog.
+ </p>
+ </div>
+ <img class="float-1 lg:w-1/3 object-contain mt-8 lg:mt-0" alt="Screen Shot 2021-06-02 at 2 38 12 PM"
+ src="https://user-images.githubusercontent.com/1789/120550754-86514480-c3b2-11eb-8995-32f5eea79664.png">
+ <img class="float-1 lg:w-1/3 object-contain -ml-10 lg:ml-0 lg:-mt-10"
+ alt="Screen Shot 2021-06-02 at 2 38 19 PM"
+ src="https://user-images.githubusercontent.com/1789/120550759-88b39e80-c3b2-11eb-88e2-ddfc1b1c7a03.png">
+ </div>
+
+ <h1 class="text-4xl font-black font-display my-8">Key bindings</h1>
+ <div class="prose">
+ <dl>
+ <dt>
+ <pre>cmd-shift-L</pre>
+ </dt>
+ <dd>
+ Split selection into lines
+ </dd>
+ </dl>
+ </div>
+ </div>
+</div>
+
+{{/layout}}
@@ -0,0 +1,7 @@
+{{#> layout }}
+<div class="bg-white py-8">
+ <div class="container mx-auto my-16 px-8 md:px-12 text-2xl md:text-4xl">
+ Sorry, we encountered a {{status}} error: {{reason}}.
+ </div>
+</div>
+{{/layout}}
@@ -0,0 +1,69 @@
+{{#> layout }}
+{{#if releases}}
+
+<div class="bg-white">
+ <div class="container mx-auto py-12 px-8 md:px-12 lg:flex lg:flex-row">
+ {{#each releases}}
+ <div class="md:flex md:flex-row">
+ <div class="font-display mb-8 md:mb-0 md:text-right">
+ <div class="text-2xl font-bold whitespace-nowrap">
+ VERSION {{name}}
+ </div>
+ <a class="text-md underline text-yellow-600 hover:text-yellow-700"
+ href="/releases/{{tag_name}}/{{assets.0.name}}">
+ DOWNLOAD
+ </a>
+ </div>
+ <div
+ class="prose prose-lg xl:prose-xl border-t md:border-t-0 pt-8 md:border-l border-gray-400 md:ml-8 md:pl-8 md:pt-0 xl:ml-16 xl:pl-16 max-w-5xl font-body">
+ {{{body}}}
+ </div>
+ </div>
+ {{/each}}
+ </div>
+</div>
+
+{{else}}
+
+<div class="bg-dotgrid-sm md:bg-dotgrid-md lg:bg-dotgrid-lg">
+ <img src="/static/svg/hero.svg" class="container mx-auto px-8 md:px-12 py-16 md:py-24 lg:py-32" />
+</div>
+
+<div class="container mx-auto py-24 lg:py-32 px-8 md:px-12 lg:flex lg:flex-row lg:items-center">
+ <div class="prose prose-xl md:prose-2xl text-gray-50 prose-gray-50 w-full lg:w-1/2">
+ <p>
+ Weβre the team behind GitHubβs Atom text editor, and weβre building something new:
+ </p>
+
+ <p>
+ <b>Zed</b> is a fully-native desktop code editor focused on high performance,
+ clean design, and seamless collaboration.
+ </p>
+
+ <p>
+ Weβre in early development, but weβd like to build a small community of developers who care deeply about
+ their tools and are willing to give us feedback. We'll be sharing alpha builds with community members and
+ telling our story along the way.
+ </p>
+
+ <p>
+ If youβre interested in joining us, please let us know.
+ </p>
+ </div>
+
+ <form class="my-16 lg:my-0 lg:ml-16 flex-1 text-xl md:text-2xl" action="/signups" method="post">
+ <input name="github_login" placeholder="@github_handle"
+ class="w-3/5 xl:w-1/2 p-3 mb-8 block bg-gray-50 placeholder-gray-500">
+ <input name="email_address" placeholder="email@addre.ss"
+ class="w-4/5 xl:w-3/4 p-3 my-8 block bg-gray-50 placeholder-gray-500">
+ <textarea name="about" class="block w-full xl:w-full h-48 p-3 my-8 bg-gray-50 placeholder-gray-500 my-6"
+ placeholder="Please tell us a bit about you and why you're interested in Zed. What code editor do you use today? What do you love and hate about it?"></textarea>
+ <button
+ class="p-4 rounded-md text-gray-50 bg-gray-500 inline-block cursor-pointer hover:bg-gray-400 font-display">
+ ENGAGE
+ </button>
+ </form>
+</div>
+
+{{/if}}
+{{/layout}}
@@ -0,0 +1,62 @@
+<html>
+
+<head>
+ <link rel="icon" href="/static/images/favicon.png">
+ <link rel="stylesheet" href="/static/styles.css">
+ <title>Zed Industries</title>
+
+
+ <script>
+ window.addEventListener("DOMContentLoaded", function () {
+ let avatar = document.getElementById("avatar");
+ let sign_out = document.getElementById("sign_out");
+ if (avatar && sign_out) {
+ avatar.addEventListener("click", function (event) {
+ sign_out.classList.toggle("hidden");
+ event.stopPropagation();
+ });
+ document.addEventListener("click", function (event) {
+ sign_out.classList.add("hidden");
+ });
+ }
+ });
+ </script>
+</head>
+
+<body class="font-body bg-black">
+ <div class="text-lg text-gray-50">
+ <div class="container mx-auto flex flex-row items-center py-4 px-8 md:px-12 font-display">
+ <a href="/" class="font-display">
+ <span class="font-black">ZED</span><span class="font-light" style="padding-left: 1px">INDUSTRIES</span>
+ </a>
+ <div class="flex-1"></div>
+ <a href="/team" class="text-sm mr-4 hover:underline">
+ Team
+ </a>
+ {{#if current_user}}
+ {{#if current_user.is_admin }}
+ <a href="/admin" class="text-sm mr-4 hover:underline">
+ Admin
+ </a>
+ {{/if}}
+ <div class="relative">
+ <img id="avatar" src="{{current_user.avatar_url}}"
+ class="w-8 rounded-full border-gray-400 border cursor-pointer" />
+ <form id="sign_out" action="/sign_out" method="post"
+ class="hidden absolute mt-1 right-0 bg-black rounded border border-gray-400 text-center text-sm p-2 px-4 whitespace-nowrap">
+ <button class="hover:underline">Sign out</button>
+ </form>
+ </div>
+ {{else}}
+ <a href=" /sign_in"
+ class="text-sm align-middle p-1 px-2 rounded-md border border-gray-50 cursor-pointer hover:bg-gray-800">
+ Log in
+ </a>
+ {{/if}}
+ </div>
+ </div>
+
+ {{> @partial-block}}
+</body>
+
+</html>
@@ -0,0 +1,19 @@
+{{#> layout }}
+<div class="bg-gray-50 py-10 text-black">
+ <div class="container mx-auto px-8 md:px-12">
+ <div class=" text-6xl font-black mb-8">
+ THANKS
+ </div>
+ <div class="text-xl max-w-md">
+ <p class="mb-8">
+ Thanks a ton for your interest! We'll add you to our list and let you know when we have something ready
+ for you to try out.
+ </p>
+
+ <p>
+ <a href="/" class="font-bold text-yellow-600 hover:text-yellow-700">Back to /</a>
+ </p>
+ </div>
+ </div>
+</div>
+{{/layout}}
@@ -0,0 +1,62 @@
+{{#> layout }}
+
+<div class="bg-white">
+ <div class="container mx-auto py-12 px-8 md:px-12 lg:flex lg:flex-row">
+ <div class="mb-16 lg:mb-0 lg:flex-1 lg:mr-8 xl:mr-16">
+ <img src="https://github.com/nathansobo.png?size=200" class="mx-auto mb-4 h-28 rounded-full">
+ <div>
+ <a href="https://github.com/nathansobo"
+ class="block text-center mb-4 font-display text-2xl font-bold whitespace-nowrap hover:underline">
+ NATHAN SOBO
+ </a>
+ <div class="prose md:prose-lg lg:prose xl:prose-lg">
+ Nathan joined GitHub in late 2011 to build the <a href="https://atom.io">Atom text editor</a>, and
+ he led the Atom team until 2018. He also co-led development of <a
+ href="https://teletype.atom.io">Teletype for Atom</a>, pioneering one of the first production
+ uses of conflict-free replicated data types for collaborative text editing. He's been dreaming about
+ building the worldβs best text editor since he graduated from college, and is excited to finally
+ have
+ the knowledge, tools, and resources to achieve this vision.
+ </div>
+ </div>
+ </div>
+ <div class="mb-16 lg:mb-0 lg:flex-1 lg:mr-8 xl:mr-16">
+ <img src="https://github.com/as-cii.png?size=200" class="mx-auto mb-4 h-28 rounded-full">
+ <div>
+ <a href="https://github.com/as-cii"
+ class="block text-center mb-4 font-display text-2xl font-bold whitespace-nowrap hover:underline">
+ ANTONIO SCANDURRA
+ </a>
+ <div class="prose md:prose-lg lg:prose xl:prose-lg">
+ Antonio joined the Atom team in 2014 while still in university after his outstanding open source
+ contributions caught the attention of the team. He later joined Nathan in architecting <a
+ href="https://teletype.atom.io">Teletype for
+ Atom</a> and researching the foundations of what has turned into Zed. For the last two years,
+ heβs
+ become an expert in distributed systems and conflict-free replicated data types through the
+ development of a real-time, distributed, conflict-free database implemented in Rust for <a
+ href="https://ditto.live">Ditto</a>.
+ </div>
+ </div>
+ </div>
+ <div class="mb-16 lg:mb-0 lg:flex-1">
+ <img src="https://github.com/maxbrunsfeld.png?size=200" class="mx-auto mb-4 h-28 rounded-full">
+ <div>
+ <a href="https://github.com/maxbrunsfeld"
+ class="block text-center mb-4 font-display text-2xl font-bold whitespace-nowrap hover:underline">
+ MAX BRUNSFELD
+ </a>
+ <div class="prose md:prose-lg lg:prose xl:prose-lg">
+ Max joined the Atom team in 2013 after working at Pivotal Labs. While driving Atom towards its 1.0
+ launch during the day, Max spent nights and weekends building <a
+ href="https://tree-sitter.github.io">Tree-sitter</a>, a blazing-fast and
+ expressive incremental parsing framework that currently powers all code analysis at GitHub. Before
+ leaving to start Zed, Max helped GitHub's semantic analysis team integrate Tree-sitter to support
+ syntax highlighting and code navigation on <a href="https://github.com">github.com</a>.
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+{{/layout}}
@@ -15,14 +15,14 @@ base64 = "0.13"
futures = "0.3"
log = "0.4"
parking_lot = "0.11.1"
-postage = {version = "0.4.1", features = ["futures-traits"]}
+postage = { version = "0.4.1", features = ["futures-traits"] }
prost = "0.7"
rand = "0.8"
rsa = "0.4"
-serde = {version = "1", features = ["derive"]}
+serde = { version = "1", features = ["derive"] }
[build-dependencies]
-prost-build = {git = "https://github.com/tokio-rs/prost", rev = "6cf97ea422b09d98de34643c4dda2d4f8b7e23e6"}
+prost-build = { git = "https://github.com/tokio-rs/prost", rev = "6cf97ea422b09d98de34643c4dda2d4f8b7e23e6" }
[dev-dependencies]
smol = "1.2.5"
@@ -20,14 +20,14 @@ test-support = ["tempdir", "serde_json", "zed-rpc/test-support"]
anyhow = "1.0.38"
arrayvec = "0.5.2"
async-trait = "0.1"
-async-tungstenite = { version="0.14", features=["async-tls"] }
+async-tungstenite = { version = "0.14", features = ["async-tls"] }
crossbeam-channel = "0.5.0"
ctor = "0.1.20"
dirs = "3.0"
easy-parallel = "3.1.0"
-fsevent = { path="../fsevent" }
+fsevent = { path = "../fsevent" }
futures = "0.3"
-gpui = { path="../gpui" }
+gpui = { path = "../gpui" }
http-auth-basic = "0.1.3"
ignore = "0.4"
lazy_static = "1.4.0"
@@ -35,31 +35,33 @@ libc = "0.2"
log = "0.4"
num_cpus = "1.13.0"
parking_lot = "0.11.1"
-postage = { version="0.4.1", features=["futures-traits"] }
+postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8.3"
rsa = "0.4"
rust-embed = "5.9.0"
seahash = "4.1"
-serde = { version="1", features=["derive"] }
-serde_json = { version="1.0.64", features=["preserve_order"], optional=true }
+serde = { version = "1", features = ["derive"] }
+serde_json = { version = "1.0.64", features = [
+ "preserve_order"
+], optional = true }
similar = "1.3"
simplelog = "0.9"
-smallvec = { version="1.6", features=["union"] }
+smallvec = { version = "1.6", features = ["union"] }
smol = "1.2.5"
surf = "2.2"
-tempdir = { version="0.3.7", optional=true }
+tempdir = { version = "0.3.7", optional = true }
tiny_http = "0.8"
toml = "0.5"
tree-sitter = "0.19.5"
tree-sitter-rust = "0.19.0"
url = "2.2"
-zed-rpc = { path="../zed-rpc" }
+zed-rpc = { path = "../zed-rpc" }
[dev-dependencies]
cargo-bundle = "0.5.0"
env_logger = "0.8"
-serde_json = { version="1.0.64", features=["preserve_order"] }
-tempdir = { version="0.3.7" }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+tempdir = { version = "0.3.7" }
unindent = "0.1.7"
[package.metadata.bundle]