diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 0000000000..22ca5405d8 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,3852 @@ +{ + "projectName": "blitz", + "projectOwner": "blitz-js", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "badgeTemplate": "\"\"-17BB8A.svg?style=for-the-badge&labelColor=000000\">", + "imageSize": 100, + "commit": true, + "commitConvention": "none", + "contributors": [ + { + "login": "flybayer", + "name": "Brandon Bayer", + "avatar_url": "https://avatars3.githubusercontent.com/u/8813276?v=4", + "profile": "https://twitter.com/flybayer", + "contributions": [ + "code", + "content", + "ideas", + "review", + "test", + "doc" + ] + }, + { + "login": "ryardley", + "name": "Rudi Yardley", + "avatar_url": "https://avatars0.githubusercontent.com/u/1256409?v=4", + "profile": "https://medium.com/@ryardley", + "contributions": [ + "code", + "ideas", + "review", + "test" + ] + }, + { + "login": "merelinguist", + "name": "Dylan Brookes", + "avatar_url": "https://avatars3.githubusercontent.com/u/24858006?v=4", + "profile": "https://merelinguist.me", + "contributions": [ + "code", + "ideas", + "review", + "test", + "doc" + ] + }, + { + "login": "aem", + "name": "Adam Markon", + "avatar_url": "https://avatars0.githubusercontent.com/u/1909883?v=4", + "profile": "https://github.com/aem", + "contributions": [ + "code", + "ideas", + "review", + "test", + "maintenance" + ] + }, + { + "login": "coreybrown89", + "name": "Corey Brown", + "avatar_url": "https://avatars1.githubusercontent.com/u/12791148?v=4", + "profile": "https://corey-brown.com", + "contributions": [ + "code", + "review", + "maintenance" + ] + }, + { + "login": "LoriKarikari", + "name": "Lori Karikari", + "avatar_url": "https://avatars1.githubusercontent.com/u/7902980?v=4", + "profile": "https://github.com/LoriKarikari", + "contributions": [ + "code", + "review", + "maintenance", + "doc" + ] + }, + { + "login": "eliasjohansson", + "name": "Elias Johansson", + "avatar_url": "https://avatars3.githubusercontent.com/u/22719177?v=4", + "profile": "https://twitter.com/GeggsElias", + "contributions": [ + "code", + "review", + "maintenance" + ] + }, + { + "login": "medelman17", + "name": "Michael Edelman ", + "avatar_url": "https://avatars1.githubusercontent.com/u/14793389?v=4", + "profile": "https://fabulas.io", + "contributions": [ + "infra", + "code" + ] + }, + { + "login": "toddgeist", + "name": "Todd Geist", + "avatar_url": "https://avatars2.githubusercontent.com/u/316792?v=4", + "profile": "http://www.geistinteractive.com", + "contributions": [ + "financial", + "code" + ] + }, + { + "login": "robdrosenberg", + "name": "Robert Rosenberg", + "avatar_url": "https://avatars0.githubusercontent.com/u/20813991?v=4", + "profile": "http://robdrosenberg.com", + "contributions": [ + "code", + "maintenance", + "doc" + ] + }, + { + "login": "quirk0o", + "name": "Beata Obrok", + "avatar_url": "https://avatars3.githubusercontent.com/u/5123725?v=4", + "profile": "https://github.com/quirk0o", + "contributions": [ + "code" + ] + }, + { + "login": "tsawan", + "name": "Tahir Awan", + "avatar_url": "https://avatars3.githubusercontent.com/u/3263082?v=4", + "profile": "https://github.com/tsawan", + "contributions": [ + "code" + ] + }, + { + "login": "camilo86", + "name": "Camilo Gonzalez", + "avatar_url": "https://avatars1.githubusercontent.com/u/2454632?v=4", + "profile": "https://raluce.com", + "contributions": [ + "code" + ] + }, + { + "login": "dkempner", + "name": "Daniel Kempner", + "avatar_url": "https://avatars3.githubusercontent.com/u/2532112?v=4", + "profile": "http://da.nielkempner.com", + "contributions": [ + "code" + ] + }, + { + "login": "gielcobben", + "name": "Giel", + "avatar_url": "https://avatars0.githubusercontent.com/u/2663212?v=4", + "profile": "http://gielcobben.com", + "contributions": [ + "code" + ] + }, + { + "login": "MrLeebo", + "name": "Jeremy Liberman", + "avatar_url": "https://avatars3.githubusercontent.com/u/2754163?v=4", + "profile": "http://jeremyliberman.com/", + "contributions": [ + "code", + "maintenance", + "test", + "doc" + ] + }, + { + "login": "jimthedev", + "name": "Jim Cummins", + "avatar_url": "https://avatars0.githubusercontent.com/u/108938?v=4", + "profile": "https://jimthedev.com", + "contributions": [ + "code" + ] + }, + { + "name": "Kristina Matuška", + "avatar_url": "https://media-exp1.licdn.com/dms/image/C5603AQHVPAjV21gw9g/profile-displayphoto-shrink_200_200/0?e=1591228800&v=beta&t=0MlbmiYhNvGv1xjLD_fOhOFjVDZ7ltNwfGNeJ4DHedQ", + "profile": "http://kristinamatuska.com/", + "contributions": [ + "design" + ] + }, + { + "login": "jasonblalock", + "name": "Jason Blalock", + "avatar_url": "https://avatars0.githubusercontent.com/u/5899929?v=4", + "profile": "https://github.com/jasonblalock", + "contributions": [ + "code" + ] + }, + { + "login": "aej11a", + "name": "aej11a", + "avatar_url": "https://avatars2.githubusercontent.com/u/10066422?v=4", + "profile": "https://github.com/aej11a", + "contributions": [ + "code" + ] + }, + { + "login": "marcoseoane", + "name": "marcoseoane", + "avatar_url": "https://avatars0.githubusercontent.com/u/28088807?v=4", + "profile": "https://github.com/marcoseoane", + "contributions": [ + "ideas" + ] + }, + { + "login": "rishabhpoddar", + "name": "Rishabh Poddar", + "avatar_url": "https://avatars2.githubusercontent.com/u/2976287?v=4", + "profile": "https://github.com/rishabhpoddar", + "contributions": [ + "ideas" + ] + }, + { + "login": "lorenzorapetti", + "name": "Lorenzo Rapetti", + "avatar_url": "https://avatars1.githubusercontent.com/u/2632174?v=4", + "profile": "https://github.com/lorenzorapetti", + "contributions": [ + "code" + ] + }, + { + "login": "wKovacs64", + "name": "Justin Hall", + "avatar_url": "https://avatars1.githubusercontent.com/u/1288694?v=4", + "profile": "https://github.com/wKovacs64", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "sijad", + "name": "Sajjad Hashemian", + "avatar_url": "https://avatars3.githubusercontent.com/u/7693001?v=4", + "profile": "https://github.com/sijad", + "contributions": [ + "code" + ] + }, + { + "login": "ETLopes", + "name": "Eduardo Lopes", + "avatar_url": "https://avatars3.githubusercontent.com/u/34959471?v=4", + "profile": "https://github.com/ETLopes", + "contributions": [ + "code" + ] + }, + { + "login": "mattleff", + "name": "Matthew Leffler", + "avatar_url": "https://avatars0.githubusercontent.com/u/120155?v=4", + "profile": "https://github.com/mattleff", + "contributions": [ + "doc" + ] + }, + { + "login": "hew", + "name": "Matt", + "avatar_url": "https://avatars0.githubusercontent.com/u/3103241?v=4", + "profile": "https://hew.tools", + "contributions": [ + "doc" + ] + }, + { + "login": "sonnypgs", + "name": "Sonny", + "avatar_url": "https://avatars3.githubusercontent.com/u/1431300?v=4", + "profile": "https://github.com/sonnypgs", + "contributions": [ + "doc" + ] + }, + { + "login": "Zeko369", + "name": "Fran Zekan", + "avatar_url": "https://avatars3.githubusercontent.com/u/3064377?v=4", + "profile": "https://github.com/Zeko369", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "janbaykara", + "name": "Jan Baykara", + "avatar_url": "https://avatars2.githubusercontent.com/u/237556?v=4", + "profile": "http://twitter.com/JanBaykara", + "contributions": [ + "doc" + ] + }, + { + "login": "mikeattara", + "name": "Mike Perry Y Attara", + "avatar_url": "https://avatars1.githubusercontent.com/u/31483629?v=4", + "profile": "https://mikeattara.com", + "contributions": [ + "doc" + ] + }, + { + "login": "DevanB", + "name": "Devan", + "avatar_url": "https://avatars0.githubusercontent.com/u/354652?v=4", + "profile": "https://devanthe.dev", + "contributions": [ + "doc" + ] + }, + { + "login": "jclancy93", + "name": "Jack Clancy", + "avatar_url": "https://avatars2.githubusercontent.com/u/7850202?v=4", + "profile": "https://github.com/jclancy93", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "ntgussoni", + "name": "Nicolas Torres", + "avatar_url": "https://avatars0.githubusercontent.com/u/10161067?v=4", + "profile": "https://github.com/ntgussoni", + "contributions": [ + "test", + "code", + "review", + "doc" + ] + }, + { + "login": "Skn0tt", + "name": "Simon Knott", + "avatar_url": "https://avatars1.githubusercontent.com/u/14912729?v=4", + "profile": "http://simonknott.de", + "contributions": [ + "code", + "test", + "maintenance", + "doc" + ] + }, + { + "login": "kandros", + "name": "Jaga Santagostino", + "avatar_url": "https://avatars0.githubusercontent.com/u/4562878?v=4", + "profile": "http://jagascript.com", + "contributions": [ + "code", + "doc", + "maintenance" + ] + }, + { + "login": "jportela", + "name": "João Portela", + "avatar_url": "https://avatars0.githubusercontent.com/u/1010018?v=4", + "profile": "http://www.joaoportela.com", + "contributions": [ + "code" + ] + }, + { + "login": "dajinchu", + "name": "Da-Jin Chu", + "avatar_url": "https://avatars0.githubusercontent.com/u/7122182?v=4", + "profile": "http://dajin.dev", + "contributions": [ + "code" + ] + }, + { + "login": "Shinyaigeek", + "name": "Shinobu Hayashi", + "avatar_url": "https://avatars1.githubusercontent.com/u/42742053?v=4", + "profile": "https://shinyaigeek.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "karankiri", + "name": "Karan Kiri", + "avatar_url": "https://avatars2.githubusercontent.com/u/19989161?v=4", + "profile": "http://karankiri.com", + "contributions": [ + "code" + ] + }, + { + "login": "fullmetalengineer", + "name": "Alan Long", + "avatar_url": "https://avatars2.githubusercontent.com/u/5294903?v=4", + "profile": "https://github.com/fullmetalengineer", + "contributions": [ + "doc" + ] + }, + { + "login": "developerfred", + "name": "codingsh", + "avatar_url": "https://avatars2.githubusercontent.com/u/57037080?v=4", + "profile": "http://codingsh.xyz", + "contributions": [ + "code" + ] + }, + { + "login": "peaonunes", + "name": "Rafael Nunes", + "avatar_url": "https://avatars0.githubusercontent.com/u/3356720?v=4", + "profile": "http://twitter.com/peaonunes", + "contributions": [ + "review", + "code" + ] + }, + { + "login": "0ww", + "name": "Simon Debbarma", + "avatar_url": "https://avatars3.githubusercontent.com/u/31207418?v=4", + "profile": "https://simonpeterdebbarma.com", + "contributions": [ + "design", + "maintenance", + "doc" + ] + }, + { + "login": "0xflotus", + "name": "0xflotus", + "avatar_url": "https://avatars3.githubusercontent.com/u/26602940?v=4", + "profile": "https://github.com/0xflotus", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "tmns", + "name": "tmns", + "avatar_url": "https://avatars3.githubusercontent.com/u/35785003?v=4", + "profile": "https://dev.to/tmns", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "harris1717", + "name": "Jru Harris", + "avatar_url": "https://avatars1.githubusercontent.com/u/8636691?v=4", + "profile": "http://jruharris.com", + "contributions": [ + "doc" + ] + }, + { + "login": "ivandevp", + "name": "Ivan Medina", + "avatar_url": "https://avatars3.githubusercontent.com/u/9284690?v=4", + "profile": "https://twitter.com/ivandevp", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "dwightwatson", + "name": "Dwight Watson", + "avatar_url": "https://avatars3.githubusercontent.com/u/1100408?v=4", + "profile": "https://www.dwightwatson.com", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "is2ei", + "name": "Horie Issei", + "avatar_url": "https://avatars3.githubusercontent.com/u/3948353?v=4", + "profile": "http://is2ei.com/", + "contributions": [ + "code" + ] + }, + { + "login": "lednhatkhanh", + "name": "Nhat Khanh", + "avatar_url": "https://avatars2.githubusercontent.com/u/9303093?v=4", + "profile": "https://twitter.com/lednhatkhanh", + "contributions": [ + "code" + ] + }, + { + "login": "abuuzayr", + "name": "Abu Uzayr", + "avatar_url": "https://avatars1.githubusercontent.com/u/19371989?v=4", + "profile": "https://builtforfifty.com", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "nabi009", + "name": "Nabiullah elham", + "avatar_url": "https://avatars0.githubusercontent.com/u/3170831?v=4", + "profile": "https://github.com/nabi009", + "contributions": [ + "code" + ] + }, + { + "login": "lachlanjc", + "name": "Lachlan Campbell", + "avatar_url": "https://avatars1.githubusercontent.com/u/5074763?v=4", + "profile": "https://lachlanjc.com", + "contributions": [ + "code" + ] + }, + { + "login": "enzoferey", + "name": "Enzo Ferey", + "avatar_url": "https://avatars1.githubusercontent.com/u/10673347?v=4", + "profile": "http://enzoferey.com", + "contributions": [ + "code" + ] + }, + { + "login": "pgrimaud", + "name": "Pierre Grimaud", + "avatar_url": "https://avatars1.githubusercontent.com/u/1866496?v=4", + "profile": "https://github.com/pgrimaud", + "contributions": [ + "code" + ] + }, + { + "login": "pixelmord", + "name": "Andreas Adam", + "avatar_url": "https://avatars2.githubusercontent.com/u/224168?v=4", + "profile": "https://pixelmord.github.io", + "contributions": [ + "code" + ] + }, + { + "login": "kevotovar", + "name": "Kevin Tovar", + "avatar_url": "https://avatars3.githubusercontent.com/u/15717067?v=4", + "profile": "https://kevo.dev", + "contributions": [ + "code" + ] + }, + { + "login": "anteprimorac", + "name": "Ante Primorac", + "avatar_url": "https://avatars0.githubusercontent.com/u/972083?v=4", + "profile": "http://anteprimorac.com.hr", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "MykalMachon", + "name": "Mykal Machon", + "avatar_url": "https://avatars1.githubusercontent.com/u/7844994?v=4", + "profile": "http://mykalmachon.dev", + "contributions": [ + "code" + ] + }, + { + "login": "jamiedavenport", + "name": "Jamie Davenport", + "avatar_url": "https://avatars2.githubusercontent.com/u/1329874?v=4", + "profile": "https://jamiedavenport.dev", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "ganeshmani", + "name": "GaneshMani", + "avatar_url": "https://avatars0.githubusercontent.com/u/17050715?v=4", + "profile": "https://cloudnweb.dev/", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "reymon359", + "name": "reymon359", + "avatar_url": "https://avatars3.githubusercontent.com/u/31936665?v=4", + "profile": "http://ramonmorcillo.com", + "contributions": [ + "code" + ] + }, + { + "login": "gvasquez11", + "name": "gvasquez11", + "avatar_url": "https://avatars1.githubusercontent.com/u/36422346?v=4", + "profile": "https://www.linkedin.com/in/gregory-vasquez-96413b184/", + "contributions": [ + "code" + ] + }, + { + "login": "josemiguelo", + "name": " José Miguel Ochoa", + "avatar_url": "https://avatars1.githubusercontent.com/u/15330034?v=4", + "profile": "https://github.com/josemiguelo", + "contributions": [ + "code" + ] + }, + { + "login": "osirvent", + "name": "Oscar Sirvent", + "avatar_url": "https://avatars2.githubusercontent.com/u/5927133?v=4", + "profile": "https://github.com/osirvent", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "donni106", + "name": "Daniel Molnar", + "avatar_url": "https://avatars0.githubusercontent.com/u/1942953?v=4", + "profile": "https://github.com/donni106", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "exclipy", + "name": "Kevin Wu Won", + "avatar_url": "https://avatars1.githubusercontent.com/u/508799?v=4", + "profile": "https://github.com/exclipy", + "contributions": [ + "doc" + ] + }, + { + "login": "tehnuge", + "name": "John Duong", + "avatar_url": "https://avatars1.githubusercontent.com/u/1928236?v=4", + "profile": "https://github.com/tehnuge", + "contributions": [ + "code" + ] + }, + { + "login": "fnoah", + "name": "Noah Fleischmann", + "avatar_url": "https://avatars0.githubusercontent.com/u/23707137?v=4", + "profile": "https://noahfleischmann.com", + "contributions": [ + "code" + ] + }, + { + "login": "toshi1127", + "name": "Matsumoto Toshi", + "avatar_url": "https://avatars3.githubusercontent.com/u/32378535?v=4", + "profile": "https://github.com/toshi1127", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "simonedelmann", + "name": "Simon Edelmann", + "avatar_url": "https://avatars2.githubusercontent.com/u/2821076?v=4", + "profile": "https://github.com/simonedelmann", + "contributions": [ + "code" + ] + }, + { + "login": "shaunchurch", + "name": "Shaun Church", + "avatar_url": "https://avatars3.githubusercontent.com/u/571764?v=4", + "profile": "https://shaun.church", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "styfle", + "name": "Steven", + "avatar_url": "https://avatars1.githubusercontent.com/u/229881?v=4", + "profile": "https://styfle.dev", + "contributions": [ + "doc" + ] + }, + { + "login": "SigurdMW", + "name": "Sigurd Moland Wahl", + "avatar_url": "https://avatars3.githubusercontent.com/u/6359003?v=4", + "profile": "https://github.com/SigurdMW", + "contributions": [ + "code" + ] + }, + { + "login": "sbardian", + "name": "Brian Andrews", + "avatar_url": "https://avatars1.githubusercontent.com/u/6384100?v=4", + "profile": "https://brianandrews.dev/", + "contributions": [ + "doc" + ] + }, + { + "login": "garrisons", + "name": "Garrison Snelling", + "avatar_url": "https://avatars0.githubusercontent.com/u/5100597?v=4", + "profile": "http://garrisonsnelling.com", + "contributions": [ + "doc" + ] + }, + { + "login": "tylangesmith", + "name": "Ty Lange-Smith", + "avatar_url": "https://avatars1.githubusercontent.com/u/22609577?v=4", + "profile": "https://github.com/tylangesmith", + "contributions": [ + "code" + ] + }, + { + "login": "rubenmoya", + "name": "Rubén Moya", + "avatar_url": "https://avatars3.githubusercontent.com/u/905225?v=4", + "profile": "https://rubenmoya.dev", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "robertgrzonka", + "name": "robertgrzonka", + "avatar_url": "https://avatars0.githubusercontent.com/u/35585466?v=4", + "profile": "https://github.com/robertgrzonka", + "contributions": [ + "code", + "infra" + ] + }, + { + "login": "agoxlea", + "name": "Alex Orr", + "avatar_url": "https://avatars3.githubusercontent.com/u/1240841?v=4", + "profile": "https://github.com/agoxlea", + "contributions": [ + "code" + ] + }, + { + "login": "chris-tse", + "name": "Chris Tse", + "avatar_url": "https://avatars1.githubusercontent.com/u/250450?v=4", + "profile": "https://christse.io", + "contributions": [ + "code" + ] + }, + { + "login": "nettofarah", + "name": "Netto Farah", + "avatar_url": "https://avatars1.githubusercontent.com/u/270688?v=4", + "profile": "http://twitter.com/nettofarah", + "contributions": [ + "code" + ] + }, + { + "login": "rohanjulka19", + "name": "Rohan Julka", + "avatar_url": "https://avatars0.githubusercontent.com/u/19673968?v=4", + "profile": "https://github.com/rohanjulka19", + "contributions": [ + "infra" + ] + }, + { + "login": "pragmaticivan", + "name": "Ivan Santos", + "avatar_url": "https://avatars3.githubusercontent.com/u/301291?v=4", + "profile": "https://www.ivansantos.me", + "contributions": [ + "code" + ] + }, + { + "login": "drenther", + "name": "Soumyajit Pathak", + "avatar_url": "https://avatars0.githubusercontent.com/u/12991390?v=4", + "profile": "https://able.bio", + "contributions": [ + "code" + ] + }, + { + "login": "SebastianKurp", + "name": "Sebastian Kurpiel", + "avatar_url": "https://avatars2.githubusercontent.com/u/16307737?v=4", + "profile": "http://www.sebastiankurpiel.com", + "contributions": [ + "doc" + ] + }, + { + "login": "scisteffan", + "name": "Steffan", + "avatar_url": "https://avatars2.githubusercontent.com/u/2676185?v=4", + "profile": "https://github.com/scisteffan", + "contributions": [ + "code", + "doc", + "financial" + ] + }, + { + "login": "kripod", + "name": "Kristóf Poduszló", + "avatar_url": "https://avatars3.githubusercontent.com/u/14854048?v=4", + "profile": "https://github.com/kripod", + "contributions": [ + "code" + ] + }, + { + "login": "Weilbyte", + "name": "Weilbyte", + "avatar_url": "https://avatars1.githubusercontent.com/u/43392677?v=4", + "profile": "https://github.com/Weilbyte", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "cardotrejos", + "name": "Ricardo Trejos", + "avatar_url": "https://avatars1.githubusercontent.com/u/8602086?v=4", + "profile": "http://ricardotrejos.tech", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "karaggeorge", + "name": "George Karagkiaouris", + "avatar_url": "https://avatars0.githubusercontent.com/u/8822835?v=4", + "profile": "https://gkaragkiaouris.tech/", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "bpas247", + "name": "Brady Pascoe", + "avatar_url": "https://avatars0.githubusercontent.com/u/18705892?v=4", + "profile": "https://www.linkedin.com/in/brady-pascoe-3bba6b13a/", + "contributions": [ + "code" + ] + }, + { + "login": "svobik7", + "name": "Jirka Svoboda", + "avatar_url": "https://avatars1.githubusercontent.com/u/761766?v=4", + "profile": "https://www.yeahcoach.com", + "contributions": [ + "code" + ] + }, + { + "login": "alan2207", + "name": "Alan Alickovic", + "avatar_url": "https://avatars3.githubusercontent.com/u/12713315?v=4", + "profile": "https://github.com/alan2207", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "yhoiseth", + "name": "Yngve Høiseth", + "avatar_url": "https://avatars0.githubusercontent.com/u/8469540?v=4", + "profile": "https://yngve.hoiseth.net", + "contributions": [ + "doc" + ] + }, + { + "login": "brunocrosier", + "name": "Bruno Crosier", + "avatar_url": "https://avatars1.githubusercontent.com/u/18399089?v=4", + "profile": "https://twitter.com/bruno_crosier", + "contributions": [ + "doc" + ] + }, + { + "login": "jschepmans", + "name": "Johan Schepmans", + "avatar_url": "https://avatars2.githubusercontent.com/u/5782977?v=4", + "profile": "https://github.com/jschepmans", + "contributions": [ + "code" + ] + }, + { + "login": "dillonraphael", + "name": "Dillon Raphael", + "avatar_url": "https://avatars0.githubusercontent.com/u/3496193?v=4", + "profile": "https://twitter.com/dillonraphael", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "clgeoio", + "name": "Cody G", + "avatar_url": "https://avatars2.githubusercontent.com/u/37571416?v=4", + "profile": "https://github.com/clgeoio", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "madflow", + "name": "madflow", + "avatar_url": "https://avatars0.githubusercontent.com/u/183248?v=4", + "profile": "https://github.com/madflow", + "contributions": [ + "doc" + ] + }, + { + "login": "nitaking", + "name": "Satoshi Nitawaki", + "avatar_url": "https://avatars2.githubusercontent.com/u/10850034?v=4", + "profile": "https://twitter.com/nitaking_", + "contributions": [ + "code", + "maintenance", + "question", + "doc" + ] + }, + { + "login": "sirmyron", + "name": "sirmyron", + "avatar_url": "https://avatars2.githubusercontent.com/u/1430136?v=4", + "profile": "https://github.com/sirmyron", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "engelkes-finstreet", + "name": "engelkes-finstreet", + "avatar_url": "https://avatars1.githubusercontent.com/u/36962022?v=4", + "profile": "https://github.com/engelkes-finstreet", + "contributions": [ + "doc", + "code", + "maintenance" + ] + }, + { + "login": "PixelsCommander", + "name": "Denis Radin", + "avatar_url": "https://avatars2.githubusercontent.com/u/810671?v=4", + "profile": "http://twitter.com/pixelscommander", + "contributions": [ + "review", + "code", + "doc" + ] + }, + { + "login": "xiaoyu-tamu", + "name": "Michael Li", + "avatar_url": "https://avatars3.githubusercontent.com/u/33362998?v=4", + "profile": "https://github.com/xiaoyu-tamu", + "contributions": [ + "code" + ] + }, + { + "login": "yuta0801", + "name": "yuta0801", + "avatar_url": "https://avatars2.githubusercontent.com/u/21266306?v=4", + "profile": "https://github.com/yuta0801", + "contributions": [ + "code" + ] + }, + { + "login": "Obii-bit", + "name": "Obadja Ris", + "avatar_url": "https://avatars2.githubusercontent.com/u/67339820?v=4", + "profile": "https://github.com/Obii-bit", + "contributions": [ + "doc" + ] + }, + { + "login": "JoseRFelix", + "name": "Jose Felix ", + "avatar_url": "https://avatars2.githubusercontent.com/u/21092519?v=4", + "profile": "http://jfelix.info", + "contributions": [ + "code" + ] + }, + { + "login": "johncantrell97", + "name": "John Cantrell", + "avatar_url": "https://avatars3.githubusercontent.com/u/41305919?v=4", + "profile": "https://github.com/johncantrell97", + "contributions": [ + "code" + ] + }, + { + "login": "cktang88", + "name": "Kwuang Tang", + "avatar_url": "https://avatars1.githubusercontent.com/u/10319942?v=4", + "profile": "http://kwuang.me", + "contributions": [ + "code" + ] + }, + { + "login": "johnletey", + "name": "John Letey", + "avatar_url": "https://avatars1.githubusercontent.com/u/62398724?v=4", + "profile": "https://github.com/johnletey", + "contributions": [ + "code" + ] + }, + { + "login": "ditorojuan", + "name": "Juan Di Toro", + "avatar_url": "https://avatars0.githubusercontent.com/u/22530892?v=4", + "profile": "https://github.com/ditorojuan", + "contributions": [ + "code" + ] + }, + { + "login": "taylorcjohnson", + "name": "Taylor Johnson", + "avatar_url": "https://avatars0.githubusercontent.com/u/10552296?v=4", + "profile": "https://github.com/taylorcjohnson", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "tsriram", + "name": "Sriram Thiagarajan", + "avatar_url": "https://avatars3.githubusercontent.com/u/450559?v=4", + "profile": "https://twitter.com/tsriram", + "contributions": [ + "doc" + ] + }, + { + "login": "sergiodxa", + "name": "Sergio Xalambrí", + "avatar_url": "https://avatars2.githubusercontent.com/u/1312018?v=4", + "profile": "https://sergiodxa.com", + "contributions": [ + "doc" + ] + }, + { + "login": "doeixd", + "name": "Patrick G", + "avatar_url": "https://avatars3.githubusercontent.com/u/13461122?v=4", + "profile": "https://github.com/doeixd", + "contributions": [ + "code" + ] + }, + { + "login": "hardfire", + "name": "अभिनाश (Avinash)", + "avatar_url": "https://avatars3.githubusercontent.com/u/513457?v=4", + "profile": "http://avinash.com.np", + "contributions": [ + "code" + ] + }, + { + "login": "enricoschaaf", + "name": "Enrico Schaaf", + "avatar_url": "https://avatars1.githubusercontent.com/u/54645197?v=4", + "profile": "http://enricoschaaf.com", + "contributions": [ + "code" + ] + }, + { + "login": "kitze", + "name": "Kitze", + "avatar_url": "https://avatars0.githubusercontent.com/u/1160594?v=4", + "profile": "http://kitze.io", + "contributions": [ + "ideas" + ] + }, + { + "login": "drmas", + "name": "Mohamed Shaban", + "avatar_url": "https://avatars3.githubusercontent.com/u/644440?v=4", + "profile": "https://github.com/drmas", + "contributions": [ + "code" + ] + }, + { + "login": "jorisre", + "name": "Joris", + "avatar_url": "https://avatars1.githubusercontent.com/u/7545547?v=4", + "profile": "https://github.com/jorisre", + "contributions": [ + "code" + ] + }, + { + "login": "Kamshak", + "name": "Valentin Funk", + "avatar_url": "https://avatars3.githubusercontent.com/u/337968?v=4", + "profile": "https://github.com/Kamshak", + "contributions": [ + "doc" + ] + }, + { + "login": "lukebennett", + "name": "Luke Bennett", + "avatar_url": "https://avatars1.githubusercontent.com/u/135390?v=4", + "profile": "https://lukebennett.com", + "contributions": [ + "code" + ] + }, + { + "login": "hmajid2301", + "name": "Haseeb Majid", + "avatar_url": "https://avatars0.githubusercontent.com/u/998807?v=4", + "profile": "https://haseebmajid.dev", + "contributions": [ + "code" + ] + }, + { + "login": "phillippschmedt", + "name": "Phillipp Schmedt", + "avatar_url": "https://avatars0.githubusercontent.com/u/16028406?v=4", + "profile": "https://github.com/phillippschmedt", + "contributions": [ + "code" + ] + }, + { + "login": "hasparus", + "name": "Piotr Monwid-Olechnowicz", + "avatar_url": "https://avatars0.githubusercontent.com/u/15332326?v=4", + "profile": "https://haspar.us", + "contributions": [ + "code" + ] + }, + { + "login": "mizchi", + "name": "Kotaro Chikuba", + "avatar_url": "https://avatars2.githubusercontent.com/u/73962?v=4", + "profile": "https://mizchi.dev", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "konradkalemba", + "name": "Konrad Kalemba", + "avatar_url": "https://avatars0.githubusercontent.com/u/8682104?v=4", + "profile": "https://github.com/konradkalemba", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "Alucard17", + "name": "Alucard17", + "avatar_url": "https://avatars1.githubusercontent.com/u/26205172?v=4", + "profile": "https://github.com/Alucard17", + "contributions": [ + "code" + ] + }, + { + "login": "Dohxis", + "name": "Domantas Mauruča", + "avatar_url": "https://avatars2.githubusercontent.com/u/8768909?v=4", + "profile": "https://github.com/Dohxis", + "contributions": [ + "test", + "code" + ] + }, + { + "login": "sandulat", + "name": "Stratulat Alexandru", + "avatar_url": "https://avatars0.githubusercontent.com/u/7345874?v=4", + "profile": "https://sandulat.com/", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "aericson", + "name": "André Ericson", + "avatar_url": "https://avatars3.githubusercontent.com/u/692542?v=4", + "profile": "https://github.com/aericson", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "cajotafer", + "name": "Carlos Fernández", + "avatar_url": "https://avatars2.githubusercontent.com/u/41461969?v=4", + "profile": "http://Cajotafer.com", + "contributions": [ + "doc" + ] + }, + { + "login": "Kosai106", + "name": "Kevin Østerkilde", + "avatar_url": "https://avatars1.githubusercontent.com/u/6379824?v=4", + "profile": "https://oesterkilde.dk/", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "aaronfulkerson", + "name": "aaronfulkerson", + "avatar_url": "https://avatars0.githubusercontent.com/u/31112737?v=4", + "profile": "https://github.com/aaronfulkerson", + "contributions": [ + "code", + "question" + ] + }, + { + "login": "alexnaiman", + "name": "Alexandru Naiman", + "avatar_url": "https://avatars3.githubusercontent.com/u/25799714?v=4", + "profile": "https://github.com/alexnaiman", + "contributions": [ + "code" + ] + }, + { + "login": "davidlutta", + "name": "David Ezekiel Lutta", + "avatar_url": "https://avatars2.githubusercontent.com/u/14890315?v=4", + "profile": "https://davidlutta.github.io/portfolio/", + "contributions": [ + "code" + ] + }, + { + "login": "wanjuntham", + "name": "wanjuntham", + "avatar_url": "https://avatars1.githubusercontent.com/u/49380551?v=4", + "profile": "https://github.com/wanjuntham", + "contributions": [ + "code" + ] + }, + { + "login": "nahue", + "name": "Victor Nahuel Chaves", + "avatar_url": "https://avatars3.githubusercontent.com/u/96837?v=4", + "profile": "http://www.nahuelchaves.xyz", + "contributions": [ + "code" + ] + }, + { + "login": "peter50216", + "name": "Peter Shih", + "avatar_url": "https://avatars3.githubusercontent.com/u/891109?v=4", + "profile": "https://github.com/peter50216", + "contributions": [ + "code" + ] + }, + { + "login": "sewerynkalemba", + "name": "Seweryn Kalemba", + "avatar_url": "https://avatars3.githubusercontent.com/u/37031328?v=4", + "profile": "http://seweryn.kale.mba", + "contributions": [ + "code" + ] + }, + { + "login": "nksaraf", + "name": "Nikhil Saraf", + "avatar_url": "https://avatars2.githubusercontent.com/u/11255148?v=4", + "profile": "https://nksaraf.github.io", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "zanedb", + "name": "Zane", + "avatar_url": "https://avatars0.githubusercontent.com/u/16865690?v=4", + "profile": "https://zane.sh", + "contributions": [ + "doc" + ] + }, + { + "login": "dulcehc", + "name": "Dulce Hernández", + "avatar_url": "https://avatars1.githubusercontent.com/u/19391835?v=4", + "profile": "https://github.com/dulcehc", + "contributions": [ + "code" + ] + }, + { + "login": "markhaehnel", + "name": "Mark Hähnel", + "avatar_url": "https://avatars2.githubusercontent.com/u/1516205?v=4", + "profile": "https://markhaehnel.de", + "contributions": [ + "code" + ] + }, + { + "login": "nemesv", + "name": "Viktor Nemes", + "avatar_url": "https://avatars0.githubusercontent.com/u/251330?v=4", + "profile": "http://stackoverflow.com/users/872395/nemesv", + "contributions": [ + "code" + ] + }, + { + "login": "goleary", + "name": "Gabe O'Leary", + "avatar_url": "https://avatars1.githubusercontent.com/u/16123225?v=4", + "profile": "http://gabeoleary.com", + "contributions": [ + "doc" + ] + }, + { + "login": "machadolucasvp", + "name": "Lucas Machado", + "avatar_url": "https://avatars0.githubusercontent.com/u/44952113?v=4", + "profile": "https://github.com/machadolucasvp", + "contributions": [ + "code" + ] + }, + { + "login": "maciekgrzybek", + "name": "maciek_grzybek", + "avatar_url": "https://avatars2.githubusercontent.com/u/16546428?v=4", + "profile": "https://github.com/maciekgrzybek", + "contributions": [ + "code" + ] + }, + { + "login": "mweibel", + "name": "Michael Weibel", + "avatar_url": "https://avatars1.githubusercontent.com/u/307427?v=4", + "profile": "https://github.com/mweibel", + "contributions": [ + "code" + ] + }, + { + "login": "isoppp", + "name": "Hiroki Isogai", + "avatar_url": "https://avatars0.githubusercontent.com/u/16318727?v=4", + "profile": "https://isoppp.com", + "contributions": [ + "code" + ] + }, + { + "login": "matamatanot", + "name": "matamatanot", + "avatar_url": "https://avatars2.githubusercontent.com/u/39780486?v=4", + "profile": "https://github.com/matamatanot", + "contributions": [ + "doc" + ] + }, + { + "login": "ericsakmar", + "name": "Eric Sakmar", + "avatar_url": "https://avatars3.githubusercontent.com/u/5620709?v=4", + "profile": "https://github.com/ericsakmar", + "contributions": [ + "doc" + ] + }, + { + "login": "leggsimon", + "name": "Simon Legg", + "avatar_url": "https://avatars2.githubusercontent.com/u/11544418?v=4", + "profile": "https://github.com/leggsimon", + "contributions": [ + "doc" + ] + }, + { + "login": "wobsoriano", + "name": "Robert Soriano", + "avatar_url": "https://avatars3.githubusercontent.com/u/13049130?v=4", + "profile": "https://robsoriano.com", + "contributions": [ + "code" + ] + }, + { + "login": "benediktms", + "name": "Benedikt Schnatterbeck", + "avatar_url": "https://avatars2.githubusercontent.com/u/48836135?v=4", + "profile": "https://github.com/benediktms", + "contributions": [ + "code" + ] + }, + { + "login": "Talor-A", + "name": "Talor Anderson", + "avatar_url": "https://avatars2.githubusercontent.com/u/11509865?v=4", + "profile": "http://taloranderson.com", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "akirabaruah", + "name": "Akira Baruah", + "avatar_url": "https://avatars2.githubusercontent.com/u/6751517?v=4", + "profile": "https://github.com/akirabaruah", + "contributions": [ + "code" + ] + }, + { + "login": "cwray-tech", + "name": "Christopher Wray", + "avatar_url": "https://avatars0.githubusercontent.com/u/53663762?v=4", + "profile": "https://chriswray.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "piotrski", + "name": "Piotrek Tomczewski", + "avatar_url": "https://avatars0.githubusercontent.com/u/244174?v=4", + "profile": "https://github.com/piotrski", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "rap2hpoutre", + "name": "Raphaël Huchet", + "avatar_url": "https://avatars3.githubusercontent.com/u/1575946?v=4", + "profile": "http://raph.site", + "contributions": [ + "doc", + "test", + "code" + ] + }, + { + "login": "KATT", + "name": "Alex Johansson", + "avatar_url": "https://avatars1.githubusercontent.com/u/459267?v=4", + "profile": "http://kattcorp.com", + "contributions": [ + "code" + ] + }, + { + "login": "dmzza", + "name": "David Mazza", + "avatar_url": "https://avatars0.githubusercontent.com/u/120893?v=4", + "profile": "http://davidmazza.com", + "contributions": [ + "code" + ] + }, + { + "login": "rayandrews", + "name": "Ray Andrew", + "avatar_url": "https://avatars1.githubusercontent.com/u/4437323?v=4", + "profile": "https://github.com/rayandrews", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "Mzaien", + "name": "Abdullah Mzaien", + "avatar_url": "https://avatars3.githubusercontent.com/u/43112535?v=4", + "profile": "http://Dal.Design", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "williamkwao", + "name": "William Kwao", + "avatar_url": "https://avatars2.githubusercontent.com/u/8839514?v=4", + "profile": "http://kwao.io", + "contributions": [ + "doc" + ] + }, + { + "login": "sakulstra", + "name": "Lukas Strassel", + "avatar_url": "https://avatars3.githubusercontent.com/u/4396533?v=4", + "profile": "https://github.com/sakulstra", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "tpatel", + "name": "Thibaut Patel", + "avatar_url": "https://avatars3.githubusercontent.com/u/494686?v=4", + "profile": "https://thibpat.com", + "contributions": [ + "code" + ] + }, + { + "login": "jonstuebe", + "name": "Jon Stuebe", + "avatar_url": "https://avatars0.githubusercontent.com/u/156722?v=4", + "profile": "http://jonstuebe.com", + "contributions": [ + "code" + ] + }, + { + "login": "ugogo", + "name": "Ugo Onali", + "avatar_url": "https://avatars2.githubusercontent.com/u/5040476?v=4", + "profile": "https://ugogo.dev", + "contributions": [ + "doc" + ] + }, + { + "login": "saintmalik", + "name": "SaintMalik", + "avatar_url": "https://avatars1.githubusercontent.com/u/37118134?v=4", + "profile": "https://saintmalik.me", + "contributions": [ + "doc" + ] + }, + { + "login": "Khaledgarbaya", + "name": "Khaled Garbaya", + "avatar_url": "https://avatars1.githubusercontent.com/u/1156093?v=4", + "profile": "https://khaledgarbaya.net", + "contributions": [ + "code" + ] + }, + { + "login": "tundera", + "name": "tundera", + "avatar_url": "https://avatars0.githubusercontent.com/u/61833561?v=4", + "profile": "https://tundera.dev", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "markylaing", + "name": "markylaing", + "avatar_url": "https://avatars2.githubusercontent.com/u/41469221?v=4", + "profile": "https://github.com/markylaing", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "AkifumiSato", + "name": "Akifumi Sato", + "avatar_url": "https://avatars2.githubusercontent.com/u/25711332?v=4", + "profile": "https://akfm.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "beeplin", + "name": "Beep LIN", + "avatar_url": "https://avatars3.githubusercontent.com/u/13058150?v=4", + "profile": "https://github.com/beeplin", + "contributions": [ + "code" + ] + }, + { + "login": "mattfwood", + "name": "Matt Wood", + "avatar_url": "https://avatars1.githubusercontent.com/u/22530815?v=4", + "profile": "https://mattwood.tech/", + "contributions": [ + "code" + ] + }, + { + "login": "jackbravo", + "name": "Joaquin Bravo Contreras", + "avatar_url": "https://avatars1.githubusercontent.com/u/15214?v=4", + "profile": "http://joaquin.axai.mx", + "contributions": [ + "code" + ] + }, + { + "login": "arjundubey-cr", + "name": "Arjun Dubey", + "avatar_url": "https://avatars0.githubusercontent.com/u/40758425?v=4", + "profile": "https://github.com/arjundubey-cr", + "contributions": [ + "code" + ] + }, + { + "login": "chanand", + "name": "chanand", + "avatar_url": "https://avatars0.githubusercontent.com/u/1317789?v=4", + "profile": "https://github.com/chanand", + "contributions": [ + "code" + ] + }, + { + "login": "phillipkregg", + "name": "phillipkregg", + "avatar_url": "https://avatars0.githubusercontent.com/u/1066044?v=4", + "profile": "https://github.com/phillipkregg", + "contributions": [ + "doc" + ] + }, + { + "login": "timReynolds", + "name": "Tim Reynolds", + "avatar_url": "https://avatars1.githubusercontent.com/u/168870?v=4", + "profile": "http://timothyreynolds.co.uk", + "contributions": [ + "doc" + ] + }, + { + "login": "linbudu599", + "name": "Linbudu", + "avatar_url": "https://avatars0.githubusercontent.com/u/48507806?v=4", + "profile": "https://linbudu.top/", + "contributions": [ + "doc" + ] + }, + { + "login": "creimers", + "name": "C Reimers", + "avatar_url": "https://avatars0.githubusercontent.com/u/6090492?v=4", + "profile": "http://www.superservice-international.com", + "contributions": [ + "doc" + ] + }, + { + "login": "kyken", + "name": "Tsuyoshi Osawa", + "avatar_url": "https://avatars2.githubusercontent.com/u/20137120?v=4", + "profile": "https://github.com/kyken", + "contributions": [ + "code" + ] + }, + { + "login": "rembrandtreyes", + "name": "Rembrandt Reyes", + "avatar_url": "https://avatars1.githubusercontent.com/u/15057964?v=4", + "profile": "https://rembrandtreyes.com/", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "doi-t", + "name": "Toshiya Doi", + "avatar_url": "https://avatars2.githubusercontent.com/u/5877477?v=4", + "profile": "https://doi-t.net", + "contributions": [ + "doc" + ] + }, + { + "login": "koolii", + "name": "t.kuriyama", + "avatar_url": "https://avatars1.githubusercontent.com/u/3866581?v=4", + "profile": "https://www.koolii.net/", + "contributions": [ + "code" + ] + }, + { + "login": "malkomalko", + "name": "Robert Malko", + "avatar_url": "https://avatars3.githubusercontent.com/u/763?v=4", + "profile": "https://github.com/malkomalko", + "contributions": [ + "code" + ] + }, + { + "login": "ranjan-purbey", + "name": "Ranjan Purbey", + "avatar_url": "https://avatars3.githubusercontent.com/u/6953187?v=4", + "profile": "https://github.com/ranjan-purbey", + "contributions": [ + "code" + ] + }, + { + "login": "tarunama", + "name": "tarunama", + "avatar_url": "https://avatars3.githubusercontent.com/u/6047881?v=4", + "profile": "https://github.com/tarunama", + "contributions": [ + "code" + ] + }, + { + "login": "bacongravy", + "name": "David Kramer", + "avatar_url": "https://avatars3.githubusercontent.com/u/16848768?v=4", + "profile": "http://www.bacongravy.net/", + "contributions": [ + "code" + ] + }, + { + "login": "mikeesto", + "name": "Michael Esteban", + "avatar_url": "https://avatars1.githubusercontent.com/u/21051488?v=4", + "profile": "https://mikeesto.com", + "contributions": [ + "doc" + ] + }, + { + "login": "marina-ki", + "name": "marina", + "avatar_url": "https://avatars0.githubusercontent.com/u/54174518?v=4", + "profile": "https://github.com/marina-ki", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "jonasthiesen", + "name": "Jonas Thiesen", + "avatar_url": "https://avatars.githubusercontent.com/u/23408018?v=4", + "profile": "https://github.com/jonasthiesen", + "contributions": [ + "doc" + ] + }, + { + "login": "thakkaryash94", + "name": "Yash Thakkar", + "avatar_url": "https://avatars.githubusercontent.com/u/7349778?v=4", + "profile": "https://thakkaryash94.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "rince", + "name": "Kazuma Suzuki", + "avatar_url": "https://avatars.githubusercontent.com/u/933895?v=4", + "profile": "https://github.com/rince", + "contributions": [ + "design", + "code" + ] + }, + { + "login": "queq1890", + "name": "Yuji Matsumoto", + "avatar_url": "https://avatars.githubusercontent.com/u/32263803?v=4", + "profile": "http://queq1890.info", + "contributions": [ + "doc" + ] + }, + { + "login": "Gim3l", + "name": "Gimel Dick", + "avatar_url": "https://avatars.githubusercontent.com/u/46765702?v=4", + "profile": "https://github.com/Gim3l", + "contributions": [ + "code" + ] + }, + { + "login": "akbo", + "name": "Andreas Bollig", + "avatar_url": "https://avatars.githubusercontent.com/u/1926271?v=4", + "profile": "https://github.com/akbo", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "ajmarkow", + "name": "AJ Markow", + "avatar_url": "https://avatars.githubusercontent.com/u/66390428?v=4", + "profile": "https://ajm.codes", + "contributions": [ + "test", + "code" + ] + }, + { + "login": "wafuwafu13", + "name": "TagawaHirotaka", + "avatar_url": "https://avatars.githubusercontent.com/u/50798936?v=4", + "profile": "https://wafuwafu13.hateblo.jp/", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "merodiro", + "name": "Amr A.Mohammed", + "avatar_url": "https://avatars.githubusercontent.com/u/17033502?v=4", + "profile": "https://github.com/merodiro", + "contributions": [ + "code" + ] + }, + { + "login": "lcswillems", + "name": "Lucas Willems", + "avatar_url": "https://avatars.githubusercontent.com/u/5437552?v=4", + "profile": "http://www.lucaswillems.com", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "alii", + "name": "Alistair Smith", + "avatar_url": "https://avatars.githubusercontent.com/u/25351731?v=4", + "profile": "https://alistair.cloud", + "contributions": [ + "code" + ] + }, + { + "login": "rodrigoehlers", + "name": "Rodrigo Ehlers", + "avatar_url": "https://avatars.githubusercontent.com/u/19683042?v=4", + "profile": "https://rodrigoehlers.com", + "contributions": [ + "code" + ] + }, + { + "login": "mtford90", + "name": "Michael Ford", + "avatar_url": "https://avatars.githubusercontent.com/u/1734057?v=4", + "profile": "https://www.builtopen.com/", + "contributions": [ + "code" + ] + }, + { + "login": "LBrian", + "name": "Brian Liu", + "avatar_url": "https://avatars.githubusercontent.com/u/3888780?v=4", + "profile": "https://brianypliu.com", + "contributions": [ + "code" + ] + }, + { + "login": "beerose", + "name": "Aleksandra Sikora", + "avatar_url": "https://avatars.githubusercontent.com/u/9019397?v=4", + "profile": "http://aleksandra.codes", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "JuanM04", + "name": "JuanM04", + "avatar_url": "https://avatars.githubusercontent.com/u/16712703?v=4", + "profile": "https://juanm04.com", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "arenddeboer", + "name": "Arend de Boer", + "avatar_url": "https://avatars.githubusercontent.com/u/7022204?v=4", + "profile": "https://github.com/arenddeboer", + "contributions": [ + "doc" + ] + }, + { + "login": "fmilani", + "name": "Felipe Milani", + "avatar_url": "https://avatars.githubusercontent.com/u/1580375?v=4", + "profile": "https://github.com/fmilani", + "contributions": [ + "doc" + ] + }, + { + "login": "jxe", + "name": "Joe Edelman", + "avatar_url": "https://avatars.githubusercontent.com/u/13018?v=4", + "profile": "http://nxhx.org", + "contributions": [ + "code" + ] + }, + { + "login": "garytube", + "name": "Gary", + "avatar_url": "https://avatars.githubusercontent.com/u/3823504?v=4", + "profile": "https://github.com/garytube", + "contributions": [ + "doc" + ] + }, + { + "login": "oliverloops", + "name": "Oliver Lopez ", + "avatar_url": "https://avatars.githubusercontent.com/u/33361399?v=4", + "profile": "http://oliverloops.com", + "contributions": [ + "doc" + ] + }, + { + "login": "DecadentIpsum", + "name": "Andreas Zaralis", + "avatar_url": "https://avatars.githubusercontent.com/u/32861532?v=4", + "profile": "https://decadentIpsum.me", + "contributions": [ + "doc" + ] + }, + { + "login": "davetorbeck", + "name": "David Torbeck", + "avatar_url": "https://avatars.githubusercontent.com/u/5829885?v=4", + "profile": "https://github.com/davetorbeck", + "contributions": [ + "doc" + ] + }, + { + "login": "gusgard", + "name": "Gustavo Gard", + "avatar_url": "https://avatars.githubusercontent.com/u/2577356?v=4", + "profile": "https://github.com/gusgard", + "contributions": [ + "doc" + ] + }, + { + "login": "Immortalin", + "name": "Immortalin", + "avatar_url": "https://avatars.githubusercontent.com/u/7126128?v=4", + "profile": "https://narrationbox.com", + "contributions": [ + "code" + ] + }, + { + "login": "cristianbgp", + "name": "Cristian Granda", + "avatar_url": "https://avatars.githubusercontent.com/u/8507974?v=4", + "profile": "https://cristianbgp.com", + "contributions": [ + "code" + ] + }, + { + "login": "deniseyu", + "name": "Denise Yu", + "avatar_url": "https://avatars.githubusercontent.com/u/8420094?v=4", + "profile": "https://deniseyu.io", + "contributions": [ + "code" + ] + }, + { + "login": "andreadellacorte", + "name": "Andrea Della Corte", + "avatar_url": "https://avatars.githubusercontent.com/u/295683?v=4", + "profile": "http://dellacorte.me", + "contributions": [ + "doc" + ] + }, + { + "login": "aditsachde", + "name": "Adit Sachde", + "avatar_url": "https://avatars.githubusercontent.com/u/23707194?v=4", + "profile": "http://aditsachde.com", + "contributions": [ + "doc" + ] + }, + { + "login": "hirenchauhan2", + "name": "Hiren Chauhan", + "avatar_url": "https://avatars.githubusercontent.com/u/8999668?v=4", + "profile": "https://github.com/hirenchauhan2", + "contributions": [ + "code" + ] + }, + { + "login": "remjx", + "name": "Mark Jackson", + "avatar_url": "https://avatars.githubusercontent.com/u/35121685?v=4", + "profile": "http://remjx.com/", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "lewisblackburn", + "name": "Lewis Blackburn", + "avatar_url": "https://avatars.githubusercontent.com/u/51877955?v=4", + "profile": "https://lewisb.cloud/", + "contributions": [ + "doc" + ] + }, + { + "login": "FDiskas", + "name": "Vytenis", + "avatar_url": "https://avatars.githubusercontent.com/u/468006?v=4", + "profile": "https://github.com/FDiskas", + "contributions": [ + "code" + ] + }, + { + "login": "matthieu994", + "name": "Matthieu", + "avatar_url": "https://avatars.githubusercontent.com/u/12969089?v=4", + "profile": "https://portfolio.matthieupetit.com", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "mitchazj", + "name": "Mitchell Johnson", + "avatar_url": "https://avatars.githubusercontent.com/u/15032956?v=4", + "profile": "https://github.com/mitchazj", + "contributions": [ + "code" + ] + }, + { + "login": "Roesh", + "name": "Roshan Manuel", + "avatar_url": "https://avatars.githubusercontent.com/u/31125563?v=4", + "profile": "https://roshan.page/", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "kevinlangleyjr", + "name": "Kevin Langley Jr.", + "avatar_url": "https://avatars.githubusercontent.com/u/877634?v=4", + "profile": "https://kevinlangleyjr.com", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "heavygabriel", + "name": "Gabriel Picard", + "avatar_url": "https://avatars.githubusercontent.com/u/51029779?v=4", + "profile": "https://projet-test-99df0.firebaseapp.com/", + "contributions": [ + "doc" + ] + }, + { + "login": "chenkie", + "name": "Ryan Chenkie", + "avatar_url": "https://avatars.githubusercontent.com/u/1847678?v=4", + "profile": "http://ryanchenkie.com/", + "contributions": [ + "doc" + ] + }, + { + "login": "sbappan", + "name": "Santhosh B. Appan", + "avatar_url": "https://avatars.githubusercontent.com/u/12586088?v=4", + "profile": "https://github.com/sbappan", + "contributions": [ + "doc" + ] + }, + { + "login": "james2406", + "name": "James Moran", + "avatar_url": "https://avatars.githubusercontent.com/u/10858584?v=4", + "profile": "http://stackoverflow.com/users/5207233/james-moran", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "bugzpodder", + "name": "Jack Zhao", + "avatar_url": "https://avatars.githubusercontent.com/u/14841421?v=4", + "profile": "http://fb.me/yz", + "contributions": [ + "code" + ] + }, + { + "login": "the-red", + "name": "Hisaki Akaza", + "avatar_url": "https://avatars.githubusercontent.com/u/4494300?v=4", + "profile": "https://github.com/the-red", + "contributions": [ + "doc" + ] + }, + { + "login": "Flavyoo", + "name": "Flavio", + "avatar_url": "https://avatars.githubusercontent.com/u/14948074?v=4", + "profile": "http://flavioander.com/", + "contributions": [ + "code" + ] + }, + { + "login": "pbteja1998", + "name": "Bhanu Teja Pachipulusu", + "avatar_url": "https://avatars.githubusercontent.com/u/17903466?v=4", + "profile": "https://bhanuteja.dev/", + "contributions": [ + "code" + ] + }, + { + "login": "pavestru", + "name": "Pavel Struhar", + "avatar_url": "https://avatars.githubusercontent.com/u/10186479?v=4", + "profile": "https://twitter.com/pavestru", + "contributions": [ + "code" + ] + }, + { + "login": "reo777", + "name": "Reo Ishiyama", + "avatar_url": "https://avatars.githubusercontent.com/u/42126368?v=4", + "profile": "https://in-thepink.com/", + "contributions": [ + "code" + ] + }, + { + "login": "tmcw", + "name": "Tom MacWright", + "avatar_url": "https://avatars.githubusercontent.com/u/32314?v=4", + "profile": "https://macwright.com/", + "contributions": [ + "doc" + ] + }, + { + "login": "franky47", + "name": "François Best", + "avatar_url": "https://avatars.githubusercontent.com/u/1174092?v=4", + "profile": "https://francoisbest.com", + "contributions": [ + "code" + ] + }, + { + "login": "FarazPatankar", + "name": "Faraz Patankar", + "avatar_url": "https://avatars.githubusercontent.com/u/10681116?v=4", + "profile": "https://github.com/FarazPatankar", + "contributions": [ + "doc" + ] + }, + { + "login": "ericvicenti", + "name": "Eric Vicenti", + "avatar_url": "https://avatars.githubusercontent.com/u/1483597?v=4", + "profile": "https://github.com/ericvicenti", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "amdolan", + "name": "Alex Dolan", + "avatar_url": "https://avatars.githubusercontent.com/u/2552275?v=4", + "profile": "https://github.com/amdolan", + "contributions": [ + "doc", + "code", + "test" + ] + }, + { + "login": "Maastrich", + "name": "Mathis Pinsault", + "avatar_url": "https://avatars.githubusercontent.com/u/58431775?v=4", + "profile": "https://github.com/Maastrich", + "contributions": [ + "doc" + ] + }, + { + "login": "gstranger", + "name": "gstranger", + "avatar_url": "https://avatars.githubusercontent.com/u/36181416?v=4", + "profile": "https://github.com/gstranger", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "markhughes", + "name": "Mark Hughes", + "avatar_url": "https://avatars.githubusercontent.com/u/1357323?v=4", + "profile": "http://twitter.com/_markeh", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "andrearizzello", + "name": "Andrea Rizzello", + "avatar_url": "https://avatars.githubusercontent.com/u/10348930?v=4", + "profile": "www.andrearizzello.work", + "contributions": [ + "doc" + ] + }, + { + "login": "jahredhope", + "name": "Jahred Hope", + "avatar_url": "https://avatars.githubusercontent.com/u/13903378?v=4", + "profile": "jahred.com.au", + "contributions": [ + "doc" + ] + }, + { + "login": "simonelnahas", + "name": "Simon El Nahas", + "avatar_url": "https://avatars.githubusercontent.com/u/29279201?v=4", + "profile": "simonelnahas.github.io/", + "contributions": [ + "doc" + ] + }, + { + "login": "Cristy94", + "name": "Buleandra Cristian", + "avatar_url": "https://avatars.githubusercontent.com/u/1384885?v=4", + "profile": "www.usertrack.net", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "peterpalau", + "name": "Pedro Enrique Palau Isaac", + "avatar_url": "https://avatars.githubusercontent.com/u/12257885?v=4", + "profile": "http://palauisaac.me/", + "contributions": [ + "code" + ] + }, + { + "login": "sean-brydon", + "name": "sean-brydon", + "avatar_url": "https://avatars.githubusercontent.com/u/55134778?v=4", + "profile": "www.seanbrydon.me", + "contributions": [ + "doc" + ] + }, + { + "login": "Dieman89", + "name": "Alessandro", + "avatar_url": "https://avatars.githubusercontent.com/u/28837891?v=4", + "profile": "buonerba.dev", + "contributions": [ + "doc" + ] + }, + { + "login": "laubonghaudoi", + "name": "laubonghaudoi", + "avatar_url": "https://avatars.githubusercontent.com/u/11172180?v=4", + "profile": "www.jyutping.org", + "contributions": [ + "doc" + ] + }, + { + "login": "TommasoBruno99", + "name": "Tommaso Bruno", + "avatar_url": "https://avatars.githubusercontent.com/u/61512591?v=4", + "profile": "https://github.com/TommasoBruno99", + "contributions": [ + "doc" + ] + }, + { + "login": "antonykamp", + "name": "Antony", + "avatar_url": "https://avatars.githubusercontent.com/u/45163503?v=4", + "profile": "antonykamp.de", + "contributions": [ + "doc" + ] + }, + { + "login": "frontsideair", + "name": "Fatih Altinok", + "avatar_url": "https://avatars.githubusercontent.com/u/868283?v=4", + "profile": "https://blog.6nok.org", + "contributions": [ + "doc" + ] + }, + { + "login": "Mokshit06", + "name": "Mokshit Jain", + "avatar_url": "https://avatars.githubusercontent.com/u/50412128?v=4", + "profile": "https://mokshitjain.co/", + "contributions": [ + "code" + ] + }, + { + "login": "mubaidr", + "name": "Muhammad Ubaid Raza", + "avatar_url": "https://avatars.githubusercontent.com/u/2222702?v=4", + "profile": "https://mubaidr.github.io", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "silicontwin", + "name": "Nick Warren", + "avatar_url": "https://avatars.githubusercontent.com/u/121665?v=4", + "profile": "https://github.com/silicontwin", + "contributions": [ + "code" + ] + }, + { + "login": "mlabate", + "name": "mlabate", + "avatar_url": "https://avatars.githubusercontent.com/u/17139676?v=4", + "profile": "https://github.com/mlabate", + "contributions": [ + "doc" + ] + }, + { + "login": "lumaxis", + "name": "Lukas Spieß", + "avatar_url": "https://avatars.githubusercontent.com/u/406937?v=4", + "profile": "https://github.com/lumaxis", + "contributions": [ + "doc" + ] + }, + { + "login": "dawnofmidnight", + "name": "DawnOfMidnight", + "avatar_url": "https://avatars.githubusercontent.com/u/78233879?v=4", + "profile": "https://dawnofmidnight.vercel.app", + "contributions": [ + "doc" + ] + }, + { + "login": "kirakik", + "name": "Kenza Iraki", + "avatar_url": "https://avatars.githubusercontent.com/u/17203119?v=4", + "profile": "https://www.linkedin.com/in/kenzairaki/", + "contributions": [ + "test", + "code" + ] + }, + { + "login": "agustif", + "name": "Agusti Fernandez", + "avatar_url": "https://avatars.githubusercontent.com/u/6601142?v=4", + "profile": "https://github.com/agustif", + "contributions": [ + "code" + ] + }, + { + "login": "Anjianto", + "name": "Anjianto", + "avatar_url": "https://avatars.githubusercontent.com/u/61521141?v=4", + "profile": "https://github.com/Anjianto", + "contributions": [ + "code" + ] + }, + { + "login": "adblanc", + "name": "Blanc Adrien", + "avatar_url": "https://avatars.githubusercontent.com/u/41756894?v=4", + "profile": "https://adrienblanc.com", + "contributions": [ + "code" + ] + }, + { + "login": "meepdeew", + "name": "meepdeew", + "avatar_url": "https://avatars.githubusercontent.com/u/43303008?v=4", + "profile": "https://github.com/meepdeew", + "contributions": [ + "doc" + ] + }, + { + "login": "Hardik3296", + "name": "Hardik Gaur", + "avatar_url": "https://avatars.githubusercontent.com/u/20360325?v=4", + "profile": "https://github.com/Hardik3296", + "contributions": [ + "doc" + ] + }, + { + "login": "acornellier", + "name": "acornellier", + "avatar_url": "https://avatars.githubusercontent.com/u/8725423?v=4", + "profile": "https://github.com/acornellier", + "contributions": [ + "code" + ] + }, + { + "login": "craigglennie", + "name": "craigglennie", + "avatar_url": "https://avatars.githubusercontent.com/u/149281?v=4", + "profile": "https://github.com/craigglennie", + "contributions": [ + "doc" + ] + }, + { + "login": "fernvilla", + "name": "Fernando Villasenor", + "avatar_url": "https://avatars.githubusercontent.com/u/5857808?v=4", + "profile": "http://www.fernvillasenor.com", + "contributions": [ + "code" + ] + }, + { + "login": "swiftgaruda", + "name": "swiftgaruda", + "avatar_url": "https://avatars.githubusercontent.com/u/16741392?v=4", + "profile": "https://github.com/swiftgaruda", + "contributions": [ + "doc" + ] + }, + { + "login": "Patil2099", + "name": "Pankaj Patil", + "avatar_url": "https://avatars.githubusercontent.com/u/35653876?v=4", + "profile": "https://pplife.home.blog", + "contributions": [ + "doc" + ] + }, + { + "login": "mabadir", + "name": "Mina Abadir", + "avatar_url": "https://avatars.githubusercontent.com/u/3389914?v=4", + "profile": "minaabadir.ca", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "frankiesardo", + "name": "Francesco Sardo", + "avatar_url": "https://avatars.githubusercontent.com/u/1476561?v=4", + "profile": "https://github.com/frankiesardo", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "enemycnt", + "name": "Nikolay", + "avatar_url": "https://avatars.githubusercontent.com/u/320313?v=4", + "profile": "https://github.com/enemycnt", + "contributions": [ + "doc" + ] + }, + { + "login": "Dipeshwagle", + "name": "Dipesh Wagle", + "avatar_url": "https://avatars.githubusercontent.com/u/4191022?v=4", + "profile": "https://dipeshwagle.com", + "contributions": [ + "code" + ] + }, + { + "login": "benbender", + "name": "Benjamin Bender", + "avatar_url": "https://avatars.githubusercontent.com/u/462455?v=4", + "profile": "https://codepoet.de", + "contributions": [ + "code" + ] + }, + { + "login": "nimashoghi", + "name": "Nima Shoghi", + "avatar_url": "https://avatars.githubusercontent.com/u/3728170?v=4", + "profile": "https://nima.sh", + "contributions": [ + "code" + ] + }, + { + "login": "chronark", + "name": "Andreas Thomas", + "avatar_url": "https://avatars.githubusercontent.com/u/18246773?v=4", + "profile": "https://github.com/chronark", + "contributions": [ + "doc" + ] + }, + { + "login": "guoqqqi", + "name": "guoqqqi", + "avatar_url": "https://avatars.githubusercontent.com/u/72343596?v=4", + "profile": "https://github.com/guoqqqi", + "contributions": [ + "doc" + ] + }, + { + "login": "timbooker", + "name": "Tim", + "avatar_url": "https://avatars.githubusercontent.com/u/612681?v=4", + "profile": "https://github.com/timbooker", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "ormarek", + "name": "Marek Orłowski", + "avatar_url": "https://avatars.githubusercontent.com/u/16357457?v=4", + "profile": "http://orlowski.me/", + "contributions": [ + "doc" + ] + }, + { + "login": "AntoineGuestin", + "name": "Antoine G", + "avatar_url": "https://avatars.githubusercontent.com/u/70888750?v=4", + "profile": "https://github.com/AntoineGuestin", + "contributions": [ + "code" + ] + }, + { + "login": "swinner2", + "name": "Sean Winner", + "avatar_url": "https://avatars.githubusercontent.com/u/6707308?v=4", + "profile": "https://github.com/swinner2", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "max-programming", + "name": "Max Programming", + "avatar_url": "https://avatars.githubusercontent.com/u/51731966?v=4", + "profile": "https://usman-s.me", + "contributions": [ + "code" + ] + }, + { + "login": "sebastianhoitz", + "name": "Sebastian Hoitz", + "avatar_url": "https://avatars.githubusercontent.com/u/353768?v=4", + "profile": "https://makemake.sh", + "contributions": [ + "test", + "code" + ] + }, + { + "login": "garnerp", + "name": "garnerp", + "avatar_url": "https://avatars.githubusercontent.com/u/737307?v=4", + "profile": "https://github.com/garnerp", + "contributions": [ + "doc" + ] + }, + { + "login": "kivi", + "name": "kivi", + "avatar_url": "https://avatars.githubusercontent.com/u/366163?v=4", + "profile": "https://github.com/kivi", + "contributions": [ + "code" + ] + }, + { + "login": "dangreaves", + "name": "Dan Greaves", + "avatar_url": "https://avatars.githubusercontent.com/u/1036142?v=4", + "profile": "http://dangreaves.com", + "contributions": [ + "code" + ] + }, + { + "login": "lksnmnn", + "name": "Lukas Neumann", + "avatar_url": "https://avatars.githubusercontent.com/u/4983285?v=4", + "profile": "lksnmnn.com", + "contributions": [ + "doc", + "code", + "test" + ] + }, + { + "login": "dbachrach", + "name": "Dustin Bachrach", + "avatar_url": "https://avatars.githubusercontent.com/u/45016?v=4", + "profile": "dbachrach.com", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "ashikka", + "name": "Ashikka Gupta", + "avatar_url": "https://avatars.githubusercontent.com/u/58368421?v=4", + "profile": "https://github.com/ashikka", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "deini", + "name": "Daniel Almaguer", + "avatar_url": "https://avatars.githubusercontent.com/u/2752665?v=4", + "profile": "https://github.com/deini", + "contributions": [ + "doc" + ] + }, + { + "login": "igeligel", + "name": "Kevin Peters", + "avatar_url": "https://avatars.githubusercontent.com/u/12736734?v=4", + "profile": "https://www.kevinpeters.net/about/", + "contributions": [ + "doc" + ] + }, + { + "login": "prisis", + "name": "Daniel Bannert", + "avatar_url": "https://avatars.githubusercontent.com/u/2716058?v=4", + "profile": "http://anolilab.de", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "benjakugler96", + "name": "Benja Kugler", + "avatar_url": "https://avatars.githubusercontent.com/u/53273645?v=4", + "profile": "https://benjakugler96.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "esemeniuc", + "name": "Eric Semeniuc", + "avatar_url": "https://avatars.githubusercontent.com/u/3838856?v=4", + "profile": "https://semeniuc.ml/", + "contributions": [ + "test", + "code" + ] + }, + { + "login": "ricardo-rp", + "name": "Ricardo Romero", + "avatar_url": "https://avatars.githubusercontent.com/u/30808767?v=4", + "profile": "https://github.com/ricardo-rp", + "contributions": [ + "doc" + ] + }, + { + "login": "anothernode", + "name": "Moritz Reiter", + "avatar_url": "https://avatars.githubusercontent.com/u/3286144?v=4", + "profile": "exocortex.anothernode.com", + "contributions": [ + "doc" + ] + }, + { + "login": "msichterman", + "name": "Matt Sichterman", + "avatar_url": "https://avatars.githubusercontent.com/u/38794918?v=4", + "profile": "https://msich.dev", + "contributions": [ + "doc" + ] + }, + { + "login": "medihack", + "name": "Kai Schlamp", + "avatar_url": "https://avatars.githubusercontent.com/u/120626?v=4", + "profile": "https://github.com/medihack", + "contributions": [ + "doc" + ] + }, + { + "login": "muyiwaolu", + "name": "Muyiwa Olu", + "avatar_url": "https://avatars.githubusercontent.com/u/6832244?v=4", + "profile": "https://muyiwa.me", + "contributions": [ + "code" + ] + }, + { + "login": "rabbihossain", + "name": "Rabbi Hossain", + "avatar_url": "https://avatars.githubusercontent.com/u/4346154?v=4", + "profile": "http://2hr.me/", + "contributions": [ + "doc" + ] + }, + { + "login": "bravo-kernel", + "name": "bravo-kernel", + "avatar_url": "https://avatars.githubusercontent.com/u/230500?v=4", + "profile": "https://github.com/bravo-kernel", + "contributions": [ + "code" + ] + }, + { + "login": "sam3d", + "name": "Sam Holmes", + "avatar_url": "https://avatars.githubusercontent.com/u/8385528?v=4", + "profile": "https://samholmes.net", + "contributions": [ + "code" + ] + }, + { + "login": "doncicuto", + "name": "Miguel Cabrerizo", + "avatar_url": "https://avatars.githubusercontent.com/u/30386061?v=4", + "profile": "https://doncicuto.medium.com", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "zenhob", + "name": "Zack Hobson", + "avatar_url": "https://avatars.githubusercontent.com/u/12092?v=4", + "profile": "http://zackhobson.com/", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "m5r", + "name": "Mokhtar", + "avatar_url": "https://avatars.githubusercontent.com/u/13026820?v=4", + "profile": "https://www.mokhtar.dev", + "contributions": [ + "doc" + ] + }, + { + "login": "kenkuan", + "name": "Ken Kuan", + "avatar_url": "https://avatars.githubusercontent.com/u/1924968?v=4", + "profile": "https://github.com/kenkuan", + "contributions": [ + "code" + ] + }, + { + "login": "meehawk", + "name": "meehawk", + "avatar_url": "https://avatars.githubusercontent.com/u/80167324?v=4", + "profile": "https://github.com/meehawk", + "contributions": [ + "code" + ] + }, + { + "login": "ravindranrahul", + "name": "Rahul Ravindran", + "avatar_url": "https://avatars.githubusercontent.com/u/10168946?v=4", + "profile": "rahulravindran.in", + "contributions": [ + "code" + ] + }, + { + "login": "s-r-x", + "name": "Ilya", + "avatar_url": "https://avatars.githubusercontent.com/u/41614937?v=4", + "profile": "https://github.com/s-r-x", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "hashimwarren", + "name": "Hashim Warren", + "avatar_url": "https://avatars.githubusercontent.com/u/6027587?v=4", + "profile": "https://github.com/hashimwarren", + "contributions": [ + "doc" + ] + }, + { + "login": "damilolarandolph", + "name": "Damilola Randolph", + "avatar_url": "https://avatars.githubusercontent.com/u/43427949?v=4", + "profile": "https://damilolarandolph.com", + "contributions": [ + "doc" + ] + }, + { + "login": "mwcampbell", + "name": "Matt Campbell", + "avatar_url": "https://avatars.githubusercontent.com/u/214820?v=4", + "profile": "https://github.com/mwcampbell", + "contributions": [ + "doc" + ] + }, + { + "login": "ratson", + "name": "(◕ᴥ◕)", + "avatar_url": "https://avatars.githubusercontent.com/u/2682937?v=4", + "profile": "https://github.com/ratson", + "contributions": [ + "code" + ] + }, + { + "login": "maciejmyslinski", + "name": "Mat Milbury", + "avatar_url": "https://avatars.githubusercontent.com/u/11421186?v=4", + "profile": "maciejmyslinski.com", + "contributions": [ + "doc" + ] + }, + { + "login": "andreasasprou", + "name": "Andreas Asprou", + "avatar_url": "https://avatars.githubusercontent.com/u/8077469?v=4", + "profile": "https://andreas.fyi", + "contributions": [ + "code", + "test" + ] + }, + { + "login": "kotx", + "name": "Kot", + "avatar_url": "https://avatars.githubusercontent.com/u/33439542?v=4", + "profile": "https://github.com/kotx", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "isaka1022", + "name": "Amane", + "avatar_url": "https://avatars.githubusercontent.com/u/28589716?v=4", + "profile": "https://github.com/isaka1022", + "contributions": [ + "doc" + ] + }, + { + "login": "fuzzthink", + "name": "John Leung", + "avatar_url": "https://avatars.githubusercontent.com/u/20699?v=4", + "profile": "johnleung.com", + "contributions": [ + "doc" + ] + }, + { + "login": "bcye", + "name": "Bruce", + "avatar_url": "https://avatars.githubusercontent.com/u/29666239?v=4", + "profile": "roettgers.co", + "contributions": [ + "code" + ] + }, + { + "login": "emilygracekz", + "name": "Emily", + "avatar_url": "https://avatars.githubusercontent.com/u/57361805?v=4", + "profile": "https://github.com/emilygracekz", + "contributions": [ + "code" + ] + }, + { + "login": "npverni", + "name": "Nathan Verni", + "avatar_url": "https://avatars.githubusercontent.com/u/3537?v=4", + "profile": "https://github.com/npverni", + "contributions": [ + "doc" + ] + }, + { + "login": "davyengone", + "name": "Davy Engone", + "avatar_url": "https://avatars.githubusercontent.com/u/4896002?v=4", + "profile": "https://davyengone.io", + "contributions": [ + "doc" + ] + }, + { + "login": "Fedeorlandau", + "name": "Federico Joel Orlandau", + "avatar_url": "https://avatars.githubusercontent.com/u/10283686?v=4", + "profile": "https://fedeorlandau.dev/", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "johnmurphy01", + "name": "John Murphy", + "avatar_url": "https://avatars.githubusercontent.com/u/2939548?v=4", + "profile": "https://github.com/johnmurphy01", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "martinsaxa", + "name": "martinsaxa", + "avatar_url": "https://avatars.githubusercontent.com/u/33789474?v=4", + "profile": "https://github.com/martinsaxa", + "contributions": [ + "code" + ] + }, + { + "login": "ajwgeek", + "name": "Austin Walhof", + "avatar_url": "https://avatars.githubusercontent.com/u/2135600?v=4", + "profile": "https://github.com/ajwgeek", + "contributions": [ + "doc" + ] + }, + { + "login": "g3offrey", + "name": "Geoffrey", + "avatar_url": "https://avatars.githubusercontent.com/u/11151445?v=4", + "profile": "g3offrey.dev", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "keevan", + "name": "Kevin Pham", + "avatar_url": "https://avatars.githubusercontent.com/u/9924643?v=4", + "profile": "https://github.com/keevan", + "contributions": [ + "doc" + ] + }, + { + "login": "kimngan-bui", + "name": "kimngan-bui", + "avatar_url": "https://avatars.githubusercontent.com/u/20723478?v=4", + "profile": "https://github.com/kimngan-bui", + "contributions": [ + "doc" + ] + }, + { + "login": "9j", + "name": "Bahk Chanhee", + "avatar_url": "https://avatars.githubusercontent.com/u/11691670?v=4", + "profile": "world.hey.com/bach", + "contributions": [ + "code" + ] + }, + { + "login": "Vandivier", + "name": "John Vandivier", + "avatar_url": "https://avatars.githubusercontent.com/u/5559355?v=4", + "profile": "http://www.afterecon.com/", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "namirsab", + "name": "Namir", + "avatar_url": "https://avatars.githubusercontent.com/u/6980777?v=4", + "profile": "http://namirsab.github.io", + "contributions": [ + "doc", + "code", + "test" + ] + }, + { + "login": "scttcper", + "name": "Scott Cooper", + "avatar_url": "https://avatars.githubusercontent.com/u/1400464?v=4", + "profile": "https://twitter.com/scttcper", + "contributions": [ + "doc" + ] + }, + { + "login": "Abduttayyeb", + "name": "Abduttayyeb M.r", + "avatar_url": "https://avatars.githubusercontent.com/u/55306260?v=4", + "profile": "abduttayyeb.github.io", + "contributions": [ + "doc" + ] + }, + { + "login": "maybebored", + "name": "Mayuran", + "avatar_url": "https://avatars.githubusercontent.com/u/20951181?v=4", + "profile": "https://github.com/maybebored", + "contributions": [ + "code" + ] + }, + { + "login": "MuckHub", + "name": "Aleksei Vesselko", + "avatar_url": "https://avatars.githubusercontent.com/u/54979136?v=4", + "profile": "https://github.com/MuckHub", + "contributions": [ + "doc" + ] + }, + { + "login": "p-siriphanthong", + "name": "Punn Siriphanthong", + "avatar_url": "https://avatars.githubusercontent.com/u/29949429?v=4", + "profile": "https://p-siriphanthong.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "shawn-fetanat", + "name": "Shawn Fetanat", + "avatar_url": "https://avatars.githubusercontent.com/u/83679827?v=4", + "profile": "https://my-portfolio-292eb.web.app", + "contributions": [ + "doc" + ] + }, + { + "login": "mochi-sann", + "name": "Moyuru", + "avatar_url": "https://avatars.githubusercontent.com/u/44772513?v=4", + "profile": "https://github.com/mochi-sann", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "camsloanftc", + "name": "Cam Sloan", + "avatar_url": "https://avatars.githubusercontent.com/u/16295659?v=4", + "profile": "https://github.com/camsloanftc", + "contributions": [ + "doc" + ] + }, + { + "login": "sitek94", + "name": "Maciek Sitkowski", + "avatar_url": "https://avatars.githubusercontent.com/u/58401630?v=4", + "profile": "https://macieksitkowski.com", + "contributions": [ + "doc" + ] + }, + { + "login": "vivek7405", + "name": "Vivek", + "avatar_url": "https://avatars.githubusercontent.com/u/24492244?v=4", + "profile": "https://github.com/vivek7405", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "cj", + "name": "CJ Lazell", + "avatar_url": "https://avatars.githubusercontent.com/u/1819?v=4", + "profile": "http://cj.io", + "contributions": [ + "code" + ] + }, + { + "login": "RobertBroersma", + "name": "Robert", + "avatar_url": "https://avatars.githubusercontent.com/u/4519828?v=4", + "profile": "robertbroersma.com", + "contributions": [ + "doc" + ] + }, + { + "login": "cbejensen", + "name": "Christian Jensen", + "avatar_url": "https://avatars.githubusercontent.com/u/12374723?v=4", + "profile": "https://christianjensen.netlify.com", + "contributions": [ + "doc" + ] + }, + { + "login": "dvnrsn", + "name": "Devin Rasmussen", + "avatar_url": "https://avatars.githubusercontent.com/u/9112811?v=4", + "profile": "https://github.com/dvnrsn", + "contributions": [ + "code" + ] + }, + { + "login": "devtombiz", + "name": "Thomas Brenneur", + "avatar_url": "https://avatars.githubusercontent.com/u/23282613?v=4", + "profile": "www.linkedin.com/in/devtom", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "lucasvazq", + "name": "Lucas Vazquez", + "avatar_url": "https://avatars.githubusercontent.com/u/38964964?v=4", + "profile": "https://lucasvazq.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "chrisj-back2work", + "name": "Chris Johnson", + "avatar_url": "https://avatars.githubusercontent.com/u/68551954?v=4", + "profile": "https://github.com/chrisj-back2work", + "contributions": [ + "doc" + ] + }, + { + "login": "thisdotrob", + "name": "Rob Stevenson", + "avatar_url": "https://avatars.githubusercontent.com/u/12902589?v=4", + "profile": "https://github.com/thisdotrob", + "contributions": [ + "doc" + ] + }, + { + "login": "lovethebomb", + "name": "Lucas Heymès", + "avatar_url": "https://avatars.githubusercontent.com/u/1363056?v=4", + "profile": "www.lucas.computer", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "NorfeldtAbtion", + "name": "Lasse Norfeldt", + "avatar_url": "https://avatars.githubusercontent.com/u/53769763?v=4", + "profile": "https://github.com/NorfeldtAbtion", + "contributions": [ + "doc" + ] + }, + { + "login": "netwarex", + "name": "Péter Nyári", + "avatar_url": "https://avatars.githubusercontent.com/u/6048614?v=4", + "profile": "https://nyaripeter.hu/", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "5minpause", + "name": "Holger Frohloff", + "avatar_url": "https://avatars.githubusercontent.com/u/84148?v=4", + "profile": "https://www.holgerfrohloff.de", + "contributions": [ + "doc" + ] + }, + { + "login": "basilk76", + "name": "Basil Khan", + "avatar_url": "https://avatars.githubusercontent.com/u/45275512?v=4", + "profile": "https://github.com/basilk76", + "contributions": [ + "doc" + ] + }, + { + "login": "danestves", + "name": "Daniel Esteves", + "avatar_url": "https://avatars.githubusercontent.com/u/31737273?v=4", + "profile": "https://danestves.com/", + "contributions": [ + "doc" + ] + }, + { + "login": "coryhouse", + "name": "Cory House", + "avatar_url": "https://avatars.githubusercontent.com/u/1688997?v=4", + "profile": "http://www.bitnative.com", + "contributions": [ + "doc" + ] + }, + { + "login": "rockmanvnx6", + "name": "Austin (Thang Pham)", + "avatar_url": "https://avatars.githubusercontent.com/u/16440123?v=4", + "profile": "https://auspham.dev/", + "contributions": [ + "doc" + ] + }, + { + "login": "noxify", + "name": "Marcus Reinhardt", + "avatar_url": "https://avatars.githubusercontent.com/u/521777?v=4", + "profile": "https://jammeryhq.com", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "davidchristie", + "name": "David Christie", + "avatar_url": "https://avatars.githubusercontent.com/u/12044333?v=4", + "profile": "https://github.com/davidchristie", + "contributions": [ + "doc" + ] + }, + { + "login": "ajanth97", + "name": "Ajanth", + "avatar_url": "https://avatars.githubusercontent.com/u/50458502?v=4", + "profile": "https://github.com/ajanth97", + "contributions": [ + "doc" + ] + }, + { + "login": "divpreet", + "name": "Div", + "avatar_url": "https://avatars.githubusercontent.com/u/2805650?v=4", + "profile": "https://github.com/divpreet", + "contributions": [ + "doc" + ] + }, + { + "login": "david-arteaga", + "name": "David Arteaga", + "avatar_url": "https://avatars.githubusercontent.com/u/7199015?v=4", + "profile": "https://github.com/david-arteaga", + "contributions": [ + "doc" + ] + }, + { + "login": "MukulKolpe", + "name": "Mukul Kolpe", + "avatar_url": "https://avatars.githubusercontent.com/u/78664749?v=4", + "profile": "https://github.com/MukulKolpe", + "contributions": [ + "code" + ] + }, + { + "login": "skotchpine", + "name": "tyler", + "avatar_url": "https://avatars.githubusercontent.com/u/13043909?v=4", + "profile": "https://github.com/skotchpine", + "contributions": [ + "code" + ] + }, + { + "login": "SofianeDjellouli", + "name": "Sofiane Djellouli", + "avatar_url": "https://avatars.githubusercontent.com/u/38258952?v=4", + "profile": "https://github.com/SofianeDjellouli", + "contributions": [ + "doc" + ] + }, + { + "login": "kreako", + "name": "kreako", + "avatar_url": "https://avatars.githubusercontent.com/u/65113001?v=4", + "profile": "https://github.com/kreako", + "contributions": [ + "doc" + ] + }, + { + "login": "sarahdayan", + "name": "Sarah Dayan", + "avatar_url": "https://avatars.githubusercontent.com/u/5370675?v=4", + "profile": "https://sarahdayan.dev", + "contributions": [ + "code" + ] + }, + { + "login": "c-ciobanu", + "name": "Cristi Ciobanu", + "avatar_url": "https://avatars.githubusercontent.com/u/33382714?v=4", + "profile": "https://github.com/c-ciobanu", + "contributions": [ + "doc" + ] + }, + { + "login": "arpitdalal", + "name": "Arpit Dalal", + "avatar_url": "https://avatars.githubusercontent.com/u/61059807?v=4", + "profile": "https://arpitdalal.dev", + "contributions": [ + "doc" + ] + }, + { + "login": "robertrisch", + "name": "robertrisch", + "avatar_url": "https://avatars.githubusercontent.com/u/73828816?v=4", + "profile": "https://github.com/robertrisch", + "contributions": [ + "doc" + ] + }, + { + "login": "dineshgadge", + "name": "Dinesh Gadge", + "avatar_url": "https://avatars.githubusercontent.com/u/186976?v=4", + "profile": "https://github.com/dineshgadge", + "contributions": [ + "code" + ] + }, + { + "login": "maltekiessling", + "name": "Malte Kießling", + "avatar_url": "https://avatars.githubusercontent.com/u/30420110?v=4", + "profile": "https://github.com/maltekiessling", + "contributions": [ + "doc" + ] + }, + { + "login": "ospfranco", + "name": "Oscar Franco", + "avatar_url": "https://avatars.githubusercontent.com/u/1634213?v=4", + "profile": "ospfranco.com", + "contributions": [ + "doc" + ] + }, + { + "login": "Nfinished", + "name": "Adam Trager", + "avatar_url": "https://avatars.githubusercontent.com/u/1719791?v=4", + "profile": "adamtrager.com", + "contributions": [ + "code" + ] + }, + { + "login": "shellord", + "name": "saheenshoukath", + "avatar_url": "https://avatars.githubusercontent.com/u/2632896?v=4", + "profile": "https://saheen.codes", + "contributions": [ + "doc" + ] + }, + { + "login": "husnuljahneer", + "name": "Husnul Jahneer", + "avatar_url": "https://avatars.githubusercontent.com/u/54552763?v=4", + "profile": "https://jahneer.me", + "contributions": [ + "doc" + ] + }, + { + "login": "ReykCS", + "name": "Reyk", + "avatar_url": "https://avatars.githubusercontent.com/u/40463716?v=4", + "profile": "https://github.com/ReykCS", + "contributions": [ + "doc" + ] + }, + { + "login": "Lokprakash-babu", + "name": "Lokprakash Babu", + "avatar_url": "https://avatars.githubusercontent.com/u/60031382?v=4", + "profile": "https://github.com/Lokprakash-babu", + "contributions": [ + "doc" + ] + }, + { + "login": "eai04191", + "name": "eai04191", + "avatar_url": "https://avatars.githubusercontent.com/u/3516343?v=4", + "profile": "https://mizle.net", + "contributions": [ + "doc" + ] + }, + { + "login": "numanaral", + "name": "Numan", + "avatar_url": "https://avatars.githubusercontent.com/u/25233323?v=4", + "profile": "https://numanaral.github.io/?ref=github", + "contributions": [ + "doc" + ] + }, + { + "login": "jscyo", + "name": "Joel Coutinho", + "avatar_url": "https://avatars.githubusercontent.com/u/6310783?v=4", + "profile": "https://github.com/jscyo", + "contributions": [ + "doc" + ] + }, + { + "login": "davidbarker", + "name": "David Barker", + "avatar_url": "https://avatars.githubusercontent.com/u/1597139?v=4", + "profile": "https://github.com/davidbarker", + "contributions": [ + "doc" + ] + }, + { + "login": "timfee", + "name": "Tim Feeley", + "avatar_url": "https://avatars.githubusercontent.com/u/3246342?v=4", + "profile": "http://www.timfeeley.com/", + "contributions": [ + "code" + ] + }, + { + "login": "Caslus", + "name": "lucas philippe", + "avatar_url": "https://avatars.githubusercontent.com/u/22855640?v=4", + "profile": "https://github.com/Caslus", + "contributions": [ + "doc" + ] + }, + { + "login": "the-bayer", + "name": "Blake Bayer", + "avatar_url": "https://avatars.githubusercontent.com/u/94391693?v=4", + "profile": "https://github.com/the-bayer", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "rmassie", + "name": "R Massie", + "avatar_url": "https://avatars.githubusercontent.com/u/7375518?v=4", + "profile": "https://github.com/rmassie", + "contributions": [ + "doc" + ] + }, + { + "login": "paulm17", + "name": "Paul", + "avatar_url": "https://avatars.githubusercontent.com/u/387463?v=4", + "profile": "https://github.com/paulm17", + "contributions": [ + "doc" + ] + }, + { + "login": "minho42", + "name": "Min ho Kim", + "avatar_url": "https://avatars.githubusercontent.com/u/15278512?v=4", + "profile": "https://minho42.com", + "contributions": [ + "doc" + ] + }, + { + "login": "webdeb", + "name": "webdeb", + "avatar_url": "https://avatars.githubusercontent.com/u/14992140?v=4", + "profile": "https://github.com/webdeb", + "contributions": [ + "doc" + ] + }, + { + "login": "iDavidB", + "name": "David", + "avatar_url": "https://avatars.githubusercontent.com/u/32268383?v=4", + "profile": "https://github.com/iDavidB", + "contributions": [ + "doc", + "code", + "test" + ] + }, + { + "login": "jakedee", + "name": "Jake Dowie", + "avatar_url": "https://avatars.githubusercontent.com/u/5058625?v=4", + "profile": "https://jdlt.co.uk", + "contributions": [ + "doc" + ] + }, + { + "login": "datner", + "name": "Datner", + "avatar_url": "https://avatars.githubusercontent.com/u/22598347?v=4", + "profile": "https://github.com/datner", + "contributions": [ + "doc", + "code", + "test" + ] + }, + { + "login": "remlse", + "name": "remlse", + "avatar_url": "https://avatars.githubusercontent.com/u/54984957?v=4", + "profile": "https://github.com/remlse", + "contributions": [ + "doc" + ] + }, + { + "login": "sergous", + "name": "Sergei Smirnov", + "avatar_url": "https://avatars.githubusercontent.com/u/545151?v=4", + "profile": "https://github.com/sergous", + "contributions": [ + "doc" + ] + }, + { + "login": "Trancever", + "name": "Dawid Urbaniak", + "avatar_url": "https://avatars.githubusercontent.com/u/18584155?v=4", + "profile": "https://twitter.com/trensik", + "contributions": [ + "doc", + "code" + ] + }, + { + "login": "SerekKiri", + "name": "Kacper Potyrała", + "avatar_url": "https://avatars.githubusercontent.com/u/29735836?v=4", + "profile": "kiri.dev", + "contributions": [ + "doc" + ] + }, + { + "login": "iojcde", + "name": "Jeeho Ahn", + "avatar_url": "https://avatars.githubusercontent.com/u/31413538?v=4", + "profile": "jcde.xyz", + "contributions": [ + "doc", + "tool" + ] + } + ], + "contributorsPerLine": 7, + "skipCi": true +} \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..15fe7ff189 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,55 @@ +node_modules +reports +*.log +.nyc_output +**/coverage +tsconfig.tsbuildinfo +**/.blitz/** +**/.next/** +**/dist/** +**/.vercel/** +**/.test* +/examples/auth2 + +prettier.config.* +jest.config.* +jest.setup.* +babel.config.* +eslint.config.* + +/__mocks__ +/__fixturse +/assets +/patches +/rfc-docs +/scripts +/types +/recipes/*/templates +/packages/generator/templates +/packages/cli/lib +/packages/babel-preset/src/fix-node-file-trace/tests/** +/test/integration/**/out/** +/nextjs/packages/create-next-app + + +// COPIED FROM nextjs/.eslintignore +/nextjs/**/.next/** +/nextjs/**/_next/** +/nextjs/**/dist/** +/nextjs/examples/** +/nextjs/packages/next/bundles/webpack/packages/*.runtime.js +/nextjs/packages/next/compiled/**/* +/nextjs/packages/react-refresh-utils/**/*.js +/nextjs/packages/react-dev-overlay/lib/** +/nextjs/**/__tmp__/** +/nextjs/.github/actions/next-stats-action/.work +/nextjs/packages/next-codemod/transforms/__testfixtures__/**/* +/nextjs/packages/next-codemod/transforms/__tests__/**/* +/nextjs/packages/next-codemod/**/*.js +/nextjs/packages/next-codemod/**/*.d.ts +/nextjs/packages/next-env/**/*.d.ts +/nextjs/packages/next/build/swc/tests/fixture/** +/nextjs/test/integration/async-modules/** +/nextjs/test/integration/eslint/** +/nextjs/test/integration/babel/** +/nextjs/test-timings.json diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..4f2f62565a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,119 @@ +module.exports = { + parser: "@babel/eslint-parser", + env: { + browser: true, + commonjs: true, + es6: true, + node: true, + }, + parserOptions: { + ecmaVersion: 6, + requireConfigFile: false, + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + babelOptions: { + presets: ["@babel/preset-env", "@babel/preset-react"], + caller: { + // Eslint supports top level await when a parser for it is included. We enable the parser by default for Babel. + supportsTopLevelAwait: true, + }, + }, + }, + plugins: ["import", "unicorn", "simple-import-sort"], + extends: ["react-app"], + rules: { + "react/react-in-jsx-scope": "off", // React is always in scope with Blitz + "jsx-a11y/anchor-is-valid": "off", //Doesn't play well with Blitz/Next usage + "import/first": "off", + "import/no-default-export": "error", + "require-await": "error", + "no-async-promise-executor": "error", + "unicorn/filename-case": [ + "error", + { + case: "kebabCase", + }, + ], + "simple-import-sort/imports": [ + "warn", + { + groups: [ + [ + // Side effect imports. + "^\\u0000", + // Packages. + // Things that start with a letter (or digit or underscore), or `@` followed by a letter. + "^@?\\w", + // Absolute imports and other imports such as Vue-style `@/foo`. + // Anything that does not start with a dot. + "^[^.]", + // Relative imports. + // Anything that starts with a dot. + "^\\.", + ], + ], + }, + ], + }, + overrides: [ + { + files: ["**/*.ts", "**/*.tsx"], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 6, + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + project: `./tsconfig.eslint.json`, + }, + plugins: ["@typescript-eslint"], + rules: { + "@typescript-eslint/no-floating-promises": "error", + // note you must disable the base rule as it can report incorrect errors + "no-use-before-define": "off", + // "@typescript-eslint/no-use-before-define": ["error"], + // note you must disable the base rule as it can report incorrect errors + "no-redeclare": "off", + "@typescript-eslint/no-redeclare": ["error"], + }, + }, + { + files: ["examples/**", "recipes/**"], + rules: { + "import/no-default-export": "off", + "unicorn/filename-case": "off", + "@typescript-eslint/no-floating-promises": "off", + }, + }, + { + files: ["examples/**"], + plugins: ["cypress"], + parserOptions: { + project: null, + }, + env: { + "cypress/globals": true, + }, + rules: { + "simple-import-sort/imports": "off", + }, + }, + { + files: ["packages/cli/src/commands/**/*"], + rules: { + "require-await": "off", + }, + }, + { + files: ["test/**", "**/__fixtures__/**"], + rules: { + "import/no-default-export": "off", + "require-await": "off", + "unicorn/filename-case": "off", + }, + }, + ], +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..6313b56c57 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..64c6f4108d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +* @beerose + +# packages/cli/**/* @aem, @flybayer +# packages/generator/**/* @aem @flybayer +# packages/generator/templates**/* @flybayer +# packages/installer/**/* @aem @flybayer diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..7ae6d91fc1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +github: blitz-js +custom: ["https://paypal.me/thebayers"] +open_collective: blitzjs +patreon: flybayer diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..dafe091b57 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,48 @@ +name: Bug Report +description: Something is not working right. Or error messages are unclear. +labels: "kind/bug, status/triage" +body: + - type: markdown + attributes: + value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible. + - type: textarea + attributes: + label: What is the problem? + validations: + required: true + - type: textarea + attributes: + label: "Paste all your error logs here:" + value: | + ``` + PASTE_HERE (leave the ``` marks) + ``` + validations: + required: true + - type: textarea + attributes: + label: "Paste all relevant code snippets here:" + value: | + ``` + PASTE_HERE (leave the ``` marks) + ``` + validations: + required: true + - type: textarea + attributes: + label: What are detailed steps to reproduce this? + value: "1." + validations: + required: true + - type: textarea + attributes: + label: "Run `blitz -v` and paste the output here:" + value: | + ``` + PASTE_HERE (leave the ``` marks) + ``` + validations: + required: true + - type: textarea + attributes: + label: "Please include below any other applicable logs and screenshots that show your problem:" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..88a895bda7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Prisma issue? + url: https://github.com/prisma/prisma/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc + about: All Prisma issues should be opened in the Prisma Github + - name: Question, Discussion, Idea? + url: https://github.com/blitz-js/blitz/discussions/new + about: Ask questions and discuss with other community members diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..cf80e6e789 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature/change request +about: Something new or better! +title: "" +labels: "status/triage" +assignees: "" +--- + +### What do you want and why? + +The more information the better! + +### Possible implementation(s) + +How might we do this? + +### Additional context + +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..4ade36cc5b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ + + +Closes: ? + +### What are the changes and their implications? + +## Bug Checklist + +- [ ] Integration test added (see [test docs](https://blitzjs.com/docs/contributing#running-tests) if needed) + +## Feature Checklist + +- [ ] Integration test added (see [test docs](https://blitzjs.com/docs/contributing#running-tests) if needed) +- [ ] Documentation added/updated (submit PR to [blitzjs.com repo `canary` branch](https://github.com/blitz-js/blitzjs.com/tree/canary)) diff --git a/.github/checkInstallTime.js b/.github/checkInstallTime.js new file mode 100755 index 0000000000..9a811ed53b --- /dev/null +++ b/.github/checkInstallTime.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +const fs = require("fs") +const yarnOut = fs.readFileSync(0, {encoding: "utf8"}) + +const [installTimeString] = /(?<=^Done in )\d+\.\d+(?=s\.$)/m.exec(yarnOut) +const installTime = Number(installTimeString) + +console.log(`Install time: ${installTime}s`) + +if (installTime < 30) { + console.log("We're below 30 secs. That's awesome!") +} else if (installTime < 50) { + console.log("We're below 50 secs. That's fine!") +} else { + console.log("We're above 50 secs. That's not great!") + process.exit(1) +} diff --git a/.github/workflows/compressed.yml b/.github/workflows/compressed.yml new file mode 100644 index 0000000000..18a78bafb9 --- /dev/null +++ b/.github/workflows/compressed.yml @@ -0,0 +1,26 @@ +name: Size Check + +on: + pull_request: + branches: [master, canary] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Compressed Size + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + - name: Count size + uses: preactjs/compressed-size-action@v2 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + exclude: "{./nextjs/**}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..9a32638544 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,477 @@ +# https://github.com/vercel/next.js/commits/canary/.github/workflows/build_test_deploy.yml + +name: CI + +on: + pull_request: + types: [opened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Cache node_modules + id: yarn-cache + uses: actions/cache@v2 + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + **/node_modules + key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.node_version}}-yarn-v14- + - name: Install dependencies + run: yarn install --frozen-lockfile --silent + env: + CI: true + - name: manypkg lint + run: yarn manypkg check + env: + CI: true + - name: Build packages + run: yarn build + env: + CI: true + - name: yarn lint + run: yarn lint + env: + CI: true + + build-linux: + name: Build + runs-on: ubuntu-latest + env: + BLITZ_TELEMETRY_DISABLED: 1 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 25 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Cache node_modules + id: yarn-cache + uses: actions/cache@v2 + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + **/node_modules + key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.node_version}}-yarn-v14- + - run: yarn install --frozen-lockfile --check-files + - name: Build Packages + run: yarn build + - uses: actions/cache@v2 + id: cache-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + + testBlitzPackages: + name: Blitz - Test Packages + needs: build-linux + runs-on: ubuntu-latest + env: + BLITZ_TELEMETRY_DISABLED: 1 + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + - name: Setup kernel to increase watchers + if: runner.os == 'Linux' + run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + - name: Test Blitz Packages + run: yarn testonly:packages + env: + CI: true + + testNextPackages: + name: Next - Test Packages + defaults: + run: + working-directory: nextjs + needs: build-linux + runs-on: ubuntu-latest + env: + BLITZ_TELEMETRY_DISABLED: 1 + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + - name: Setup kernel to increase watchers + if: runner.os == 'Linux' + run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + - name: Test Next Packages + run: yarn testonly:packages + env: + CI: true + + testBlitzExamples: + timeout-minutes: 30 + name: Blitz - Test Example Apps (ubuntu-latest) + needs: build-linux + runs-on: ubuntu-latest + env: + BLITZ_TELEMETRY_DISABLED: 1 + DEBUG: blitz:session + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + # Needed to get cypress binary + - run: yarn cypress install + - name: Install sass + run: yarn install -W sass + - name: Setup kernel to increase watchers + if: runner.os == 'Linux' + run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + - name: Test examples + run: yarn testonly:examples + env: + CI: true + + testBlitzExamplesWin: + timeout-minutes: 30 + name: Blitz - Test Example Apps (windows-latest) + runs-on: windows-latest + env: + BLITZ_TELEMETRY_DISABLED: 1 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 25 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + # - name: Get yarn cache directory path + # id: yarn-cache-dir-path + # run: echo "::set-output name=dir::$(yarn cache dir)" + # - name: Cache node_modules + # id: yarn-cache + # uses: actions/cache@v2 + # with: + # path: | + # ${{ steps.yarn-cache-dir-path.outputs.dir }} + # **/node_modules + # key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }} + # restore-keys: | + # ${{ runner.os }}-${{ runner.node_version}}-yarn-v14- + - run: yarn install --frozen-lockfile --check-files + - name: Build Packages + run: yarn build + # Needed to get cypress binary + - run: yarn cypress install + - name: Install sass + run: yarn install -W sass + - name: Test examples + run: yarn testonly:examples + env: + CI: true + + checkPrecompiled: + name: Check Pre-compiled + defaults: + run: + working-directory: nextjs + runs-on: ubuntu-latest + needs: build-linux + env: + BLITZ_TELEMETRY_DISABLED: 1 + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - run: ./scripts/check-pre-compiled.sh + + testUnit: + name: Nextjs - Test Unit + defaults: + run: + working-directory: nextjs + runs-on: ubuntu-latest + needs: build-linux + env: + BLITZ_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - run: node run-tests.js --type unit -g 1/1 + + testIntegrationBlitz: + name: Blitz - Test Integration + needs: build-linux + runs-on: ubuntu-latest + env: + BLITZ_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + + # TODO: remove after we fix watchpack watching too much + - name: Setup kernel to increase watchers + if: runner.os == 'Linux' + run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + + - run: xvfb-run node nextjs/run-tests.js -c 3 + + testIntegrationBlitzWin: + name: Blitz - Test Integration (Windows) + runs-on: windows-latest + env: + BLITZ_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 25 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + # - name: Get yarn cache directory path + # id: yarn-cache-dir-path + # run: echo "::set-output name=dir::$(yarn cache dir)" + # - name: Cache node_modules + # id: yarn-cache + # uses: actions/cache@v2 + # with: + # path: | + # ${{ steps.yarn-cache-dir-path.outputs.dir }} + # **/node_modules + # key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }} + # restore-keys: | + # ${{ runner.os }}-${{ runner.node_version}}-yarn-v14- + - run: yarn install --frozen-lockfile --check-files + - name: Build Packages + run: yarn build + - run: node nextjs/run-tests.js + + testIntegration: + name: Nextjs - Test Integration + defaults: + run: + working-directory: nextjs + runs-on: ubuntu-latest + needs: build-linux + env: + BLITZ_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + strategy: + fail-fast: false + matrix: + group: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + # TODO: remove after we fix watchpack watching too much + - name: Setup kernel to increase watchers + if: runner.os == 'Linux' + run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + - run: xvfb-run node run-tests.js -g ${{ matrix.group }}/20 -c 3 + + testIntegrationWin: + name: Nextjs - Test Integration (Windows) + runs-on: windows-latest + env: + BLITZ_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 25 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: "14" + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + # - name: Get yarn cache directory path + # id: yarn-cache-dir-path + # run: echo "::set-output name=dir::$(yarn cache dir)" + # - name: Cache node_modules + # id: yarn-cache + # uses: actions/cache@v2 + # with: + # path: | + # ${{ steps.yarn-cache-dir-path.outputs.dir }} + # **/node_modules + # key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }} + # restore-keys: | + # ${{ runner.os }}-${{ runner.node_version}}-yarn-v14- + - run: yarn install --frozen-lockfile --check-files + - name: Build Packages + run: yarn build + - run: node run-tests.js test/integration/basic/test/index.test.js test/integration/production/test/index.test.js test/integration/multi-pages/test/index.test.js + working-directory: nextjs + + testElectron: + name: Nextjs - Test Electron + defaults: + run: + working-directory: nextjs + runs-on: ubuntu-latest + needs: build-linux + env: + BLITZ_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + TEST_ELECTRON: 1 + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + + # TODO: remove after we fix watchpack watching too much + - name: Setup kernel to increase watchers + run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + - run: cd test/integration/with-electron/app && yarn + - run: xvfb-run node run-tests.js test/integration/with-electron/test/index.test.js + + testsPass: + name: thank you, next + runs-on: ubuntu-latest + needs: + [ + checkPrecompiled, + testIntegration, + testIntegrationBlitz, + testIntegrationBlitzWin, + testUnit, + testBlitzPackages, + testNextPackages, + testBlitzExamples, + testBlitzExamplesWin, + ] + steps: + - run: exit 0 + + testFirefox: + name: Nextjs - Test Firefox (production) + defaults: + run: + working-directory: nextjs + runs-on: ubuntu-latest + needs: build-linux + env: + HEADLESS: true + BROWSER_NAME: "firefox" + BLITZ_TELEMETRY_DISABLED: 1 + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - run: node run-tests.js -c 1 test/integration/production/test/index.test.js + + testSafari: + name: Nextjs - Test Safari (production) + defaults: + run: + working-directory: nextjs + runs-on: ubuntu-latest + needs: build-linux + env: + BROWSERSTACK: true + BROWSER_NAME: "safari" + BLITZ_TELEMETRY_DISABLED: 1 + SKIP_LOCAL_SELENIUM_SERVER: true + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - run: '[[ -z "$BROWSERSTACK_ACCESS_KEY" ]] && echo "Skipping for PR" || node run-tests.js -c 1 test/integration/production/test/index.test.js' + + testSafariOld: + name: Nextjs - Test Safari 10.1 (nav) + defaults: + run: + working-directory: nextjs + runs-on: ubuntu-latest + needs: [build-linux, testSafari] + env: + BROWSERSTACK: true + LEGACY_SAFARI: true + BROWSERNAME: "safari" + BLITZ_TELEMETRY_DISABLED: 1 + SKIP_LOCAL_SELENIUM_SERVER: true + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ runner.os }}-${{ github.sha }} + - run: '[[ -z "$BROWSERSTACK_ACCESS_KEY" ]] && echo "Skipping for PR" || node run-tests.js test/integration/production-nav/test/index.test.js' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..56808c005c --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +.log +.DS_Store +.idea +.jest-* +node_modules +reports +*.log +.nyc_output +**/coverage +.yarn +.yarnrc +tsconfig.tsbuildinfo +.blitz +.next +dist +.now +# local env files +**/.env.local +**/.env.*.local +**/.envrc +.blitz-* +.blitz-cli-cache +.vscode +.tsbuildinfo +.nvmrc +**/.test* +examples/auth2 +.idea +.ultra.cache.json +db.sqlite-journal +test/integration/**/db.json +test/**/*/out +test/**/blitz-env.d.ts +examples/**/blitz-env.d.ts +.blitz** + +/examples/playwright/playwright-report/ +/examples/playwright/test-results/ diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000000..31354ec138 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..d2ae35e84b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged diff --git a/.kodiak.toml b/.kodiak.toml new file mode 100644 index 0000000000..749f395f85 --- /dev/null +++ b/.kodiak.toml @@ -0,0 +1,5 @@ +# .kodiak.toml +# Minimal config. version is the only required field. +version = 1 +merge.automerge_label = "0 - <(^_^)> - merge it! ✌️" +approve.auto_approve_usernames = ["flybayer", "beerose", "depfu"] diff --git a/.node-version b/.node-version new file mode 100644 index 0000000000..31102b28de --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +14.18.1 diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000000..23e5ff72a6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,16 @@ +.DS_Store +.prettierrc +.nyc_output +.travis.yml +coverage +coverage.lcov +bench +docs +src +examples +babel.config.js +test +CONTRIBUTING.md +CODE_OF_CONDUCT.md +*.ts +!*.d.ts \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..cffe8cdef1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..c97afebe36 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,25 @@ +**/migrations/** +.blitz +.next +.log +.DS_Store +.jest-* +packages/cli/lib +node_modules +reports +*.log +**/.env* +.nyc_output +**/coverage +.yarn +.yarnrc +tsconfig.tsbuildinfo +dist +bin +!packages/blitz/src/bin +packages/generator/templates/** +.github/ISSUE_TEMPLATE/bug_report.md +nextjs/packages/next/compiled/** + +// Because file from nextjs upstream isn't formatted properly +nextjs/packages/next/build/webpack-config.ts diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..be1c30da70 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# The Blitz Community Code of Conduct + +[Read the Code of Conduct at Blitzjs.com](https://blitzjs.com/docs/code-of-conduct) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..3b5ed7fcf3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# Contributing + +[Read the Contributing Guide at Blitzjs.com](https://blitzjs.com/docs/contributing) + +## Notes For Core Team + +### To Publish a new NPM Package under `@blitzjs/` namespace + +1. cd into the package directory +2. Run `npm publish --tag danger --access public` + - `--access public` is required because scoped packages are set to private by default + +### Syncing Next.js Fork + +1. Run `yarn push-nextjs` + - If it fails with an error of `git-subrepo: Can't commit: 'subrepo/nextjs' doesn't contain upstream HEAD:`, then run `yarn push-nextjs --force` (see https://github.com/ingydotnet/git-subrepo/issues/530) +2. Create new git branch for the upgrade +3. In the forked repo (https://github.com/blitz-js/next.js), run: + 1. `git pull` + 2. `git fetch --all` + 3. `git merge v10.2.0` (change the version to be the version you are updating to) + 4. Run `rm -rf examples && git add examples` + 5. To resolve conflict with their version for a path, like docs, run this: + - `git checkout --theirs docs && git add docs` + 6. Resolve all merge conflicts and complete merge + 7. Run `yarn` and make sure all builds complete + 8. Run `yarn lint` and fix any issues + 9. Commit all changes to finish merge + 10. `git push` +4. Run `yarn pull-nextjs` +5. Run `yarn` +6. Run `yarn manypkg check` and optionally `yarn manypkg fix` to fix any issues +7. Under `nextjs/`, run `./scripts/check-pre-compiled.sh` and commit the changes +8. Run `yarn build:nextjs` +9. Run `yarn lint` - fix any issues +10. Run `yarn build` - fix any issues +11. Run `yarn test:nextjs-size` and update tests if there are any failures +12. Open PR and fix any failing tests +13. Update any references to nextjs in new code including imports like `next/image`, etc. +14. Any doc updates needed? +15. Merge PR +16. `yarn push-nextjs` + +#### Troubleshooting + +##### yarn lint - Failed to load parser + +Caused by invalid version of `@babel/eslint-parser`. `7.13.14` is a working version. I think it may be an incompatibility between this version and the version of eslint? + +- change version of eslint-parser +- run `yarn --check-files` +- run `./scripts/check-pre-compiled.sh` from `./nextjs/` +- run `yarn build:nextjs` from root +- Try linting again + +``` +~/c/blitz> yarn lint +yarn run v1.22.10 +$ eslint --ext ".js,.ts,.tsx" . + +Oops! Something went wrong! :( + +ESLint: 7.21.0 + +Error: Failed to load parser './parser.js' declared in 'examples/auth/.eslintrc.js » eslint-config-blitz » eslint-config-next': Cannot find module '@babel/parser' + at webpackEmptyContext (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/bundle.js:1:33258) + at Object.73139 (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/bundle.js:2194:783181) + at __nccwpck_require__ (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/bundle.js:2194:1065271) + at Object.eslintParser (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/bundle.js:1:43676) + at Object. (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/eslint-parser.js:1:100) + at Module._compile (/Users/b/c/blitz/node_modules/v8-compile-cache/v8-compile-cache.js:192:30) + at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10) + at Module.load (internal/modules/cjs/loader.js:863:32) + at Function.Module._load (internal/modules/cjs/loader.js:708:14) + at Module.require (internal/modules/cjs/loader.js:887:19) +error Command failed with exit code 2. +``` + +##### Failed to compile - LICENSE + +This error occurs sometimes when you import code from packages/next/build/utils.ts into some other code like config-shared.ts. Solution is to move the code into another file. + +``` +Failed to compile. +../../../packages/next/dist/compiled/webpack/LICENSE +Module parse failed: Unexpected token (1:10) +You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See webpack.js.org/concepts#loaders +> Copyright JS Foundation and other contributors +| +| Permission is hereby granted, free of charge, to any person obtaining +``` diff --git a/CONTRIBUTOR_STATS.md b/CONTRIBUTOR_STATS.md new file mode 100644 index 0000000000..75744cc29a --- /dev/null +++ b/CONTRIBUTOR_STATS.md @@ -0,0 +1,3 @@ +# Contributor over time + +[![Contributor over time](https://contributor-graph-api.apiseven.com/contributors-svg?chart=contributorOverTime&repo=blitz-js/blitz)](https://www.apiseven.com/en/contributor-graph?chart=contributorOverTime&repo=blitz-js/blitz) diff --git a/LICENSE b/LICENSE index f409bbbf20..4efa340991 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Brandon Bayer +Copyright (c) 2021 Brandon Bayer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MEETING_NOTES.md b/MEETING_NOTES.md new file mode 100644 index 0000000000..f69565dda4 --- /dev/null +++ b/MEETING_NOTES.md @@ -0,0 +1,124 @@ +# 2020-08-17 Blitz Contributor Call + +- Attending: Brandon Bayer, Adam Markon, Kellen Mace, Myron Davis, Dwight Watson +- Brandon: + - Auth out, set up by default in canary release + - Need to work on a potential CSRF bug + - Next major release will include auth by default and allow you to choose your form library + - Next auth features are email confirmation + - After that, logging and plugins are next +- Adam: + - Overhauled the recipe infrastructure. Now using jscodeshift instead of recast + - Added support for conditional JSX in templates + - Going to work on custom templates next +- Dwight + - Has been opening issues for problems + - Made a few PRs for some issues + +# 2020-07-07 Blitz Contributor Call + +- Attending: Brandon Bayer, Robert Rosenburg, Jeremy Liberman +- Brandon: + - Finishing up session managment code + - Waiting for review of session managment code from Rishabh + - Will be working on actual auth code (vs session management) like password hashing, password reset, social login, etc. + - Benefits of our session managment vs rails + - Don’t have to redirect to login page + - Using top level error component that catches authentication errors + - You can login from anywhere, during sign up +- Robert: + - Waiting on Kirstina’s website designs. Desktop design is finished, but she's working on mobile design + - Code snippet / sandbox of blitz code for the website + +# 2020-06-23 Blitz Contributor Call + +- Attending: Brandon Bayer, Robert Rosenberg, Justin Hall, Adam Markon +- Brandon: + - Server side session management code is mostly set up. Still need to integrate client side of RPC calls to expose session information + - Identity verification/Oauth integrations still need to be firmed up + - Once auth is wrapped up we should be ready to start on plugins +- Adam: + - Been experimenting with smart page generation based on the current schema model + - MDX installer recipes +- Robert: + - Waiting on designs from Kristina + - Prism component from theme ui customization? If not try and grab source code from docusaurus line highlighting component +- Justin: + - Updated the tutorial + - Helped with various bug fixes + +# 2020-06-17 Blitz Contributor Call + +- Attending: Brandon Bayer, Fran Zekan, Sigurd Wahl +- Brandon: + - Spent the past week implementing HTTP middlware which is now released! + - Now working on implementing session management! +- Fran: + - Finishing up the PR for adding `blitz db seed` +- Sigurd: + - New to the community, slowly learning the codebase + - Exicited to get a first PR in here sometime soon :) + +# 2020-06-09 Blitz Contributor Call + +- Attending: Brandon Bayer, Rudi Yardley, Fran Zekan, Adam Markon, Robert Rosenberg, Kristina Matuska +- Brandon: + - blitzjs.com published, docs + marketing site v0.1 live + - For now most of the docs copied from react-query and Next, we should eventually clean them up so they're stylistically similar to ours, but really easy to start + - HTTP middleware is almost done, just fixing a few edge cases + - Authentication is up next after that, translating pseudocode into actual code +- Kristina: + - Homepage design mostly wrapped up right now, have to finish up mobile and light mode and then ready to move on to the rest of the docs + - Syntax highlighting, we can customize colors + - Sidebar inspiration from tailwindcss +- Robert: + - Decided to wait to convert existing components to theme UI until the final design is done + - More docs content: + - More guides + - Improve the tutorial to incorporate relationship generation + - Add branches for canary and master + - If you add a feature you can add your documentation to the canary branch +- Adam: + - blitz generate model finished! + - Installer rewrite complete + - At similar place to what Gatsby has for installing stuff + - Next up: + - Support gatsby MDX recipes + - Make all code generators aware of actual model attributes +- Fran: + - Working on a package for getting the blitz config anywhere - getConfig() + - Prevent app from "warming" the server when deployed as server rather than serverless + - Testing examples - e2e with cypress and unit tests with Jest so we can link to a testing setup in the docs/getting started guide +- Rudi: + - Extracted out the @blitzjs/file-pipeline (previously synchronizer) + - Extracted out the @blitzjs/display package + - Working on various Next.js compatibility issues + - Debugging a bug in blitz start where it gets stuck at \_manifest.json + +# 2020-05-26 Blitz Contributor Call + +- Attending: Brandon Bayer, Robert Rosenberg, Adam Markon, Simon Debbarma +- Brandon: + - Kitze livestream last week went great — recording on youtube + - Codebase walkthrough yesterday went great — recording on youtube + - Website overhaul, installed Theme UI +- Adam: + - Opened PR for Prisma model generation from the CLI + - Working on Installer stuff and prepping for integration with Gatsby recipes +- Simon + - Working on custom illustrations for the web +- Robert + - Misc work on website +- Website + - Most website components are owned by us now instead of docusaurus, we'll need to be weary of api updates and any other important component updates + - Make sidebar like tailwind docs sidebar + - Dark theme needs to be fixed + - Theme switcher inconsistent + - Live code sandbox examples + - Code comparison between blitz and rails +- Auth + - Rishabh continuing to work on pseudo code for the session management library + - Brandon planning to build http middleware support this week +- CLI + - Adam working on new features, including generating prisma models and making existing templates aware of actual model attributes + - Plugin ideas / discussion once recipes are farther along. Will post an RFC for plugins at some point diff --git a/README.md b/README.md index f1d2bed238..8605bb719d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,805 @@ -# blitz -Framework for building monolithic, full-stack, serverless React apps with zero data-fetching and zero client-side state management +[![Blitz.js](https://raw.githubusercontent.com/blitz-js/art/master/github-cover-photo.png)](https://blitzjs.com) + + +

+ + + + + + + + + + + + +

+ + +
+ +

The Fullstack React Framework

+ +
"Zero-API" Data Layer — Built on Next.js — Inspired by Ruby on Rails
+

Read the Documentation

+
+ +“Zero-API” data layer **lets you import server code directly into your React components** instead of having to manually add API endpoints and do client-side fetching and caching. + +New Blitz apps come with **all the boring stuff already set up for you!** Like ESLint, Prettier, Jest, user sign up, log in, and password reset. + +Provides **helpful defaults and conventions** for things like routing, file structure, and authentication while also being extremely flexible. + + +
+ +### Quick Start + +You need Node.js 12 or newer + +#### Install Blitz + +Run `npm install -g blitz` or `yarn global add blitz` + +_You can alternatively use [`npx`](https://www.npmjs.com/package/npx)_ + +#### Create a New App + +1. `blitz new myAppName` +2. `cd myAppName` +3. `blitz dev` +4. View your brand new app at http://localhost:3000 + +

+ + +Bytes Newsletter + + + +

+ + + +### The Foundational Principles + +1. Fullstack & Monolithic +2. API Not Required +3. Convention over Configuration +4. Loose Opinions +5. Easy to Start, Easy to Scale +6. Stability +7. Community over Code + +[The Blitz Manifesto](https://blitzjs.com/docs/manifesto) explains these principles in detail. + +
+ +### What is Blitz Designed For? + +Blitz is designed for tiny to large database-backed applications that have one or more graphical user interfaces. + +While we currently only support web, we are pursuing the dream of a single monolithic application that runs on web and mobile with maximum code sharing and minimal boilerplate. + +
+ +## Welcome to the Blitz Community 👋 + +The Blitz community is warm, safe, diverse, inclusive, and fun! LGBTQ+, women, and minorities are especially welcome. Please read our [Code of Conduct](https://blitzjs.com/docs/code-of-conduct). + +[Join our Discord Community](https://discord.blitzjs.com) where we help each other build Blitz apps. It's also where we collaborate on building Blitz itself. + +For questions and longer form discussions, [post in our forum](https://github.com/blitz-js/blitz/discussions). + +There's still a lot of work to do, so you are especially invited to join us in building Blitz! A good place to start is [The Contributing Guide](https://blitzjs.com/docs/contributing). + +
+ +## Financial Contributors + +Your financial contributions help ensure Blitz continues to be developed and maintained! We have monthly sponsorship options starting at $5/month. + +👉 View options and contribute at [GitHub Sponsors](https://github.com/sponsors/blitz-js), [PayPal](https://paypal.me/thebayers), or [Open Collective](https://opencollective.com/blitzjs) + + +### 🌱 Seedling Sponsors + + + + + + + +
+ + + + + + + + + + + +
+ + +### 🥉 Bronze Sponsors + + + + + + + +
+ + + + + +
+ + + +### 🥈 Silver Sponsors + + + + + +
+ + + +
+ +### 🏆 Gold Sponsors + + + + + +### 💎 Diamond Sponsors + + + + + +
+ +## Core Team ✨ + + + + + + + + +

Brandon Bayer

Creator

Aleksandra Sikora

Lead Maintainer
+ + + +
+ + +## Maintainers (Level 2) ✨ + +_Code ownership, pull request approvals and merging, etc_ (see [Maintainers L2](https://blitzjs.com/docs/maintainers#level-2-maintainers)) + + + + + + + + +

Simon Knott

SuperJSON

Juan Martín Seery

Website/Docs
+ + + +
+ +## Maintainers (Level 1) ✨ + +_Issue triage, pull request triage, community encouragement and moderation, etc_ (see [Maintainers L1](https://blitzjs.com/docs/maintainers#level-1-maintainers)) + + + + + + + + + + + + + +

Jeremy Liberman
+ + Mina Abadir avatar
+ + Mina Abadir + +
+
+ + Abu Uzayr avatar
+ + Abu Uzayr + +
+
+ + Damilola Randolph avatar
+ + Damilola Randolph + +
+
+ + Saheen Shoukath avatar
+ + Saheen Shoukath + +
+
+ + Husnul Jahneer avatar
+ + Husnul Jahneer + +
+
+ + + + +
+ +## Contributors ✨ + +Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Brandon Bayer

💻 🖋 🤔 👀 ⚠️ 📖

Rudi Yardley

💻 🤔 👀 ⚠️

Dylan Brookes

💻 🤔 👀 ⚠️ 📖

Adam Markon

💻 🤔 👀 ⚠️ 🚧

Corey Brown

💻 👀 🚧

Lori Karikari

💻 👀 🚧 📖

Elias Johansson

💻 👀 🚧

Michael Edelman

🚇 💻

Todd Geist

💵 💻

Robert Rosenberg

💻 🚧 📖

Beata Obrok

💻

Tahir Awan

💻

Camilo Gonzalez

💻

Daniel Kempner

💻

Giel

💻

Jeremy Liberman

💻 🚧 ⚠️ 📖

Jim Cummins

💻

Kristina Matuška

🎨

Jason Blalock

💻

aej11a

💻

marcoseoane

🤔

Rishabh Poddar

🤔

Lorenzo Rapetti

💻

Justin Hall

💻 📖

Sajjad Hashemian

💻

Eduardo Lopes

💻

Matthew Leffler

📖

Matt

📖

Sonny

📖

Fran Zekan

💻 📖 ⚠️

Jan Baykara

📖

Mike Perry Y Attara

📖

Devan

📖

Jack Clancy

💻 🚧

Nicolas Torres

⚠️ 💻 👀 📖

Simon Knott

💻 ⚠️ 🚧 📖

Jaga Santagostino

💻 📖 🚧

João Portela

💻

Da-Jin Chu

💻

Shinobu Hayashi

💻

Karan Kiri

💻

Alan Long

📖

codingsh

💻

Rafael Nunes

👀 💻

Simon Debbarma

🎨 🚧 📖

0xflotus

💻 📖

tmns

💻 📖

Jru Harris

📖

Ivan Medina

💻 🚧

Dwight Watson

💻 📖

Horie Issei

💻

Nhat Khanh

💻

Abu Uzayr

💻 📖

Nabiullah elham

💻

Lachlan Campbell

💻

Enzo Ferey

💻

Pierre Grimaud

💻

Andreas Adam

💻

Kevin Tovar

💻

Ante Primorac

💻 📖

Mykal Machon

💻

Jamie Davenport

💻 🚧

GaneshMani

💻 ⚠️

reymon359

💻

gvasquez11

💻

José Miguel Ochoa

💻

Oscar Sirvent

💻 📖

Daniel Molnar

📖 💻

Kevin Wu Won

📖

John Duong

💻

Noah Fleischmann

💻

Matsumoto Toshi

💻 📖

Simon Edelmann

💻

Shaun Church

📖 💻

Steven

📖

Sigurd Moland Wahl

💻

Brian Andrews

📖

Garrison Snelling

📖

Ty Lange-Smith

💻

Rubén Moya

💻 ⚠️

robertgrzonka

💻 🚇

Alex Orr

💻

Chris Tse

💻

Netto Farah

💻

Rohan Julka

🚇

Ivan Santos

💻

Soumyajit Pathak

💻

Sebastian Kurpiel

📖

Steffan

💻 📖 💵

Kristóf Poduszló

💻

Weilbyte

💻 📖

Ricardo Trejos

💻 📖

George Karagkiaouris

💻 📖

Brady Pascoe

💻

Jirka Svoboda

💻

Alan Alickovic

💻 📖

Yngve Høiseth

📖

Bruno Crosier

📖

Johan Schepmans

💻

Dillon Raphael

💻 ⚠️ 📖

Cody G

💻 ⚠️

madflow

📖

Satoshi Nitawaki

💻 🚧 💬 📖

sirmyron

📖 💻

engelkes-finstreet

📖 💻 🚧

Denis Radin

👀 💻 📖

Michael Li

💻

yuta0801

💻

Obadja Ris

📖

Jose Felix

💻

John Cantrell

💻

Kwuang Tang

💻

John Letey

💻

Juan Di Toro

💻

Taylor Johnson

💻 📖

Sriram Thiagarajan

📖

Sergio Xalambrí

📖

Patrick G

💻

अभिनाश (Avinash)

💻

Enrico Schaaf

💻

Kitze

🤔

Mohamed Shaban

💻

Joris

💻

Valentin Funk

📖

Luke Bennett

💻

Haseeb Majid

💻

Phillipp Schmedt

💻

Piotr Monwid-Olechnowicz

💻

Kotaro Chikuba

💻 ⚠️

Konrad Kalemba

💻 📖

Alucard17

💻

Domantas Mauruča

⚠️ 💻

Stratulat Alexandru

💻 🚧

André Ericson

💻 📖

Carlos Fernández

📖

Kevin Østerkilde

📖 💻

aaronfulkerson

💻 💬

Alexandru Naiman

💻

David Ezekiel Lutta

💻

wanjuntham

💻

Victor Nahuel Chaves

💻

Peter Shih

💻

Seweryn Kalemba

💻

Nikhil Saraf

💻 📖

Zane

📖

Dulce Hernández

💻

Mark Hähnel

💻

Viktor Nemes

💻

Gabe O'Leary

📖

Lucas Machado

💻

maciek_grzybek

💻

Michael Weibel

💻

Hiroki Isogai

💻

matamatanot

📖

Eric Sakmar

📖

Simon Legg

📖

Robert Soriano

💻

Benedikt Schnatterbeck

💻

Talor Anderson

💻 📖

Akira Baruah

💻

Christopher Wray

💻

Piotrek Tomczewski

💻 📖

Raphaël Huchet

📖 ⚠️ 💻

Alex Johansson

💻

David Mazza

💻

Ray Andrew

💻 📖

Abdullah Mzaien

💻 📖

William Kwao

📖

Lukas Strassel

💻 ⚠️

Thibaut Patel

💻

Jon Stuebe

💻

Ugo Onali

📖

SaintMalik

📖

Khaled Garbaya

💻

tundera

💻 ⚠️ 📖

markylaing

💻 📖

Akifumi Sato

💻

Beep LIN

💻

Matt Wood

💻

Joaquin Bravo Contreras

💻

Arjun Dubey

💻

chanand

💻

phillipkregg

📖

Tim Reynolds

📖

Linbudu

📖

C Reimers

📖

Tsuyoshi Osawa

💻

Rembrandt Reyes

💻 📖 ⚠️

Toshiya Doi

📖

t.kuriyama

💻

Robert Malko

💻

Ranjan Purbey

💻

tarunama

💻

David Kramer

💻

Michael Esteban

📖

marina

📖 💻

Jonas Thiesen

📖

Yash Thakkar

💻

Kazuma Suzuki

🎨 💻

Yuji Matsumoto

📖

Gimel Dick

💻

Andreas Bollig

💻 📖

AJ Markow

⚠️ 💻

TagawaHirotaka

💻 ⚠️

Amr A.Mohammed

💻

Lucas Willems

📖 💻

Alistair Smith

💻

Rodrigo Ehlers

💻

Michael Ford

💻

Brian Liu

💻

Aleksandra Sikora

💻 📖 ⚠️

JuanM04

💻 📖 ⚠️

Arend de Boer

📖

Felipe Milani

📖

Joe Edelman

💻

Gary

📖

Oliver Lopez

📖

Andreas Zaralis

📖

David Torbeck

📖

Gustavo Gard

📖

Immortalin

💻

Cristian Granda

💻

Denise Yu

💻

Andrea Della Corte

📖

Adit Sachde

📖

Hiren Chauhan

💻

Mark Jackson

📖 💻

Lewis Blackburn

📖

Vytenis

💻

Matthieu

💻 ⚠️

Mitchell Johnson

💻

Roshan Manuel

💻 📖 ⚠️

Kevin Langley Jr.

💻 📖

Gabriel Picard

📖

Ryan Chenkie

📖

Santhosh B. Appan

📖

James Moran

💻 📖

Jack Zhao

💻

Hisaki Akaza

📖

Flavio

💻

Bhanu Teja Pachipulusu

💻

Pavel Struhar

💻

Reo Ishiyama

💻

Tom MacWright

📖

François Best

💻

Faraz Patankar

📖

Eric Vicenti

📖 💻

Alex Dolan

📖 💻 ⚠️

Mathis Pinsault

📖

gstranger

💻 📖

Mark Hughes

💻 📖

Andrea Rizzello

📖

Jahred Hope

📖

Simon El Nahas

📖

Buleandra Cristian

📖 💻

Pedro Enrique Palau Isaac

💻

sean-brydon

📖

Alessandro

📖

laubonghaudoi

📖

Tommaso Bruno

📖

Antony

📖

Fatih Altinok

📖

Mokshit Jain

💻

Muhammad Ubaid Raza

💻 📖

Nick Warren

💻

mlabate

📖

Lukas Spieß

📖

DawnOfMidnight

📖

Kenza Iraki

⚠️ 💻

Agusti Fernandez

💻

Anjianto

💻

Blanc Adrien

💻

meepdeew

📖

Hardik Gaur

📖

acornellier

💻

craigglennie

📖

Fernando Villasenor

💻

swiftgaruda

📖

Pankaj Patil

📖

Mina Abadir

💻 📖 ⚠️

Francesco Sardo

📖 💻

Nikolay

📖

Dipesh Wagle

💻

Benjamin Bender

💻

Nima Shoghi

💻

Andreas Thomas

📖

guoqqqi

📖

Tim

💻 ⚠️

Marek Orłowski

📖

Antoine G

💻

Sean Winner

💻 ⚠️ 📖

Max Programming

💻

Sebastian Hoitz

⚠️ 💻

garnerp

📖

kivi

💻

Dan Greaves

💻

Lukas Neumann

📖 💻 ⚠️

Dustin Bachrach

💻 📖

Ashikka Gupta

💻 ⚠️

Daniel Almaguer

📖

Kevin Peters

📖

Daniel Bannert

💻 📖

Benja Kugler

💻

Eric Semeniuc

⚠️ 💻

Ricardo Romero

📖

Moritz Reiter

📖

Matt Sichterman

📖

Kai Schlamp

📖

Muyiwa Olu

💻

Rabbi Hossain

📖

bravo-kernel

💻

Sam Holmes

💻

Miguel Cabrerizo

💻 📖

Zack Hobson

💻 📖

Mokhtar

📖

Ken Kuan

💻

meehawk

💻

Rahul Ravindran

💻

Ilya

💻 📖 ⚠️

Hashim Warren

📖

Damilola Randolph

📖

Matt Campbell

📖

(◕ᴥ◕)

💻

Mat Milbury

📖

Andreas Asprou

💻 ⚠️

Kot

💻 ⚠️ 📖

Amane

📖

John Leung

📖

Bruce

💻

Emily

💻

Nathan Verni

📖

Davy Engone

📖

Federico Joel Orlandau

📖 💻

John Murphy

📖 💻

martinsaxa

💻

Austin Walhof

📖

Geoffrey

💻 📖 ⚠️

Kevin Pham

📖

kimngan-bui

📖

Bahk Chanhee

💻

John Vandivier

💻 ⚠️ 📖

Namir

📖 💻 ⚠️

Scott Cooper

📖

Abduttayyeb M.r

📖

Mayuran

💻

Aleksei Vesselko

📖

Punn Siriphanthong

💻

Shawn Fetanat

📖

Moyuru

💻 ⚠️ 📖

Cam Sloan

📖

Maciek Sitkowski

📖

Vivek

📖 💻

CJ Lazell

💻

Robert

📖

Christian Jensen

📖

Devin Rasmussen

💻

Thomas Brenneur

💻 📖 ⚠️

Lucas Vazquez

💻

Chris Johnson

📖

Rob Stevenson

📖

Lucas Heymès

💻 📖

Lasse Norfeldt

📖

Péter Nyári

📖 💻

Holger Frohloff

📖

Basil Khan

📖

Daniel Esteves

📖

Cory House

📖

Austin (Thang Pham)

📖

Marcus Reinhardt

📖 💻

David Christie

📖

Ajanth

📖

Div

📖

David Arteaga

📖

Mukul Kolpe

💻

tyler

💻

Sofiane Djellouli

📖

kreako

📖

Sarah Dayan

💻

Cristi Ciobanu

📖

Arpit Dalal

📖

robertrisch

📖

Dinesh Gadge

💻

Malte Kießling

📖

Oscar Franco

📖

Adam Trager

💻

saheenshoukath

📖

Husnul Jahneer

📖

Reyk

📖

Lokprakash Babu

📖

eai04191

📖

Numan

📖

Joel Coutinho

📖

David Barker

📖

Tim Feeley

💻

lucas philippe

📖

Blake Bayer

💻 📖

R Massie

📖

Paul

📖

Min ho Kim

📖

webdeb

📖

David

📖 💻 ⚠️

Jake Dowie

📖

Datner

📖 💻 ⚠️

remlse

📖

Sergei Smirnov

📖

Dawid Urbaniak

📖 💻

Kacper Potyrała

📖

Jeeho Ahn

📖 🔧
+ + + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..38ea3b20f0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,10 @@ +# Security Policy + +## Supported Versions + +TODO + +## Reporting a Vulnerability + +Email Brandon Bayer at b@bayer.ws to report a vulnerability, and he will follow up with you asap. + diff --git a/__mocks__/fs.js b/__mocks__/fs.js new file mode 100644 index 0000000000..fd7ed68318 --- /dev/null +++ b/__mocks__/fs.js @@ -0,0 +1,3 @@ +const {fs} = require("memfs") + +module.exports = fs diff --git a/assets/Fauna_Logo_Blue.png b/assets/Fauna_Logo_Blue.png new file mode 100644 index 0000000000..7b6f7485a5 Binary files /dev/null and b/assets/Fauna_Logo_Blue.png differ diff --git a/assets/andreas.jpg b/assets/andreas.jpg new file mode 100644 index 0000000000..43d8967b0b Binary files /dev/null and b/assets/andreas.jpg differ diff --git a/assets/boostry.svg b/assets/boostry.svg new file mode 100644 index 0000000000..489222d14c --- /dev/null +++ b/assets/boostry.svg @@ -0,0 +1 @@ +アセット 15 \ No newline at end of file diff --git a/assets/jdlt.png b/assets/jdlt.png new file mode 100644 index 0000000000..f62b731f13 Binary files /dev/null and b/assets/jdlt.png differ diff --git a/assets/meetkai.png b/assets/meetkai.png new file mode 100644 index 0000000000..f3896f65ae Binary files /dev/null and b/assets/meetkai.png differ diff --git a/assets/render-logo-color2.png b/assets/render-logo-color2.png new file mode 100644 index 0000000000..9cd298ebe7 Binary files /dev/null and b/assets/render-logo-color2.png differ diff --git a/assets/rit_logo.png b/assets/rit_logo.png new file mode 100644 index 0000000000..0632eb5b1f Binary files /dev/null and b/assets/rit_logo.png differ diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000000..37a1405c44 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,64 @@ +module.exports = { + presets: [ + "@babel/preset-typescript", + "@babel/preset-react", + [ + "@babel/preset-env", + { + modules: false, + loose: true, + exclude: [ + "@babel/plugin-transform-async-to-generator", + "@babel/plugin-transform-regenerator", + ], + }, + ], + ], + plugins: [ + "babel-plugin-annotate-pure-calls", + "babel-plugin-dev-expression", + ["@babel/plugin-proposal-class-properties", {loose: true}], + "babel-plugin-macros", + [ + "transform-inline-environment-variables", + { + include: ["BLITZ_PROD_BUILD"], + }, + ], + ], + overrides: [ + { + test: "./test/**/*", + presets: [ + [ + "@babel/preset-env", + { + modules: false, + exclude: [ + "@babel/plugin-transform-async-to-generator", + "@babel/plugin-transform-regenerator", + ], + }, + ], + "blitz/babel", + ], + plugins: [], + }, + { + test: "./nextjs/test/**/*", + presets: [ + [ + "@babel/preset-env", + { + modules: false, + exclude: [ + "@babel/plugin-transform-async-to-generator", + "@babel/plugin-transform-regenerator", + ], + }, + ], + "blitz/babel", + ], + }, + ], +} diff --git a/examples/auth/.eslintrc.js b/examples/auth/.eslintrc.js new file mode 100644 index 0000000000..f845b10d52 --- /dev/null +++ b/examples/auth/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["blitz"], +} diff --git a/examples/auth/.gitignore b/examples/auth/.gitignore new file mode 100644 index 0000000000..e88ce12e7a --- /dev/null +++ b/examples/auth/.gitignore @@ -0,0 +1,57 @@ +# dependencies +node_modules +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* +.npm +web_modules/ + +# blitz +/.blitz/ +/.next/ +*.sqlite +.now +.blitz-console-history +blitz-log.log + +# misc +.DS_Store + +# local env files +.env +.envrc +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +coverage +*.lcov +.nyc_output +lib-cov + +# Caches +*.tsbuildinfo +.eslintcache +.node_repl_history +.yarn-integrity + +# Serverless directories +.serverless/ + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +.vercel diff --git a/examples/auth/.npmrc b/examples/auth/.npmrc new file mode 100644 index 0000000000..cffe8cdef1 --- /dev/null +++ b/examples/auth/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/examples/auth/.prettierignore b/examples/auth/.prettierignore new file mode 100644 index 0000000000..c61fdf72dc --- /dev/null +++ b/examples/auth/.prettierignore @@ -0,0 +1,5 @@ +.gitkeep +.env* +*.ico +*.lock +db/migrations diff --git a/examples/auth/.vercelignore b/examples/auth/.vercelignore new file mode 100644 index 0000000000..d587271652 --- /dev/null +++ b/examples/auth/.vercelignore @@ -0,0 +1,2 @@ +.blitz +*.sqlite diff --git a/examples/auth/README.md b/examples/auth/README.md new file mode 100644 index 0000000000..c68fda9cc3 --- /dev/null +++ b/examples/auth/README.md @@ -0,0 +1,26 @@ +# auth + +## Getting Started + +1. Add this code to db/schema.prisma: + +``` +model Project { + id Int @default(autoincrement()) @id + name String +} +``` + +2. DB migrate + +``` +blitz prisma migrate dev +``` + +3. Start the dev server + +``` +blitz dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/examples/auth/app/api/.keep b/examples/auth/app/api/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/auth/app/api/auth/[...auth].ts b/examples/auth/app/api/auth/[...auth].ts new file mode 100644 index 0000000000..ce5b6539fd --- /dev/null +++ b/examples/auth/app/api/auth/[...auth].ts @@ -0,0 +1,145 @@ +import {passportAuth, PublicData} from "blitz" +import db from "db" +import {Strategy as TwitterStrategy} from "passport-twitter" +import {Strategy as GitHubStrategy} from "passport-github2" +import {Strategy as Auth0Strategy} from "passport-auth0" +import {Role} from "types" + +function assert(condition: any, message: string): asserts condition { + if (!condition) throw new Error(message) +} + +// These aren't required, but this is a good practice to ensure you have the env vars you need +assert(process.env.TWITTER_CONSUMER_KEY, "You must provide the TWITTER_CONSUMER_KEY env variable") +assert( + process.env.TWITTER_CONSUMER_SECRET, + "You must provide the TWITTER_CONSUMER_SECRET env variable", +) +assert(process.env.GITHUB_CLIENT_ID, "You must provide the GITHUB_CLIENT_ID env variable") +assert(process.env.GITHUB_CLIENT_SECRET, "You must provide the GITHUB_CLIENT_SECRET env variable") + +assert(process.env.AUTH0_DOMAIN, "You must provide the AUTH0_DOMAIN env variable") +assert(process.env.AUTH0_CLIENT_ID, "You must provide the AUTH0_CLIENT_ID env variable") +assert(process.env.AUTH0_CLIENT_SECRET, "You must provide the AUTH0_CLIENT_SECRET env variable") + +export default passportAuth(({ctx, req, res}) => ({ + successRedirectUrl: "/", + strategies: [ + { + strategy: new TwitterStrategy( + { + consumerKey: process.env.TWITTER_CONSUMER_KEY as string, + consumerSecret: process.env.TWITTER_CONSUMER_SECRET as string, + callbackURL: + process.env.NODE_ENV === "production" + ? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/twitter/callback" + : "http://localhost:3000/api/auth/twitter/callback", + includeEmail: true, + }, + async function (_token, _tokenSecret, profile, done) { + const email = profile.emails && profile.emails[0]?.value + + if (!email) { + // This can happen if you haven't enabled email access in your twitter app permissions + return done(new Error("Twitter OAuth response doesn't have email.")) + } + + console.log(ctx.session.userId) + + const user = await db.user.upsert({ + where: {email}, + create: { + email, + name: profile.displayName, + }, + update: {email}, + }) + + const publicData = {userId: user.id, role: user.role, source: "twitter"} + done(null, {publicData}) + }, + ), + }, + { + strategy: new GitHubStrategy( + { + clientID: process.env.GITHUB_CLIENT_ID as string, + clientSecret: process.env.GITHUB_CLIENT_SECRET as string, + callbackURL: + process.env.NODE_ENV === "production" + ? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/github/callback" + : "http://localhost:3000/api/auth/github/callback", + }, + async function ( + _token: string, + _tokenSecret: string, + profile: any, + done: (err: Error | null, data?: {publicData: PublicData}) => void, + ) { + const email = profile.emails && profile.emails[0]?.value + + if (!email) { + // This can happen if you haven't enabled email access in your twitter app permissions + return done(new Error("Twitter OAuth response doesn't have email.")) + } + + const user = await db.user.upsert({ + where: {email}, + create: { + email, + name: profile.displayName, + }, + update: {email}, + }) + + const publicData = { + userId: user.id, + role: user.role as Role, + source: "github", + githubUsername: profile.username, + } + done(null, {publicData}) + }, + ), + }, + { + authenticateOptions: {scope: "openid email profile"}, + strategy: new Auth0Strategy( + { + domain: process.env.AUTH0_DOMAIN as string, + clientID: process.env.AUTH0_CLIENT_ID as string, + clientSecret: process.env.AUTH0_CLIENT_SECRET as string, + callbackURL: + process.env.NODE_ENV === "production" + ? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/auth0/callback" + : "http://localhost:3000/api/auth/auth0/callback", + }, + async function (_token, _tokenSecret, _extraParams, profile, done) { + const email = profile.emails && profile.emails[0]?.value + + if (!email) { + // This can happen if you haven't enabled email access in your twitter app permissions + return done(new Error("Auth0 response doesn't have email.")) + } + + const user = await db.user.upsert({ + where: {email}, + create: { + email, + name: profile.displayName, + }, + update: {email}, + }) + + const publicData = { + userId: user.id, + role: user.role, + source: "auth0", + githubUsername: profile.username, + } + done(undefined, {publicData}) + }, + ), + }, + ], +})) diff --git a/examples/auth/app/auth/components/LoginForm.tsx b/examples/auth/app/auth/components/LoginForm.tsx new file mode 100644 index 0000000000..9bd3117951 --- /dev/null +++ b/examples/auth/app/auth/components/LoginForm.tsx @@ -0,0 +1,56 @@ +import {AuthenticationError, Link, useMutation} from "blitz" +import {LabeledTextField} from "app/core/components/LabeledTextField" +import {Form, FORM_ERROR} from "app/core/components/Form" +import login from "app/auth/mutations/login" +import {Login} from "app/auth/validations" + +import {Routes} from ".blitz" + +type LoginFormProps = { + onSuccess?: () => void +} + +export const LoginForm = (props: LoginFormProps) => { + const [loginMutation] = useMutation(login) + + return ( +
+

Login

+ +
{ + try { + await loginMutation(values) + props.onSuccess?.() + } catch (error: any) { + if (error instanceof AuthenticationError) { + return {[FORM_ERROR]: "Sorry, those credentials are invalid"} + } else { + return { + [FORM_ERROR]: + "Sorry, we had an unexpected error. Please try again. - " + error.toString(), + } + } + } + }} + > + + + + + +
+ Or Sign Up +
+
+ ) +} + +export default LoginForm diff --git a/examples/auth/app/auth/components/SignupForm.tsx b/examples/auth/app/auth/components/SignupForm.tsx new file mode 100644 index 0000000000..0825471ae6 --- /dev/null +++ b/examples/auth/app/auth/components/SignupForm.tsx @@ -0,0 +1,44 @@ +import React from "react" +import {useMutation} from "blitz" +import {LabeledTextField} from "app/core/components/LabeledTextField" +import {Form, FORM_ERROR} from "app/core/components/Form" +import signup from "app/auth/mutations/signup" +import {Signup} from "app/auth/validations" + +type SignupFormProps = { + onSuccess?: () => void +} + +export const SignupForm = (props: SignupFormProps) => { + const [signupMutation] = useMutation(signup) + + return ( +
+

Create an Account

+ +
{ + try { + await signupMutation(values) + props.onSuccess?.() + } catch (error: any) { + if (error.code === "P2002" && error.meta?.target?.includes("email")) { + // This error comes from Prisma + return {email: "This email is already being used"} + } else { + return {[FORM_ERROR]: error.toString()} + } + } + }} + > + + + +
+ ) +} + +export default SignupForm diff --git a/examples/auth/app/auth/mutations/changePassword.ts b/examples/auth/app/auth/mutations/changePassword.ts new file mode 100644 index 0000000000..ea9b9a5406 --- /dev/null +++ b/examples/auth/app/auth/mutations/changePassword.ts @@ -0,0 +1,23 @@ +import {NotFoundError, SecurePassword, resolver} from "blitz" +import db from "db" +import {authenticateUser} from "./login" +import {ChangePassword} from "../validations" + +export default resolver.pipe( + resolver.zod(ChangePassword), + resolver.authorize(), + async ({currentPassword, newPassword}, ctx) => { + const user = await db.user.findFirst({where: {id: ctx.session.userId!}}) + if (!user) throw new NotFoundError() + + await authenticateUser(user.email, currentPassword) + + const hashedPassword = await SecurePassword.hash(newPassword) + await db.user.update({ + where: {id: user.id}, + data: {hashedPassword}, + }) + + return true + }, +) diff --git a/examples/auth/app/auth/mutations/forgotPassword.test.ts b/examples/auth/app/auth/mutations/forgotPassword.test.ts new file mode 100644 index 0000000000..bf3c8a2111 --- /dev/null +++ b/examples/auth/app/auth/mutations/forgotPassword.test.ts @@ -0,0 +1,56 @@ +import {hash256, Ctx} from "blitz" +import forgotPassword from "./forgotPassword" +import db from "db" +import previewEmail from "preview-email" + +beforeEach(async () => { + await db.$reset() +}) + +const generatedToken = "plain-token" +jest.mock("next/stdlib-server", () => ({ + ...jest.requireActual("next/stdlib-server")!, + generateToken: () => generatedToken, +})) +jest.mock("preview-email", () => jest.fn()) + +describe("forgotPassword mutation", () => { + it("does not throw error if user doesn't exist", async () => { + await expect(forgotPassword({email: "no-user@email.com"}, {} as Ctx)).resolves.not.toThrow() + }) + + it("works correctly", async () => { + // Create test user + const user = await db.user.create({ + data: { + email: "user@example.com", + tokens: { + // Create old token to ensure it's deleted + create: { + type: "RESET_PASSWORD", + hashedToken: "token", + expiresAt: new Date(), + sentTo: "user@example.com", + }, + }, + }, + include: {tokens: true}, + }) + + // Invoke the mutation + await forgotPassword({email: user.email}, {} as Ctx) + + const tokens = await db.token.findMany({where: {userId: user.id}}) + const token = tokens[0] + + // delete's existing tokens + expect(tokens.length).toBe(1) + + expect(token.id).not.toBe(user.tokens[0].id) + expect(token.type).toBe("RESET_PASSWORD") + expect(token.sentTo).toBe(user.email) + expect(token.hashedToken).toBe(hash256(generatedToken)) + expect(token.expiresAt > new Date()).toBe(true) + expect(previewEmail).toBeCalled() + }) +}) diff --git a/examples/auth/app/auth/mutations/forgotPassword.ts b/examples/auth/app/auth/mutations/forgotPassword.ts new file mode 100644 index 0000000000..a8b346d93f --- /dev/null +++ b/examples/auth/app/auth/mutations/forgotPassword.ts @@ -0,0 +1,41 @@ +import {resolver, generateToken, hash256} from "blitz" +import db from "db" +import {forgotPasswordMailer} from "mailers/forgotPasswordMailer" +import {ForgotPassword} from "../validations" + +const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4 + +export default resolver.pipe(resolver.zod(ForgotPassword), async ({email}) => { + // 1. Get the user + const user = await db.user.findFirst({where: {email: email.toLowerCase()}}) + + // 2. Generate the token and expiration date. + const token = generateToken() + const hashedToken = hash256(token) + const expiresAt = new Date() + expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS) + + // 3. If user with this email was found + if (user) { + // 4. Delete any existing password reset tokens + await db.token.deleteMany({where: {type: "RESET_PASSWORD", userId: user.id}}) + // 5. Save this new token in the database. + await db.token.create({ + data: { + user: {connect: {id: user.id}}, + type: "RESET_PASSWORD", + expiresAt, + hashedToken, + sentTo: user.email, + }, + }) + // 6. Send the email + await forgotPasswordMailer({to: user.email, token}).send() + } else { + // 7. If no user found wait the same time so attackers can't tell the difference + await new Promise((resolve) => setTimeout(resolve, 750)) + } + + // 8. Return the same result whether a password reset email was sent or not + return +}) diff --git a/examples/auth/app/auth/mutations/login.ts b/examples/auth/app/auth/mutations/login.ts new file mode 100644 index 0000000000..f59f8f7483 --- /dev/null +++ b/examples/auth/app/auth/mutations/login.ts @@ -0,0 +1,37 @@ +import {resolver, SecurePassword, AuthenticationError} from "blitz" +import db from "db" +import {Login} from "../validations" +import {Role} from "types" + +export const authenticateUser = async (email: string, password: string) => { + const user = await db.user.findFirst({where: {email}}) + if (!user) throw new AuthenticationError() + + const result = await SecurePassword.verify(user.hashedPassword, password) + + if (result === SecurePassword.VALID_NEEDS_REHASH) { + // Upgrade hashed password with a more secure hash + const improvedHash = await SecurePassword.hash(password) + await db.user.update({where: {id: user.id}, data: {hashedPassword: improvedHash}}) + } + + const {hashedPassword, ...rest} = user + return rest +} + +export default resolver.pipe(resolver.zod(Login), async ({email, password}, ctx) => { + // This throws an error if credentials are invalid + const user = await authenticateUser(email, password) + + await ctx.session.$create({userId: user.id, role: user.role as Role}) + + return user +}) + +export const config = { + api: { + bodyParser: { + sizeLimit: "2mb", + }, + }, +} diff --git a/examples/auth/app/auth/mutations/logout.ts b/examples/auth/app/auth/mutations/logout.ts new file mode 100644 index 0000000000..ca7e6de856 --- /dev/null +++ b/examples/auth/app/auth/mutations/logout.ts @@ -0,0 +1,5 @@ +import {Ctx} from "blitz" + +export default async function logout(_: any, ctx: Ctx) { + return await ctx.session.$revoke() +} diff --git a/examples/auth/app/auth/mutations/resetPassword.test.ts b/examples/auth/app/auth/mutations/resetPassword.test.ts new file mode 100644 index 0000000000..64727ac05d --- /dev/null +++ b/examples/auth/app/auth/mutations/resetPassword.test.ts @@ -0,0 +1,82 @@ +import resetPassword from "./resetPassword" +import db from "db" +import {hash256, SecurePassword} from "blitz" + +beforeEach(async () => { + await db.$reset() +}) + +const mockCtx: any = { + session: { + $create: jest.fn, + }, +} + +describe("resetPassword mutation", () => { + it("works correctly", async () => { + expect(true).toBe(true) + + // Create test user + const goodToken = "randomPasswordResetToken" + const expiredToken = "expiredRandomPasswordResetToken" + const future = new Date() + future.setHours(future.getHours() + 4) + const past = new Date() + past.setHours(past.getHours() - 4) + + const user = await db.user.create({ + data: { + email: "user@example.com", + tokens: { + // Create old token to ensure it's deleted + create: [ + { + type: "RESET_PASSWORD", + hashedToken: hash256(expiredToken), + expiresAt: past, + sentTo: "user@example.com", + }, + { + type: "RESET_PASSWORD", + hashedToken: hash256(goodToken), + expiresAt: future, + sentTo: "user@example.com", + }, + ], + }, + }, + include: {tokens: true}, + }) + + const newPassword = "newPassword" + + // Non-existent token + await expect( + resetPassword({token: "no-token", password: "", passwordConfirmation: ""}, mockCtx), + ).rejects.toThrowError() + + // Expired token + await expect( + resetPassword( + {token: expiredToken, password: newPassword, passwordConfirmation: newPassword}, + mockCtx, + ), + ).rejects.toThrowError() + + // Good token + await resetPassword( + {token: goodToken, password: newPassword, passwordConfirmation: newPassword}, + mockCtx, + ) + + // Delete's the token + const numberOfTokens = await db.token.count({where: {userId: user.id}}) + expect(numberOfTokens).toBe(0) + + // Updates user's password + const updatedUser = await db.user.findFirst({where: {id: user.id}}) + expect(await SecurePassword.verify(updatedUser!.hashedPassword, newPassword)).toBe( + SecurePassword.VALID, + ) + }) +}) diff --git a/examples/auth/app/auth/mutations/resetPassword.ts b/examples/auth/app/auth/mutations/resetPassword.ts new file mode 100644 index 0000000000..4fd11a263d --- /dev/null +++ b/examples/auth/app/auth/mutations/resetPassword.ts @@ -0,0 +1,47 @@ +import {resolver, SecurePassword, hash256} from "blitz" +import db from "db" +import {ResetPassword} from "../validations" +import login from "./login" + +export class ResetPasswordError extends Error { + name = "ResetPasswordError" + message = "Reset password link is invalid or it has expired." +} + +export default resolver.pipe(resolver.zod(ResetPassword), async ({password, token}, ctx) => { + // 1. Try to find this token in the database + const hashedToken = hash256(token) + const possibleToken = await db.token.findFirst({ + where: {hashedToken, type: "RESET_PASSWORD"}, + include: {user: true}, + }) + + // 2. If token not found, error + if (!possibleToken) { + throw new ResetPasswordError() + } + const savedToken = possibleToken + + // 3. Delete token so it can't be used again + await db.token.delete({where: {id: savedToken.id}}) + + // 4. If token has expired, error + if (savedToken.expiresAt < new Date()) { + throw new ResetPasswordError() + } + + // 5. Since token is valid, now we can update the user's password + const hashedPassword = await SecurePassword.hash(password) + const user = await db.user.update({ + where: {id: savedToken.userId}, + data: {hashedPassword}, + }) + + // 6. Revoke all existing login sessions for this user + await db.session.deleteMany({where: {userId: user.id}}) + + // 7. Now log the user in with the new credentials + await login({email: user.email, password}, ctx) + + return true +}) diff --git a/examples/auth/app/auth/mutations/signup.ts b/examples/auth/app/auth/mutations/signup.ts new file mode 100644 index 0000000000..98b0b787cd --- /dev/null +++ b/examples/auth/app/auth/mutations/signup.ts @@ -0,0 +1,15 @@ +import {resolver, SecurePassword} from "blitz" +import db from "db" +import {Signup} from "app/auth/validations" +import {Role} from "types" + +export default resolver.pipe(resolver.zod(Signup), async ({email, password}, ctx) => { + const hashedPassword = await SecurePassword.hash(password) + const user = await db.user.create({ + data: {email: email.toLowerCase(), hashedPassword, role: "user"}, + select: {id: true, name: true, email: true, role: true}, + }) + + await ctx.session.$create({userId: user.id, role: user.role as Role}) + return user +}) diff --git a/examples/auth/app/auth/pages/forgot-password.tsx b/examples/auth/app/auth/pages/forgot-password.tsx new file mode 100644 index 0000000000..e871c48845 --- /dev/null +++ b/examples/auth/app/auth/pages/forgot-password.tsx @@ -0,0 +1,47 @@ +import {BlitzPage, useMutation} from "blitz" +import Layout from "app/core/layouts/Layout" +import {LabeledTextField} from "app/core/components/LabeledTextField" +import {Form, FORM_ERROR} from "app/core/components/Form" +import {ForgotPassword} from "app/auth/validations" +import forgotPassword from "app/auth/mutations/forgotPassword" + +const ForgotPasswordPage: BlitzPage = () => { + const [forgotPasswordMutation, {isSuccess}] = useMutation(forgotPassword) + + return ( +
+

Forgot your password?

+ + {isSuccess ? ( +
+

Request Submitted

+

+ If your email is in our system, you will receive instructions to reset your password + shortly. +

+
+ ) : ( +
{ + try { + await forgotPasswordMutation(values) + } catch (error) { + return { + [FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.", + } + } + }} + > + + + )} +
+ ) +} + +ForgotPasswordPage.getLayout = (page) => {page} + +export default ForgotPasswordPage diff --git a/examples/auth/app/auth/pages/login.tsx b/examples/auth/app/auth/pages/login.tsx new file mode 100644 index 0000000000..1029c569d4 --- /dev/null +++ b/examples/auth/app/auth/pages/login.tsx @@ -0,0 +1,25 @@ +import React from "react" +import {useRouter, BlitzPage} from "blitz" +import Layout from "app/core/layouts/Layout" +import {LoginForm} from "app/auth/components/LoginForm" +import {Routes} from ".blitz" + +const LoginPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ { + const next = router.query.next ? decodeURIComponent(router.query.next as string) : "/" + router.push(next) + }} + /> +
+ ) +} + +LoginPage.redirectAuthenticatedTo = Routes.Home().pathname +LoginPage.getLayout = (page) => {page} + +export default LoginPage diff --git a/examples/auth/app/auth/pages/reset-password.tsx b/examples/auth/app/auth/pages/reset-password.tsx new file mode 100644 index 0000000000..85fcd4095d --- /dev/null +++ b/examples/auth/app/auth/pages/reset-password.tsx @@ -0,0 +1,60 @@ +import {BlitzPage, useRouterQuery, Link, useMutation} from "blitz" +import Layout from "app/core/layouts/Layout" +import {LabeledTextField} from "app/core/components/LabeledTextField" +import {Form, FORM_ERROR} from "app/core/components/Form" +import {ResetPassword} from "app/auth/validations" +import resetPassword from "app/auth/mutations/resetPassword" + +import {Routes} from ".blitz" + +const ResetPasswordPage: BlitzPage = () => { + const query = useRouterQuery() + const [resetPasswordMutation, {isSuccess}] = useMutation(resetPassword) + + return ( +
+

Set a New Password

+ + {isSuccess ? ( +
+

Password Reset Successfully

+

+ Go to the homepage +

+
+ ) : ( +
{ + try { + await resetPasswordMutation(values) + } catch (error: any) { + if (error.name === "ResetPasswordError") { + return { + [FORM_ERROR]: error.message, + } + } else { + return { + [FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.", + } + } + } + }} + > + + + + )} +
+ ) +} + +ResetPasswordPage.getLayout = (page) => {page} + +export default ResetPasswordPage diff --git a/examples/auth/app/auth/pages/signup.tsx b/examples/auth/app/auth/pages/signup.tsx new file mode 100644 index 0000000000..c8432e61e6 --- /dev/null +++ b/examples/auth/app/auth/pages/signup.tsx @@ -0,0 +1,17 @@ +import {useRouter, BlitzPage} from "blitz" +import Layout from "app/core/layouts/Layout" +import {SignupForm} from "app/auth/components/SignupForm" + +const SignupPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ router.push("/")} /> +
+ ) +} + +SignupPage.getLayout = (page) => {page} + +export default SignupPage diff --git a/examples/auth/app/auth/validations.ts b/examples/auth/app/auth/validations.ts new file mode 100644 index 0000000000..a017ee0059 --- /dev/null +++ b/examples/auth/app/auth/validations.ts @@ -0,0 +1,33 @@ +import {z} from "zod" + +const password = z.string().min(10).max(100) + +export const Signup = z.object({ + email: z.string().email(), + password, +}) + +export const Login = z.object({ + email: z.string().email(), + password: z.string(), +}) + +export const ForgotPassword = z.object({ + email: z.string().email(), +}) + +export const ResetPassword = z + .object({ + password: password, + passwordConfirmation: password, + token: z.string(), + }) + .refine((data) => data.password === data.passwordConfirmation, { + message: "Passwords don't match", + path: ["passwordConfirmation"], // set the path of the error + }) + +export const ChangePassword = z.object({ + currentPassword: z.string(), + newPassword: password, +}) diff --git a/examples/auth/app/core/components/.keep b/examples/auth/app/core/components/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/auth/app/core/components/Form.tsx b/examples/auth/app/core/components/Form.tsx new file mode 100644 index 0000000000..d8313f9e8d --- /dev/null +++ b/examples/auth/app/core/components/Form.tsx @@ -0,0 +1,59 @@ +import React, {ReactNode, PropsWithoutRef} from "react" +import {Form as FinalForm, FormProps as FinalFormProps} from "react-final-form" +import {z} from "zod" +import {validateZodSchema} from "blitz" +export {FORM_ERROR} from "final-form" + +export interface FormProps> + extends Omit, "onSubmit"> { + /** All your form fields */ + children?: ReactNode + /** Text to display in the submit button */ + submitText?: string + schema?: S + onSubmit: FinalFormProps>["onSubmit"] + initialValues?: FinalFormProps>["initialValues"] +} + +export function Form>({ + children, + submitText, + schema, + initialValues, + onSubmit, + ...props +}: FormProps) { + return ( + ( +
+ {/* Form fields supplied as children are rendered here */} + {children} + + {submitError && ( +
+ {submitError} +
+ )} + + {submitText && ( + + )} + + +
+ )} + /> + ) +} + +export default Form diff --git a/examples/auth/app/core/components/LabeledTextField.tsx b/examples/auth/app/core/components/LabeledTextField.tsx new file mode 100644 index 0000000000..25b03bc5fc --- /dev/null +++ b/examples/auth/app/core/components/LabeledTextField.tsx @@ -0,0 +1,59 @@ +import React, {PropsWithoutRef} from "react" +import {useField} from "react-final-form" + +export interface LabeledTextFieldProps extends PropsWithoutRef { + /** Field name. */ + name: string + /** Field label. */ + label: string + /** Field type. Doesn't include radio buttons and checkboxes */ + type?: "text" | "password" | "email" | "number" + outerProps?: PropsWithoutRef +} + +export const LabeledTextField = React.forwardRef( + ({name, label, outerProps, ...props}, ref) => { + const { + input, + meta: {touched, error, submitError, submitting}, + } = useField(name, { + parse: props.type === "number" ? Number : undefined, + }) + + const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError + + return ( +
+ + + {touched && normalizedError && ( +
+ {normalizedError} +
+ )} + + +
+ ) + }, +) + +export default LabeledTextField diff --git a/examples/auth/app/core/hooks/useCurrentUser.ts b/examples/auth/app/core/hooks/useCurrentUser.ts new file mode 100644 index 0000000000..031ec68457 --- /dev/null +++ b/examples/auth/app/core/hooks/useCurrentUser.ts @@ -0,0 +1,7 @@ +import {useQuery} from "blitz" +import getCurrentUser from "app/users/queries/getCurrentUser" + +export const useCurrentUser = () => { + const [user] = useQuery(getCurrentUser, null) + return user +} diff --git a/examples/auth/app/core/layouts/.keep b/examples/auth/app/core/layouts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/auth/app/core/layouts/Layout.tsx b/examples/auth/app/core/layouts/Layout.tsx new file mode 100644 index 0000000000..15b83fc1a1 --- /dev/null +++ b/examples/auth/app/core/layouts/Layout.tsx @@ -0,0 +1,32 @@ +import {useSession, useRouter, useMutation, Head, BlitzLayout} from "blitz" +import logout from "app/auth/mutations/logout" + +const Layout: BlitzLayout<{title?: string}> = ({title, children}) => { + const session = useSession({suspense: false}) + const router = useRouter() + const [logoutMutation] = useMutation(logout) + return ( + <> + + {title || "__name__"} + + + +
+ {session.userId && ( + + )} +
{children}
+
+ + ) +} + +export default Layout diff --git a/examples/auth/app/pages/404.tsx b/examples/auth/app/pages/404.tsx new file mode 100644 index 0000000000..84c795f017 --- /dev/null +++ b/examples/auth/app/pages/404.tsx @@ -0,0 +1,19 @@ +import {Head, ErrorComponent} from "blitz" + +// ------------------------------------------------------ +// This page is rendered if a route match is not found +// ------------------------------------------------------ +export default function Page404() { + const statusCode = 404 + const title = "This page could not be found" + return ( + <> + + + {statusCode}: {title} + + + + + ) +} diff --git a/examples/auth/app/pages/_app.tsx b/examples/auth/app/pages/_app.tsx new file mode 100644 index 0000000000..a4c6187ad8 --- /dev/null +++ b/examples/auth/app/pages/_app.tsx @@ -0,0 +1,45 @@ +import { + AppProps, + ErrorBoundary, + ErrorComponent, + AuthenticationError, + AuthorizationError, + ErrorFallbackProps, + useQueryErrorResetBoundary, +} from "blitz" +import LoginForm from "app/auth/components/LoginForm" +import {ReactQueryDevtools} from "react-query/devtools" + +export default function App({Component, pageProps}: AppProps) { + const getLayout = Component.getLayout || ((page) => page) + const {reset} = useQueryErrorResetBoundary() + + return ( + <> + + {getLayout()} + + + + ) +} + +function RootErrorFallback({error, resetErrorBoundary}: ErrorFallbackProps) { + if (error instanceof AuthenticationError) { + return + } else if (error instanceof AuthorizationError) { + return ( + + ) + } else { + return ( + + ) + } +} diff --git a/examples/auth/app/pages/_document.tsx b/examples/auth/app/pages/_document.tsx new file mode 100644 index 0000000000..788eb1febe --- /dev/null +++ b/examples/auth/app/pages/_document.tsx @@ -0,0 +1,23 @@ +import {Document, Html, DocumentHead, Main, BlitzScript /*DocumentContext*/} from "blitz" + +class MyDocument extends Document { + // Only uncomment if you need to customize this behaviour + // static async getInitialProps(ctx: DocumentContext) { + // const initialProps = await Document.getInitialProps(ctx) + // return {...initialProps} + // } + + render() { + return ( + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/auth/app/pages/index.test.tsx b/examples/auth/app/pages/index.test.tsx new file mode 100644 index 0000000000..596401407a --- /dev/null +++ b/examples/auth/app/pages/index.test.tsx @@ -0,0 +1,27 @@ +import {render} from "test/utils" +import Home from "./index" + +jest.mock("next/data-client", () => ({ + ...jest.requireActual("next/data-client")!, + useQuery: () => [ + { + id: 1, + name: "User", + email: "user@email.com", + role: "user", + }, + ], +})) + +test("renders blitz documentation link", () => { + // This is an example of how to ensure a specific item is in the document + // But it's disabled by default (by test.skip) so the test doesn't fail + // when you remove the the default content from the page + + // This is an example on how to mock api hooks when testing + + const {getByText} = render() + const element = getByText(/powered by blitz/i) + // @ts-ignore + expect(element).toBeInTheDocument() +}) diff --git a/examples/auth/app/pages/index.tsx b/examples/auth/app/pages/index.tsx new file mode 100644 index 0000000000..7cb5ae7ecf --- /dev/null +++ b/examples/auth/app/pages/index.tsx @@ -0,0 +1,244 @@ +import {Suspense} from "react" +import { + Head, + Link, + useSession, + useRouterQuery, + useMutation, + invoke, + Image, + useQuery, + BlitzPage, +} from "blitz" +import getUser from "app/users/queries/getUser" +import trackView from "app/users/mutations/trackView" +import Layout from "app/core/layouts/Layout" +import logo from "public/logo.png" + +import {Routes} from ".blitz" + +const CurrentUserInfo = () => { + const session = useSession() + const [currentUser] = useQuery(getUser, {where: {id: session.userId!}}) + + return
{JSON.stringify(currentUser, null, 2)}
+} + +const UserStuff = () => { + const session = useSession() + const query = useRouterQuery() + const [trackViewMutation] = useMutation(trackView) + + return ( +
+ {!session.userId && ( + <> +
+ Sign Up +
+
+ Login +
+ + Login with Twitter + + + Login with Github + + {query.authError &&
{query.authError}
} + + )} +
{JSON.stringify(session, null, 2)}
+ + + + + +
+ ) +} + +const Home: BlitzPage = () => ( + +
+ + auth + + + +
+
+ blitz.js +
+ + + + +
+ + + + + + +
+
+) + +Home.suppressFirstRenderFlicker = true + +export default Home diff --git a/examples/auth/app/pages/projects/[projectId].tsx b/examples/auth/app/pages/projects/[projectId].tsx new file mode 100644 index 0000000000..6f920988cd --- /dev/null +++ b/examples/auth/app/pages/projects/[projectId].tsx @@ -0,0 +1,67 @@ +import {Suspense} from "react" +import {Head, Link, useRouter, useQuery, useParam, BlitzPage, useMutation} from "blitz" +import Layout from "app/core/layouts/Layout" +import getProject from "app/projects/queries/getProject" +import deleteProject from "app/projects/mutations/deleteProject" + +import {Routes} from ".blitz" + +export const Project = () => { + const router = useRouter() + const projectId = useParam("projectId", "number") + const [deleteProjectMutation, {isSuccess}] = useMutation(deleteProject) + const [project] = useQuery(getProject, {id: projectId}, {enabled: !isSuccess}) + + if (!project) return null + + return ( + <> + + Project {project.id} + + +
+

Project {project.id}

+
{JSON.stringify(project, null, 2)}
+ + + Edit + + + +
+ + ) +} + +const ShowProjectPage: BlitzPage = () => { + return ( +
+

+ + Projects + +

+ + Loading...
}> + + + + ) +} + +ShowProjectPage.authenticate = true +ShowProjectPage.getLayout = (page) => {page} + +export default ShowProjectPage diff --git a/examples/auth/app/pages/projects/[projectId]/edit.tsx b/examples/auth/app/pages/projects/[projectId]/edit.tsx new file mode 100644 index 0000000000..d45b8ccdc3 --- /dev/null +++ b/examples/auth/app/pages/projects/[projectId]/edit.tsx @@ -0,0 +1,80 @@ +import {Suspense} from "react" +import {Head, Link, useRouter, useQuery, useMutation, useParam, BlitzPage} from "blitz" +import Layout from "app/core/layouts/Layout" +import getProject from "app/projects/queries/getProject" +import updateProject from "app/projects/mutations/updateProject" +import {ProjectForm, FORM_ERROR} from "app/projects/components/ProjectForm" + +import {Routes} from ".blitz" + +export const EditProject = () => { + const router = useRouter() + const projectId = useParam("projectId", "number") + const [project, {setQueryData}] = useQuery( + getProject, + {id: projectId}, + { + // This ensures the query never refreshes and overwrites the form data while the user is editing. + staleTime: Infinity, + }, + ) + const [updateProjectMutation] = useMutation(updateProject) + + return ( + <> + + Edit Project {project.id} + + +
+

Edit Project {project.id}

+
{JSON.stringify(project)}
+ + { + try { + const updated = await updateProjectMutation({ + id: project.id, + ...values, + }) + await setQueryData(updated) + router.push(`/projects/${updated.id}`) + } catch (error: any) { + console.error(error) + return { + [FORM_ERROR]: error.toString(), + } + } + }} + /> +
+ + ) +} + +const EditProjectPage: BlitzPage = () => { + return ( +
+ Loading...
}> + + + +

+ + Projects + +

+ + ) +} + +EditProjectPage.authenticate = true +EditProjectPage.getLayout = (page) => {page} + +export default EditProjectPage diff --git a/examples/auth/app/pages/projects/index.tsx b/examples/auth/app/pages/projects/index.tsx new file mode 100644 index 0000000000..634c520a54 --- /dev/null +++ b/examples/auth/app/pages/projects/index.tsx @@ -0,0 +1,69 @@ +import {Suspense} from "react" +import {Head, Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz" +import Layout from "app/core/layouts/Layout" +import getProjects from "app/projects/queries/getProjects" + +import {Routes} from ".blitz" + +const ITEMS_PER_PAGE = 100 + +export const ProjectsList = () => { + const router = useRouter() + const page = Number(router.query.page) || 0 + const [{projects, hasMore}] = usePaginatedQuery(getProjects, { + orderBy: {id: "asc"}, + skip: ITEMS_PER_PAGE * page, + take: ITEMS_PER_PAGE, + }) + + const goToPreviousPage = () => router.push({query: {page: page - 1}}) + const goToNextPage = () => router.push({query: {page: page + 1}}) + + return ( +
+ + + + +
+ ) +} + +const ProjectsPage: BlitzPage = () => { + return ( + <> + + Projects + + +
+

+ + Create Project + +

+ + Loading...
}> + + + + + ) +} + +ProjectsPage.authenticate = {redirectTo: "/login"} +ProjectsPage.getLayout = (page) => {page} + +export default ProjectsPage diff --git a/examples/auth/app/pages/projects/new.tsx b/examples/auth/app/pages/projects/new.tsx new file mode 100644 index 0000000000..9e54c03eb8 --- /dev/null +++ b/examples/auth/app/pages/projects/new.tsx @@ -0,0 +1,48 @@ +import {Link, useRouter, useMutation, BlitzPage} from "blitz" +import Layout from "app/core/layouts/Layout" +import createProject from "app/projects/mutations/createProject" +import {ProjectForm, FORM_ERROR} from "app/projects/components/ProjectForm" + +import {Routes} from ".blitz" + +const NewProjectPage: BlitzPage = () => { + const router = useRouter() + const [createProjectMutation] = useMutation(createProject) + + return ( +
+

Create New Project

+ + { + try { + const project = await createProjectMutation(values) + router.push(Routes.ShowProjectPage({projectId: project.id})) + } catch (error: any) { + console.error(error) + return { + [FORM_ERROR]: error.toString(), + } + } + }} + /> + +

+ + Projects + +

+
+ ) +} + +NewProjectPage.authenticate = true +NewProjectPage.getLayout = (page) => {page} + +export default NewProjectPage diff --git a/examples/auth/app/pages/ssr.tsx b/examples/auth/app/pages/ssr.tsx new file mode 100644 index 0000000000..3b99b304a6 --- /dev/null +++ b/examples/auth/app/pages/ssr.tsx @@ -0,0 +1,75 @@ +import { + getSession, + invokeWithMiddleware, + useRouter, + ErrorComponent as ErrorPage, + useMutation, + AuthenticationError, + AuthorizationError, + GetServerSideProps, + InferGetServerSidePropsType, + BlitzPage, +} from "blitz" +import getUser from "app/users/queries/getUser" +import logout from "app/auth/mutations/logout" + +export const getServerSideProps: GetServerSideProps = async ({req, res}) => { + const session = await getSession(req, res) + console.log("Session id:", session.userId) + try { + const user = await invokeWithMiddleware( + getUser, + {where: {id: Number(session.userId)}}, + {res, req}, + ) + return {props: {user}} + } catch (error: any) { + if (error.name === "NotFoundError") { + res.statusCode = 404 + res.end() + return {props: {}} + } else if (error instanceof AuthenticationError) { + res.writeHead(302, {location: "/login"}).end() + return {props: {}} + } else if (error instanceof AuthorizationError) { + return { + props: { + error: { + statusCode: error.statusCode, + message: "Sorry, you are not authorized to access this", + }, + }, + } + } else { + return {props: {error: {statusCode: error.statusCode || 500, message: error.message}}} + } + } +} + +const PageSSR: BlitzPage> = ({ + user, + error, +}) => { + const router = useRouter() + const [logoutMutation] = useMutation(logout) + + if (error) { + return + } + + return ( +
+
Logged in user id: {user?.id}
+ +
+ ) +} + +export default PageSSR diff --git a/examples/auth/app/pages/test.tsx b/examples/auth/app/pages/test.tsx new file mode 100644 index 0000000000..760638dfc1 --- /dev/null +++ b/examples/auth/app/pages/test.tsx @@ -0,0 +1,13 @@ +import {useRouter} from "next/router" +import React from "react" + +export default function Test() { + const {replace} = useRouter() + + const handleChange = (event: any) => { + // replace({ query }, undefined, { shallow: true }) + replace(`/test?p=${event.target.value}`) + } + + return +} diff --git a/examples/auth/app/projects/components/ProjectForm.tsx b/examples/auth/app/projects/components/ProjectForm.tsx new file mode 100644 index 0000000000..9102153021 --- /dev/null +++ b/examples/auth/app/projects/components/ProjectForm.tsx @@ -0,0 +1,12 @@ +import {Form, FormProps} from "app/core/components/Form" +import {LabeledTextField} from "app/core/components/LabeledTextField" +import {ZodType} from "zod" +export {FORM_ERROR} from "app/core/components/Form" + +export function ProjectForm>(props: FormProps) { + return ( + {...props}> + + + ) +} diff --git a/examples/auth/app/projects/mutations/createProject.ts b/examples/auth/app/projects/mutations/createProject.ts new file mode 100644 index 0000000000..e76e9761d5 --- /dev/null +++ b/examples/auth/app/projects/mutations/createProject.ts @@ -0,0 +1,14 @@ +import {resolver} from "blitz" +import db from "db" +import {z} from "zod" + +const CreateProject = z.object({ + name: z.string(), +}) + +export default resolver.pipe(resolver.zod(CreateProject), resolver.authorize(), async (input) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const project = await db.project.create({data: input}) + + return project +}) diff --git a/examples/auth/app/projects/mutations/deleteProject.ts b/examples/auth/app/projects/mutations/deleteProject.ts new file mode 100644 index 0000000000..1e8c8316e8 --- /dev/null +++ b/examples/auth/app/projects/mutations/deleteProject.ts @@ -0,0 +1,14 @@ +import {resolver} from "blitz" +import db from "db" +import {z} from "zod" + +const DeleteProject = z.object({ + id: z.number(), +}) + +export default resolver.pipe(resolver.zod(DeleteProject), resolver.authorize(), async ({id}) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const project = await db.project.delete({where: {id}}) + + return project +}) diff --git a/examples/auth/app/projects/mutations/updateProject.ts b/examples/auth/app/projects/mutations/updateProject.ts new file mode 100644 index 0000000000..e25fc54e8c --- /dev/null +++ b/examples/auth/app/projects/mutations/updateProject.ts @@ -0,0 +1,19 @@ +import {resolver} from "blitz" +import db from "db" +import {z} from "zod" + +const UpdateProject = z.object({ + id: z.number(), + name: z.string(), +}) + +export default resolver.pipe( + resolver.zod(UpdateProject), + resolver.authorize(), + async ({id, ...data}) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const project = await db.project.update({where: {id}, data}) + + return project + }, +) diff --git a/examples/auth/app/projects/queries/getProject.ts b/examples/auth/app/projects/queries/getProject.ts new file mode 100644 index 0000000000..02ec3ca28b --- /dev/null +++ b/examples/auth/app/projects/queries/getProject.ts @@ -0,0 +1,17 @@ +import {resolver, NotFoundError} from "blitz" +import db from "db" +import {z} from "zod" + +const GetProject = z.object({ + // This accepts type of undefined, but is required at runtime + id: z.number().optional().refine(Boolean, "Required"), +}) + +export default resolver.pipe(resolver.zod(GetProject), resolver.authorize(), async ({id}) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const project = await db.project.findFirst({where: {id}}) + + if (!project) throw new NotFoundError() + + return project +}) diff --git a/examples/auth/app/projects/queries/getProjects.ts b/examples/auth/app/projects/queries/getProjects.ts new file mode 100644 index 0000000000..2d1285f94f --- /dev/null +++ b/examples/auth/app/projects/queries/getProjects.ts @@ -0,0 +1,25 @@ +import {paginate, resolver} from "blitz" +import db, {Prisma} from "db" + +interface GetProjectsInput + extends Pick {} + +export default resolver.pipe( + resolver.authorize(), + async ({where, orderBy, skip = 0, take = 100}: GetProjectsInput) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const {items: projects, hasMore, nextPage, count} = await paginate({ + skip, + take, + count: () => db.project.count({where}), + query: (paginateArgs) => db.project.findMany({...paginateArgs, where, orderBy}), + }) + + return { + projects, + nextPage, + hasMore, + count, + } + }, +) diff --git a/examples/auth/app/queries/users/currentUserQuery.ts b/examples/auth/app/queries/users/currentUserQuery.ts new file mode 100644 index 0000000000..0da40ea0ab --- /dev/null +++ b/examples/auth/app/queries/users/currentUserQuery.ts @@ -0,0 +1,13 @@ +import {Ctx} from "blitz" +import db from "db" + +export default async function currentUserQuery(_ = null, {session}: Ctx) { + if (!session.userId) return null + + const user = await db.user.findFirst({ + where: {id: session.userId}, + select: {id: true, name: true, email: true, role: true}, + }) + + return user +} diff --git a/examples/auth/app/users/mutations/trackView.ts b/examples/auth/app/users/mutations/trackView.ts new file mode 100644 index 0000000000..4b03190999 --- /dev/null +++ b/examples/auth/app/users/mutations/trackView.ts @@ -0,0 +1,9 @@ +import {Ctx} from "blitz" + +export default async function trackView(_: any, {session}: Ctx) { + const currentViews = session.views || 0 + await session.$setPublicData({views: currentViews + 1}) + await session.$setPrivateData({views: currentViews + 1}) + + return +} diff --git a/examples/auth/app/users/queries/getCurrentUser.ts b/examples/auth/app/users/queries/getCurrentUser.ts new file mode 100644 index 0000000000..e19a903636 --- /dev/null +++ b/examples/auth/app/users/queries/getCurrentUser.ts @@ -0,0 +1,13 @@ +import {Ctx} from "blitz" +import db from "db" + +export default async function getCurrentUser(_ = null, {session}: Ctx) { + if (!session.userId) return null + + const user = await db.user.findFirst({ + where: {id: session.userId}, + select: {id: true, name: true, email: true, role: true}, + }) + + return user +} diff --git a/examples/auth/app/users/queries/getUser.ts b/examples/auth/app/users/queries/getUser.ts new file mode 100644 index 0000000000..070250e0ef --- /dev/null +++ b/examples/auth/app/users/queries/getUser.ts @@ -0,0 +1,18 @@ +import {Ctx, NotFoundError} from "blitz" +import db, {Prisma} from "db" + +type GetUserInput = { + where: Prisma.UserFindFirstArgs["where"] +} + +export default async function getUser({where}: GetUserInput, ctx: Ctx) { + if (!ctx.session.userId) return null + + const user = await db.user.findFirst({where}) + + if (!user) throw new NotFoundError(`User with id ${where?.id} does not exist`) + + const {hashedPassword, ...rest} = user + + return rest +} diff --git a/examples/auth/app/users/queries/getUsers.ts b/examples/auth/app/users/queries/getUsers.ts new file mode 100644 index 0000000000..8bdb7223f6 --- /dev/null +++ b/examples/auth/app/users/queries/getUsers.ts @@ -0,0 +1,26 @@ +import {Ctx} from "blitz" +import db, {Prisma} from "db" + +type GetUsersInput = Pick + +export default async function getUsers({where, orderBy, skip = 0, take}: GetUsersInput, ctx: Ctx) { + ctx.session.$authorize() + + const users = await db.user.findMany({ + where, + orderBy, + take, + skip, + }) + + const count = await db.user.count() + const hasMore = typeof take === "number" ? skip + take < count : false + const nextPage = hasMore ? {take, skip: skip + take!} : null + + return { + users, + nextPage, + hasMore, + count, + } +} diff --git a/examples/auth/babel.config.js b/examples/auth/babel.config.js new file mode 100644 index 0000000000..dfdf62cea1 --- /dev/null +++ b/examples/auth/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["blitz/babel"], + plugins: [], +} diff --git a/examples/auth/blitz.config.ts b/examples/auth/blitz.config.ts new file mode 100644 index 0000000000..5ef9722e48 --- /dev/null +++ b/examples/auth/blitz.config.ts @@ -0,0 +1,43 @@ +import {sessionMiddleware, simpleRolesIsAuthorized} from "blitz" +import db from "db" +const withBundleAnalyzer = require("@next/bundle-analyzer")({ + enabled: process.env.ANALYZE === "true", +}) + +module.exports = withBundleAnalyzer({ + middleware: [ + sessionMiddleware({ + cookiePrefix: "blitz-auth-example", + isAuthorized: simpleRolesIsAuthorized, + // sessionExpiryMinutes: 4, + getSession: (handle) => db.session.findFirst({where: {handle}}), + }), + ], + cli: { + clearConsoleOnBlitzDev: false, + }, + codegen: { + templateDir: "./my-templates", + }, + log: { + // level: "trace", + }, + experimental: { + initServer() { + console.log("Hello world from initServer") + }, + }, + /* + webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + // Important: return the modified config + return config + }, + webpackDevMiddleware: (config) => { + // Perform customizations to webpack dev middleware config + // Important: return the modified config + return config + }, + */ +}) diff --git a/examples/auth/cypress.json b/examples/auth/cypress.json new file mode 100644 index 0000000000..489a43829c --- /dev/null +++ b/examples/auth/cypress.json @@ -0,0 +1,9 @@ +{ + "baseUrl": "http://localhost:3099", + "defaultCommandTimeout": 10000, + "retries": { + "runMode": 1 + }, + "video": false, + "chromeWebSecurity": false +} diff --git a/examples/auth/cypress/fixtures/example.json b/examples/auth/cypress/fixtures/example.json new file mode 100644 index 0000000000..02e4254378 --- /dev/null +++ b/examples/auth/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/examples/auth/cypress/index.d.ts b/examples/auth/cypress/index.d.ts new file mode 100644 index 0000000000..13c1ec5189 --- /dev/null +++ b/examples/auth/cypress/index.d.ts @@ -0,0 +1,8 @@ +/// +/// + +declare namespace Cypress { + interface Chainable { + signup(user: {email: string; password: string}): void + } +} diff --git a/examples/auth/cypress/integration/index.test.ts b/examples/auth/cypress/integration/index.test.ts new file mode 100644 index 0000000000..9fec8561bc --- /dev/null +++ b/examples/auth/cypress/integration/index.test.ts @@ -0,0 +1,80 @@ +import {createRandomUser} from "../support/helpers" + +describe("index page", () => { + beforeEach(() => { + cy.visit("/") + }) + + it("goes to the signup page", () => { + cy.contains("a", "Sign Up").click() + cy.location("pathname").should("equal", "/signup") + }) + + it("goes to the login page", () => { + cy.contains("a", /login/i).click() + cy.location("pathname").should("equal", "/login") + }) + + it("allows the user to signup", () => { + const user = createRandomUser() + + cy.signup(user) + cy.wait(1000) + + cy.location("pathname").should("equal", "/") + cy.contains("button", "Logout") + cy.wait(1000) + }) + + it("allows the user to log in", () => { + const user = createRandomUser() + + cy.signup(user) + cy.wait(1000) + + cy.contains("button", "Logout").click() + cy.wait(1000) + cy.contains("a", /login/i).click() + + cy.contains("Email").find("input").type(user.email) + cy.contains("Password").find("input").type(user.password) + cy.contains("button", /login/i).click() + + cy.location("pathname").should("equal", "/") + cy.wait(1000) + cy.contains("button", "Logout") + cy.wait(1000) + }) + + it("allows the user to logout", () => { + const user = createRandomUser() + + cy.signup(user) + cy.wait(1000) + + cy.contains("button", "Logout").click() + + cy.location("pathname").should("equal", "/") + cy.wait(1000) + cy.contains("a", /login/i) + }) + + it("tracks anonymous sessions", () => { + // TODO - why does this fail on windows?? + cy.skipOn("windows") + const user = createRandomUser() + + cy.contains("button", "Track view").click() + cy.wait(500) + cy.contains("button", "Track view").click() + cy.wait(1000) + cy.contains('"views": 2') + + cy.signup(user) + cy.wait(1000) + + cy.contains('"views": 2') + }) +}) + +export {} diff --git a/examples/auth/cypress/plugins/index.ts b/examples/auth/cypress/plugins/index.ts new file mode 100644 index 0000000000..326165510e --- /dev/null +++ b/examples/auth/cypress/plugins/index.ts @@ -0,0 +1,22 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +//@ts-ignore +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/examples/auth/cypress/support/commands.ts b/examples/auth/cypress/support/commands.ts new file mode 100644 index 0000000000..2727ed2b84 --- /dev/null +++ b/examples/auth/cypress/support/commands.ts @@ -0,0 +1,19 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// + +Cypress.Commands.add("signup", ({email, password}) => { + cy.contains("a", "Sign Up").click() + + cy.contains("Email").find("input").type(email) + cy.contains("Password").find("input").type(password) + cy.contains("button", "Create Account").click() +}) diff --git a/examples/auth/cypress/support/helpers.ts b/examples/auth/cypress/support/helpers.ts new file mode 100644 index 0000000000..f6b07ed118 --- /dev/null +++ b/examples/auth/cypress/support/helpers.ts @@ -0,0 +1,7 @@ +export const createRandomUser = () => { + const random = Math.round(Math.random() * 100000).toString() + const email = `test_${random}@example.com` + const password = `password_${random}` + + return {email, password} +} diff --git a/examples/auth/cypress/support/index.ts b/examples/auth/cypress/support/index.ts new file mode 100644 index 0000000000..e272080237 --- /dev/null +++ b/examples/auth/cypress/support/index.ts @@ -0,0 +1,26 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import "./commands" + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +Cypress.Screenshot.defaults({ + screenshotOnRunFailure: false, +}) + +require("@cypress/skip-test/support") diff --git a/examples/auth/cypress/tsconfig.json b/examples/auth/cypress/tsconfig.json new file mode 100644 index 0000000000..b47d597f30 --- /dev/null +++ b/examples/auth/cypress/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "es5"], + "types": ["cypress"] + } +} diff --git a/examples/auth/db/index.ts b/examples/auth/db/index.ts new file mode 100644 index 0000000000..cde0794c22 --- /dev/null +++ b/examples/auth/db/index.ts @@ -0,0 +1,7 @@ +import {enhancePrisma} from "blitz" +import {PrismaClient} from "@prisma/client" + +const EnhancedPrisma = enhancePrisma(PrismaClient) + +export * from "@prisma/client" +export default new EnhancedPrisma() diff --git a/examples/auth/db/migrations/.keep b/examples/auth/db/migrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/auth/db/migrations/20201214220426_initial_migration/migration.sql b/examples/auth/db/migrations/20201214220426_initial_migration/migration.sql new file mode 100644 index 0000000000..3258a3324d --- /dev/null +++ b/examples/auth/db/migrations/20201214220426_initial_migration/migration.sql @@ -0,0 +1,32 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "name" TEXT, + "email" TEXT NOT NULL, + "hashedPassword" TEXT, + "role" TEXT NOT NULL DEFAULT 'user' +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "expiresAt" DATETIME, + "handle" TEXT NOT NULL, + "userId" INTEGER, + "hashedSessionToken" TEXT, + "antiCSRFToken" TEXT, + "publicData" TEXT, + "privateData" TEXT, + + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session.handle_unique" ON "Session"("handle"); diff --git a/examples/auth/db/migrations/20210123205208_add_projects/migration.sql b/examples/auth/db/migrations/20210123205208_add_projects/migration.sql new file mode 100644 index 0000000000..1dcf09cd06 --- /dev/null +++ b/examples/auth/db/migrations/20210123205208_add_projects/migration.sql @@ -0,0 +1,7 @@ +-- CreateTable +CREATE TABLE "Project" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "name" TEXT NOT NULL +); diff --git a/examples/auth/db/migrations/20210126232902_add_project_due_date/migration.sql b/examples/auth/db/migrations/20210126232902_add_project_due_date/migration.sql new file mode 100644 index 0000000000..c312f4e87b --- /dev/null +++ b/examples/auth/db/migrations/20210126232902_add_project_due_date/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "dueDate" DATETIME; diff --git a/examples/auth/db/migrations/20210204235014_add_tokens/migration.sql b/examples/auth/db/migrations/20210204235014_add_tokens/migration.sql new file mode 100644 index 0000000000..336442d75d --- /dev/null +++ b/examples/auth/db/migrations/20210204235014_add_tokens/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "Token" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "hashedToken" TEXT NOT NULL, + "type" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + "sentTo" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Token.hashedToken_type_unique" ON "Token"("hashedToken", "type"); diff --git a/examples/auth/db/migrations/migration_lock.toml b/examples/auth/db/migrations/migration_lock.toml new file mode 100644 index 0000000000..963afcc9a1 --- /dev/null +++ b/examples/auth/db/migrations/migration_lock.toml @@ -0,0 +1,2 @@ +# Please do not edit this file manually +provider = "sqlite" \ No newline at end of file diff --git a/examples/auth/db/schema.prisma b/examples/auth/db/schema.prisma new file mode 100644 index 0000000000..2addf1dd3d --- /dev/null +++ b/examples/auth/db/schema.prisma @@ -0,0 +1,70 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource sqlite { + provider = "sqlite" + url = "file:./db.sqlite" +} + +// SQLite is easy to start with, but if you use Postgres in production +// you should also use it in development with the following: +//datasource postgresql { +// provider = "postgresql" +// url = env("DATABASE_URL") +//} + +generator client { + provider = "prisma-client-js" +} + +// -------------------------------------- + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String? + email String @unique + hashedPassword String? + role String @default("user") + + sessions Session[] + tokens Token[] +} + +model Session { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + expiresAt DateTime? + handle String @unique + user User? @relation(fields: [userId], references: [id]) + userId Int? + hashedSessionToken String? + antiCSRFToken String? + publicData String? + privateData String? +} + +model Token { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + hashedToken String + type String + expiresAt DateTime + sentTo String + + user User @relation(fields: [userId], references: [id]) + userId Int + + @@unique([hashedToken, type]) +} + +model Project { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String + dueDate DateTime? +} diff --git a/examples/auth/db/seeds.ts b/examples/auth/db/seeds.ts new file mode 100644 index 0000000000..9c6eb559fa --- /dev/null +++ b/examples/auth/db/seeds.ts @@ -0,0 +1,6 @@ +import db from "./index" +const seed = async () => { + const user = await db.user.create({data: {name: "FooBar", email: "hey@" + new Date().getTime()}}) + console.log("Created user", user) +} +export default seed diff --git a/examples/auth/integrations/.keep b/examples/auth/integrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/auth/jest.config.js b/examples/auth/jest.config.js new file mode 100644 index 0000000000..297905551f --- /dev/null +++ b/examples/auth/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: "blitz", +} diff --git a/examples/auth/mailers/.keep b/examples/auth/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/auth/mailers/forgotPasswordMailer.ts b/examples/auth/mailers/forgotPasswordMailer.ts new file mode 100644 index 0000000000..9d075e6420 --- /dev/null +++ b/examples/auth/mailers/forgotPasswordMailer.ts @@ -0,0 +1,45 @@ +/* TODO - You need to add a mailer integration in `integrations/` and import here. + * + * The integration file can be very simple. Instantiate the email client + * and then export it. That way you can import here and anywhere else + * and use it straight away. + */ + +type ResetPasswordMailer = { + to: string + token: string +} + +export function forgotPasswordMailer({to, token}: ResetPasswordMailer) { + // In production, set APP_ORIGIN to your production server origin + const origin = process.env.APP_ORIGIN || process.env.BLITZ_DEV_SERVER_ORIGIN + const resetUrl = `${origin}/reset-password?token=${token}` + + const msg = { + from: "TODO@example.com", + to, + subject: "Your Password Reset Instructions", + html: ` +

Reset Your Password

+

NOTE: You must set up a production email integration in mailers/forgotPasswordMailer.ts

+ + + Click here to set a new password + + `, + } + + return { + async send() { + if (process.env.NODE_ENV === "production") { + // TODO - send the production email, like this: + // await postmark.sendEmail(msg) + throw new Error("No production email implementation in mailers/forgotPasswordMailer") + } else { + // Preview email in the browser + const previewEmail = (await import("preview-email")).default + await previewEmail(msg) + } + }, + } +} diff --git a/examples/auth/package.json b/examples/auth/package.json new file mode 100644 index 0000000000..94faeb77b6 --- /dev/null +++ b/examples/auth/package.json @@ -0,0 +1,64 @@ +{ + "name": "@examples/auth", + "version": "0.34.0-canary.0", + "scripts": { + "dev": "blitz dev", + "build": "blitz build", + "start": "blitz start", + "console": "blitz console", + "studio": "blitz prisma studio", + "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", + "analyze": "cross-env ANALYZE=true blitz build", + "cy:open": "cypress open", + "cy:run": "cypress run --browser chrome", + "test": "prisma generate && blitz codegen && yarn test:jest && yarn test:e2e", + "test:jest": "jest", + "test:server": "cross-env NODE_ENV=test blitz prisma migrate deploy && blitz build && cross-env NODE_ENV=test blitz start -p 3099", + "test:e2e": "cross-env NODE_ENV=test start-server-and-test test:server http://localhost:3099 cy:run" + }, + "browserslist": [ + "defaults" + ], + "prisma": { + "schema": "db/schema.prisma" + }, + "prettier": { + "semi": false, + "printWidth": 100, + "bracketSpacing": false, + "trailingComma": "all" + }, + "dependencies": { + "@prisma/client": "2.24.1", + "blitz": "0.45.4", + "final-form": "4.20.1", + "passport-auth0": "1.4.0", + "passport-github2": "0.1.12", + "passport-twitter": "1.0.4", + "prisma": "2.24.1", + "react": "0.0.0-experimental-6a589ad71", + "react-dom": "0.0.0-experimental-6a589ad71", + "react-final-form": "6.5.2" + }, + "devDependencies": { + "@cypress/skip-test": "2.6.0", + "@next/bundle-analyzer": "^10.0.6", + "@testing-library/react": "11.2.5", + "@testing-library/react-hooks": "^4.0.1", + "@types/passport-auth0": "1.0.4", + "@types/passport-github2": "1.2.4", + "@types/passport-twitter": "1.0.36", + "@types/preview-email": "2.0.0", + "@types/react": "17.0.2", + "cross-env": "7.0.3", + "cypress": "6.2.1", + "eslint": "7.21.0", + "husky": "5.1.2", + "lint-staged": "10.5.4", + "prettier": "2.2.1", + "pretty-quick": "3.1.0", + "preview-email": "3.0.3", + "start-server-and-test": "1.11.7" + }, + "private": true +} diff --git a/examples/auth/public/favicon.ico b/examples/auth/public/favicon.ico new file mode 100755 index 0000000000..a94b0f7a7f Binary files /dev/null and b/examples/auth/public/favicon.ico differ diff --git a/examples/auth/public/logo.png b/examples/auth/public/logo.png new file mode 100644 index 0000000000..6a982616ee Binary files /dev/null and b/examples/auth/public/logo.png differ diff --git a/examples/auth/test/.keep b/examples/auth/test/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/auth/test/setup.ts b/examples/auth/test/setup.ts new file mode 100644 index 0000000000..c278ddb9e6 --- /dev/null +++ b/examples/auth/test/setup.ts @@ -0,0 +1,4 @@ +// This is the jest 'setupFilesAfterEnv' setup file +// It's a good place to set globals, add global before/after hooks, etc + +export {} // so TS doesn't complain diff --git a/examples/auth/test/utils.tsx b/examples/auth/test/utils.tsx new file mode 100644 index 0000000000..8a3c58d55f --- /dev/null +++ b/examples/auth/test/utils.tsx @@ -0,0 +1,108 @@ +import {RouterContext, BlitzRouter, BlitzProvider} from "blitz" +import {render as defaultRender} from "@testing-library/react" +import {renderHook as defaultRenderHook} from "@testing-library/react-hooks" + +export * from "@testing-library/react" + +// -------------------------------------------------------------------------------- +// This file customizes the render() and renderHook() test functions provided +// by React testing library. It adds a router context wrapper with a mocked router. +// +// You should always import `render` and `renderHook` from this file +// +// This is the place to add any other context providers you need while testing. +// -------------------------------------------------------------------------------- + +// -------------------------------------------------- +// render() +// -------------------------------------------------- +// Override the default test render with our own +// +// You can override the router mock like this: +// +// const { baseElement } = render(, { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function render( + ui: RenderUI, + {wrapper, router, dehydratedState, ...options}: RenderOptions = {}, +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({children}) => ( + + + {children} + + + ) + } + return defaultRender(ui, {wrapper, ...options}) +} + +// -------------------------------------------------- +// renderHook() +// -------------------------------------------------- +// Override the default test renderHook with our own +// +// You can override the router mock like this: +// +// const result = renderHook(() => myHook(), { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function renderHook( + hook: RenderHook, + {wrapper, router, dehydratedState, ...options}: RenderHookOptions = {}, +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({children}) => ( + + + {children} + + + ) + } + return defaultRenderHook(hook, {wrapper, ...options}) +} + +export const mockRouter: BlitzRouter = { + basePath: "", + pathname: "/", + route: "/", + asPath: "/", + params: {}, + query: {}, + isReady: true, + isLocaleDomain: false, + isPreview: false, + push: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + isFallback: false, +} + +type DefaultParams = Parameters +type RenderUI = DefaultParams[0] +type RenderOptions = DefaultParams[1] & { + router?: Partial + dehydratedState?: unknown +} + +type DefaultHookParams = Parameters +type RenderHook = DefaultHookParams[0] +type RenderHookOptions = DefaultHookParams[1] & { + router?: Partial + dehydratedState?: unknown +} diff --git a/examples/auth/tsconfig.json b/examples/auth/tsconfig.json new file mode 100644 index 0000000000..07cbfb2470 --- /dev/null +++ b/examples/auth/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "strict": true, + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": "./", + "allowJs": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "downlevelIteration": true, + "jsx": "preserve" + }, + "exclude": ["node_modules", "cypress", "test"], + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/examples/auth/types b/examples/auth/types new file mode 120000 index 0000000000..8788aa2845 --- /dev/null +++ b/examples/auth/types @@ -0,0 +1 @@ +../../types \ No newline at end of file diff --git a/examples/auth/types.ts b/examples/auth/types.ts new file mode 100644 index 0000000000..4b5b6f2feb --- /dev/null +++ b/examples/auth/types.ts @@ -0,0 +1,26 @@ +import {DefaultCtx, SessionContext, SimpleRolesIsAuthorized} from "blitz" +import {User} from "db" + +export type Role = "ADMIN" | "USER" + +declare module "blitz" { + export interface Ctx extends DefaultCtx { + session: SessionContext + } + export interface Session { + isAuthorized: SimpleRolesIsAuthorized + PublicData: { + userId: User["id"] + role: Role + views?: number + } + } +} + +// This should not be needed. Usually it isn't but for some reason in this example it is +declare module "react" { + interface StyleHTMLAttributes extends React.HTMLAttributes { + jsx?: boolean + global?: boolean + } +} diff --git a/examples/auth/utils/.keep b/examples/auth/utils/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/custom-server/.env b/examples/custom-server/.env new file mode 100644 index 0000000000..9e261f1218 --- /dev/null +++ b/examples/custom-server/.env @@ -0,0 +1,2 @@ +# This env file should be checked into source control +# This is the place for default values that should be used in all environments diff --git a/examples/custom-server/.env.test b/examples/custom-server/.env.test new file mode 100644 index 0000000000..024d653130 --- /dev/null +++ b/examples/custom-server/.env.test @@ -0,0 +1 @@ +SESSION_SECRET_KEY=A4B979A108440A218DE8F11CBB13A7B0 diff --git a/examples/custom-server/.eslintrc.js b/examples/custom-server/.eslintrc.js new file mode 100644 index 0000000000..b2037cc5e5 --- /dev/null +++ b/examples/custom-server/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: "blitz", +} diff --git a/examples/custom-server/.gitignore b/examples/custom-server/.gitignore new file mode 100644 index 0000000000..ab6e843bd8 --- /dev/null +++ b/examples/custom-server/.gitignore @@ -0,0 +1,55 @@ +# dependencies +node_modules +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* +.npm +web_modules/ + +# blitz +/.blitz/ +/.next/ +*.sqlite +.now +.blitz-console-history +blitz-log.log + +# misc +.DS_Store + +# local env files +.env.local +.env.*.local +.envrc + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +.coverage +*.lcov +.nyc_output +lib-cov + +# Caches +*.tsbuildinfo +.eslintcache +.node_repl_history +.yarn-integrity + +# Serverless directories +.serverless/ + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Custom build +build diff --git a/examples/custom-server/.npmrc b/examples/custom-server/.npmrc new file mode 100644 index 0000000000..cffe8cdef1 --- /dev/null +++ b/examples/custom-server/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/examples/custom-server/.prettierignore b/examples/custom-server/.prettierignore new file mode 100644 index 0000000000..ad8c486b66 --- /dev/null +++ b/examples/custom-server/.prettierignore @@ -0,0 +1,7 @@ +.gitkeep +.env* +*.ico +*.lock +db/migrations +.next +.blitz diff --git a/examples/custom-server/README.md b/examples/custom-server/README.md new file mode 100644 index 0000000000..dc24f9c32b --- /dev/null +++ b/examples/custom-server/README.md @@ -0,0 +1,29 @@ +# custom-server + +## Development + +1. Install + ```sh + yarn + ``` +2. Migrate + ```sh + blitz prisma migrate dev + ``` +3. Start the dev server + +```sh +blitz dev + +// Or if you want hot-reloading of server.js, use: +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +## Production + +```sh +blitz build +blitz start +``` diff --git a/examples/custom-server/app/auth/auth-utils.ts b/examples/custom-server/app/auth/auth-utils.ts new file mode 100644 index 0000000000..145ae20b95 --- /dev/null +++ b/examples/custom-server/app/auth/auth-utils.ts @@ -0,0 +1,39 @@ +import {AuthenticationError} from "blitz" +import SecurePassword from "secure-password" +import db from "db" + +const SP = new SecurePassword() + +export const hashPassword = async (password: string) => { + const hashedBuffer = await SP.hash(Buffer.from(password)) + return hashedBuffer.toString("base64") +} +export const verifyPassword = async (hashedPassword: string, password: string) => { + try { + return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64")) + } catch (error) { + console.error(error) + return false + } +} + +export const authenticateUser = async (email: string, password: string) => { + const user = await db.user.findFirst({where: {email: email.toLowerCase()}}) + + if (!user || !user.hashedPassword) throw new AuthenticationError() + + switch (await verifyPassword(user.hashedPassword, password)) { + case SecurePassword.VALID: + break + case SecurePassword.VALID_NEEDS_REHASH: + // Upgrade hashed password with a more secure hash + const improvedHash = await hashPassword(password) + await db.user.update({where: {id: user.id}, data: {hashedPassword: improvedHash}}) + break + default: + throw new AuthenticationError() + } + + const {hashedPassword, ...rest} = user + return rest +} diff --git a/examples/custom-server/app/auth/components/LoginForm.tsx b/examples/custom-server/app/auth/components/LoginForm.tsx new file mode 100644 index 0000000000..a06adacbd6 --- /dev/null +++ b/examples/custom-server/app/auth/components/LoginForm.tsx @@ -0,0 +1,50 @@ +import React from "react" +import {AuthenticationError, Link, useMutation} from "blitz" +import {LabeledTextField} from "app/components/LabeledTextField" +import {Form, FORM_ERROR} from "app/components/Form" +import login from "app/auth/mutations/login" +import {LoginInput} from "app/auth/validations" + +type LoginFormProps = { + onSuccess?: () => void +} + +export const LoginForm = (props: LoginFormProps) => { + const [loginMutation] = useMutation(login) + + return ( +
+

Login

+ +
{ + try { + await loginMutation(values) + props.onSuccess?.() + } catch (error) { + if (error instanceof AuthenticationError) { + return {[FORM_ERROR]: "Sorry, those credentials are invalid"} + } else { + return { + [FORM_ERROR]: + "Sorry, we had an unexpected error. Please try again. - " + error.toString(), + } + } + } + }} + > + + + + +
+ Or Sign Up +
+
+ ) +} + +export default LoginForm diff --git a/examples/custom-server/app/auth/components/SignupForm.tsx b/examples/custom-server/app/auth/components/SignupForm.tsx new file mode 100644 index 0000000000..eb7028df5e --- /dev/null +++ b/examples/custom-server/app/auth/components/SignupForm.tsx @@ -0,0 +1,44 @@ +import React from "react" +import {useMutation} from "blitz" +import {LabeledTextField} from "app/components/LabeledTextField" +import {Form, FORM_ERROR} from "app/components/Form" +import signup from "app/auth/mutations/signup" +import {SignupInput} from "app/auth/validations" + +type SignupFormProps = { + onSuccess?: () => void +} + +export const SignupForm = (props: SignupFormProps) => { + const [signupMutation] = useMutation(signup) + + return ( +
+

Create an Account

+ +
{ + try { + await signupMutation(values) + props.onSuccess?.() + } catch (error) { + if (error.code === "P2002" && error.meta?.target?.includes("email")) { + // This error comes from Prisma + return {email: "This email is already being used"} + } else { + return {[FORM_ERROR]: error.toString()} + } + } + }} + > + + + +
+ ) +} + +export default SignupForm diff --git a/examples/custom-server/app/auth/mutations/login.ts b/examples/custom-server/app/auth/mutations/login.ts new file mode 100644 index 0000000000..e4cf5efd7a --- /dev/null +++ b/examples/custom-server/app/auth/mutations/login.ts @@ -0,0 +1,15 @@ +import {Ctx} from "blitz" +import {authenticateUser} from "app/auth/auth-utils" +import {LoginInput, LoginInputType} from "../validations" + +export default async function login(input: LoginInputType, {session}: Ctx) { + // This throws an error if input is invalid + const {email, password} = LoginInput.parse(input) + + // This throws an error if credentials are invalid + const user = await authenticateUser(email, password) + + await session.$create({userId: user.id}) + + return user +} diff --git a/examples/custom-server/app/auth/mutations/logout.ts b/examples/custom-server/app/auth/mutations/logout.ts new file mode 100644 index 0000000000..3ef66453da --- /dev/null +++ b/examples/custom-server/app/auth/mutations/logout.ts @@ -0,0 +1,5 @@ +import {Ctx} from "blitz" + +export default async function logout(_: any, {session}: Ctx) { + return await session.$revoke() +} diff --git a/examples/custom-server/app/auth/mutations/signup.ts b/examples/custom-server/app/auth/mutations/signup.ts new file mode 100644 index 0000000000..5741a5352a --- /dev/null +++ b/examples/custom-server/app/auth/mutations/signup.ts @@ -0,0 +1,19 @@ +import {Ctx} from "blitz" +import db from "db" +import {hashPassword} from "app/auth/auth-utils" +import {SignupInput, SignupInputType} from "app/auth/validations" + +export default async function signup(input: SignupInputType, {session}: Ctx) { + // This throws an error if input is invalid + const {email, password} = SignupInput.parse(input) + + const hashedPassword = await hashPassword(password) + const user = await db.user.create({ + data: {email: email.toLowerCase(), hashedPassword, role: "user"}, + select: {id: true, name: true, email: true, role: true}, + }) + + await session.$create({userId: user.id}) + + return user +} diff --git a/examples/custom-server/app/auth/pages/login.tsx b/examples/custom-server/app/auth/pages/login.tsx new file mode 100644 index 0000000000..2ddd6cf8f0 --- /dev/null +++ b/examples/custom-server/app/auth/pages/login.tsx @@ -0,0 +1,18 @@ +import React from "react" +import {useRouter, BlitzPage} from "blitz" +import Layout from "app/layouts/Layout" +import {LoginForm} from "app/auth/components/LoginForm" + +const LoginPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ router.push("/")} /> +
+ ) +} + +LoginPage.getLayout = (page) => {page} + +export default LoginPage diff --git a/examples/custom-server/app/auth/pages/signup.tsx b/examples/custom-server/app/auth/pages/signup.tsx new file mode 100644 index 0000000000..73775bdfca --- /dev/null +++ b/examples/custom-server/app/auth/pages/signup.tsx @@ -0,0 +1,18 @@ +import React from "react" +import {useRouter, BlitzPage} from "blitz" +import Layout from "app/layouts/Layout" +import {SignupForm} from "app/auth/components/SignupForm" + +const SignupPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ router.push("/")} /> +
+ ) +} + +SignupPage.getLayout = (page) => {page} + +export default SignupPage diff --git a/examples/custom-server/app/auth/validations.ts b/examples/custom-server/app/auth/validations.ts new file mode 100644 index 0000000000..af33328308 --- /dev/null +++ b/examples/custom-server/app/auth/validations.ts @@ -0,0 +1,13 @@ +import {z} from "zod" + +export const SignupInput = z.object({ + email: z.string().email(), + password: z.string().min(10).max(100), +}) +export type SignupInputType = z.infer + +export const LoginInput = z.object({ + email: z.string().email(), + password: z.string(), +}) +export type LoginInputType = z.infer diff --git a/examples/custom-server/app/components/.keep b/examples/custom-server/app/components/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/custom-server/app/components/Form.tsx b/examples/custom-server/app/components/Form.tsx new file mode 100644 index 0000000000..fbb359fae4 --- /dev/null +++ b/examples/custom-server/app/components/Form.tsx @@ -0,0 +1,58 @@ +import React, {ReactNode, PropsWithoutRef} from "react" +import {Form as FinalForm, FormProps as FinalFormProps} from "react-final-form" +import {z} from "zod" +import {validateZodSchema} from "blitz" +export {FORM_ERROR} from "final-form" + +type FormProps> = { + /** All your form fields */ + children: ReactNode + /** Text to display in the submit button */ + submitText?: string + schema?: S + onSubmit: FinalFormProps>["onSubmit"] + initialValues?: FinalFormProps>["initialValues"] +} & Omit, "onSubmit"> + +export function Form>({ + children, + submitText, + schema, + initialValues, + onSubmit, + ...props +}: FormProps) { + return ( + ( +
+ {/* Form fields supplied as children are rendered here */} + {children} + + {submitError && ( +
+ {submitError} +
+ )} + + {submitText && ( + + )} + + +
+ )} + /> + ) +} + +export default Form diff --git a/examples/custom-server/app/components/LabeledTextField.tsx b/examples/custom-server/app/components/LabeledTextField.tsx new file mode 100644 index 0000000000..84fd2b08f7 --- /dev/null +++ b/examples/custom-server/app/components/LabeledTextField.tsx @@ -0,0 +1,57 @@ +import React, {PropsWithoutRef} from "react" +import {useField} from "react-final-form" + +export interface LabeledTextFieldProps extends PropsWithoutRef { + /** Field name. */ + name: string + /** Field label. */ + label: string + /** Field type. Doesn't include radio buttons and checkboxes */ + type?: "text" | "password" | "email" | "number" + outerProps?: PropsWithoutRef +} + +export const LabeledTextField = React.forwardRef( + ({name, label, outerProps, ...props}, ref) => { + const { + input, + meta: {touched, error, submitError, submitting}, + } = useField(name) + + const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError + + return ( +
+ + + {touched && normalizedError && ( +
+ {normalizedError} +
+ )} + + +
+ ) + }, +) + +export default LabeledTextField diff --git a/examples/custom-server/app/hooks/useCurrentUser.ts b/examples/custom-server/app/hooks/useCurrentUser.ts new file mode 100644 index 0000000000..ffd7fb2302 --- /dev/null +++ b/examples/custom-server/app/hooks/useCurrentUser.ts @@ -0,0 +1,10 @@ +import {useQuery, useSession} from "blitz" +import getCurrentUser from "app/users/queries/getCurrentUser" + +export const useCurrentUser = () => { + // We wouldn't have to useSession() here, but doing so improves perf on initial + // load since we can skip the getCurrentUser() request. + const session = useSession() + const [user] = useQuery(getCurrentUser, null, {enabled: !!session.userId}) + return session.userId ? user : null +} diff --git a/examples/custom-server/app/layouts/.keep b/examples/custom-server/app/layouts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/custom-server/app/layouts/Layout.tsx b/examples/custom-server/app/layouts/Layout.tsx new file mode 100644 index 0000000000..adc65f5b11 --- /dev/null +++ b/examples/custom-server/app/layouts/Layout.tsx @@ -0,0 +1,16 @@ +import {Head, BlitzLayout} from "blitz" + +const Layout: BlitzLayout<{title?: string}> = ({title, children}) => { + return ( + <> + + {title || "custom-server"} + + + + {children} + + ) +} + +export default Layout diff --git a/examples/custom-server/app/pages/404.tsx b/examples/custom-server/app/pages/404.tsx new file mode 100644 index 0000000000..84c795f017 --- /dev/null +++ b/examples/custom-server/app/pages/404.tsx @@ -0,0 +1,19 @@ +import {Head, ErrorComponent} from "blitz" + +// ------------------------------------------------------ +// This page is rendered if a route match is not found +// ------------------------------------------------------ +export default function Page404() { + const statusCode = 404 + const title = "This page could not be found" + return ( + <> + + + {statusCode}: {title} + + + + + ) +} diff --git a/examples/custom-server/app/pages/_app.tsx b/examples/custom-server/app/pages/_app.tsx new file mode 100644 index 0000000000..2293ee2a0c --- /dev/null +++ b/examples/custom-server/app/pages/_app.tsx @@ -0,0 +1,41 @@ +import { + AppProps, + ErrorBoundary, + ErrorFallbackProps, + ErrorComponent, + AuthenticationError, + AuthorizationError, + useQueryErrorResetBoundary, +} from "blitz" +import LoginForm from "app/auth/components/LoginForm" + +export default function App({Component, pageProps}: AppProps) { + const getLayout = Component.getLayout || ((page) => page) + const {reset} = useQueryErrorResetBoundary() + + return ( + + {getLayout()} + + ) +} + +function RootErrorFallback({error, resetErrorBoundary}: ErrorFallbackProps) { + if (error instanceof AuthenticationError) { + return + } else if (error instanceof AuthorizationError) { + return ( + + ) + } else { + return ( + + ) + } +} diff --git a/examples/custom-server/app/pages/_document.tsx b/examples/custom-server/app/pages/_document.tsx new file mode 100644 index 0000000000..788eb1febe --- /dev/null +++ b/examples/custom-server/app/pages/_document.tsx @@ -0,0 +1,23 @@ +import {Document, Html, DocumentHead, Main, BlitzScript /*DocumentContext*/} from "blitz" + +class MyDocument extends Document { + // Only uncomment if you need to customize this behaviour + // static async getInitialProps(ctx: DocumentContext) { + // const initialProps = await Document.getInitialProps(ctx) + // return {...initialProps} + // } + + render() { + return ( + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/custom-server/app/pages/index.tsx b/examples/custom-server/app/pages/index.tsx new file mode 100644 index 0000000000..1cc8c004b3 --- /dev/null +++ b/examples/custom-server/app/pages/index.tsx @@ -0,0 +1,273 @@ +import {Link, BlitzPage, useMutation, Image} from "blitz" +import Layout from "app/layouts/Layout" +import logout from "app/auth/mutations/logout" +import {useCurrentUser} from "app/hooks/useCurrentUser" +import {Suspense} from "react" +import logo from "public/logo.png" + +/* + * This file is just for a pleasant getting started page for your new app. + * You can delete everything in here and start from scratch if you like. + */ + +const UserInfo = () => { + const currentUser = useCurrentUser() + const [logoutMutation] = useMutation(logout) + + if (currentUser) { + return ( + <> + +
+ User id: {currentUser.id} +
+ User role: {currentUser.role} +
+ + ) + } else { + return ( + <> + + + Sign Up + + + + + Login + + + + ) + } +} + +const Home: BlitzPage = () => { + return ( +
+
+
+ blitz.js +
+

+ Congrats! Your app is ready, including user sign-up and log-in. +

+
+ + + +
+

+ + To add a new model to your app,
+ run the following in your terminal: +
+

+
+          blitz generate all project name:string
+        
+
+          blitz prisma migrate dev
+        
+
+

+ Then restart the server +

+
+            Ctrl + c
+          
+
+            blitz dev
+          
+

+ and go to{" "} + + /projects + +

+
+ +
+ + + + +
+ ) +} + +Home.getLayout = (page) => {page} + +export default Home diff --git a/examples/custom-server/app/users/queries/getCurrentUser.ts b/examples/custom-server/app/users/queries/getCurrentUser.ts new file mode 100644 index 0000000000..e19a903636 --- /dev/null +++ b/examples/custom-server/app/users/queries/getCurrentUser.ts @@ -0,0 +1,13 @@ +import {Ctx} from "blitz" +import db from "db" + +export default async function getCurrentUser(_ = null, {session}: Ctx) { + if (!session.userId) return null + + const user = await db.user.findFirst({ + where: {id: session.userId}, + select: {id: true, name: true, email: true, role: true}, + }) + + return user +} diff --git a/examples/custom-server/babel.config.js b/examples/custom-server/babel.config.js new file mode 100644 index 0000000000..dfdf62cea1 --- /dev/null +++ b/examples/custom-server/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["blitz/babel"], + plugins: [], +} diff --git a/examples/custom-server/blitz-env.d.ts b/examples/custom-server/blitz-env.d.ts new file mode 100644 index 0000000000..9bc3dd46b9 --- /dev/null +++ b/examples/custom-server/blitz-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/custom-server/blitz.config.js b/examples/custom-server/blitz.config.js new file mode 100644 index 0000000000..537c1431ea --- /dev/null +++ b/examples/custom-server/blitz.config.js @@ -0,0 +1,18 @@ +const {sessionMiddleware, simpleRolesIsAuthorized} = require("blitz") + +module.exports = { + middleware: [ + sessionMiddleware({ + cookiePrefix: "blitz-custom-server-example", + isAuthorized: simpleRolesIsAuthorized, + }), + ], + /* Uncomment this to customize the webpack config + webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + // Important: return the modified config + return config + }, + */ +} diff --git a/examples/custom-server/cypress.json b/examples/custom-server/cypress.json new file mode 100644 index 0000000000..80e5d9a07e --- /dev/null +++ b/examples/custom-server/cypress.json @@ -0,0 +1,6 @@ +{ + "baseUrl": "http://localhost:3099", + "defaultCommandTimeout": 10000, + "video": false, + "chromeWebSecurity": false +} diff --git a/examples/custom-server/cypress/fixtures/example.json b/examples/custom-server/cypress/fixtures/example.json new file mode 100644 index 0000000000..02e4254378 --- /dev/null +++ b/examples/custom-server/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/examples/custom-server/cypress/index.d.ts b/examples/custom-server/cypress/index.d.ts new file mode 100644 index 0000000000..13c1ec5189 --- /dev/null +++ b/examples/custom-server/cypress/index.d.ts @@ -0,0 +1,8 @@ +/// +/// + +declare namespace Cypress { + interface Chainable { + signup(user: {email: string; password: string}): void + } +} diff --git a/examples/custom-server/cypress/integration/index.test.ts b/examples/custom-server/cypress/integration/index.test.ts new file mode 100644 index 0000000000..44f3e1b446 --- /dev/null +++ b/examples/custom-server/cypress/integration/index.test.ts @@ -0,0 +1,57 @@ +import {createRandomUser} from "../support/helpers" + +describe("index page", () => { + beforeEach(() => { + cy.visit("/") + }) + + it("goes to the signup page", () => { + cy.contains("a", "Sign Up").click() + cy.location("pathname").should("equal", "/signup") + }) + + it("goes to the login page", () => { + cy.contains("a", /login/i).click() + cy.location("pathname").should("equal", "/login") + }) + + it("allows the user to signup", () => { + const user = createRandomUser() + + cy.signup(user) + + cy.location("pathname").should("equal", "/") + cy.contains("button", "Logout") + }) + + it("allows the user to log in", () => { + const user = createRandomUser() + + cy.signup(user) + + cy.contains("button", "Logout").click() + cy.wait(500) + cy.contains("a", /login/i).click() + + cy.contains("Email").find("input").type(user.email) + cy.contains("Password").find("input").type(user.password) + cy.contains("button", /login/i).click() + cy.wait(500) + + cy.location("pathname").should("equal", "/") + cy.contains("button", "Logout") + }) + + it("allows the user to logout", () => { + const user = createRandomUser() + + cy.signup(user) + + cy.contains("button", "Logout").click() + + cy.location("pathname").should("equal", "/") + cy.contains("a", /login/i) + }) +}) + +export {} diff --git a/examples/custom-server/cypress/plugins/index.ts b/examples/custom-server/cypress/plugins/index.ts new file mode 100644 index 0000000000..326165510e --- /dev/null +++ b/examples/custom-server/cypress/plugins/index.ts @@ -0,0 +1,22 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +//@ts-ignore +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/examples/custom-server/cypress/support/commands.ts b/examples/custom-server/cypress/support/commands.ts new file mode 100644 index 0000000000..2727ed2b84 --- /dev/null +++ b/examples/custom-server/cypress/support/commands.ts @@ -0,0 +1,19 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// + +Cypress.Commands.add("signup", ({email, password}) => { + cy.contains("a", "Sign Up").click() + + cy.contains("Email").find("input").type(email) + cy.contains("Password").find("input").type(password) + cy.contains("button", "Create Account").click() +}) diff --git a/examples/custom-server/cypress/support/helpers.ts b/examples/custom-server/cypress/support/helpers.ts new file mode 100644 index 0000000000..f6b07ed118 --- /dev/null +++ b/examples/custom-server/cypress/support/helpers.ts @@ -0,0 +1,7 @@ +export const createRandomUser = () => { + const random = Math.round(Math.random() * 100000).toString() + const email = `test_${random}@example.com` + const password = `password_${random}` + + return {email, password} +} diff --git a/examples/custom-server/cypress/support/index.ts b/examples/custom-server/cypress/support/index.ts new file mode 100644 index 0000000000..e272080237 --- /dev/null +++ b/examples/custom-server/cypress/support/index.ts @@ -0,0 +1,26 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import "./commands" + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +Cypress.Screenshot.defaults({ + screenshotOnRunFailure: false, +}) + +require("@cypress/skip-test/support") diff --git a/examples/custom-server/db/index.ts b/examples/custom-server/db/index.ts new file mode 100644 index 0000000000..cde0794c22 --- /dev/null +++ b/examples/custom-server/db/index.ts @@ -0,0 +1,7 @@ +import {enhancePrisma} from "blitz" +import {PrismaClient} from "@prisma/client" + +const EnhancedPrisma = enhancePrisma(PrismaClient) + +export * from "@prisma/client" +export default new EnhancedPrisma() diff --git a/examples/custom-server/db/migrations/.keep b/examples/custom-server/db/migrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/custom-server/db/migrations/20210112001717_initial/migration.sql b/examples/custom-server/db/migrations/20210112001717_initial/migration.sql new file mode 100644 index 0000000000..3258a3324d --- /dev/null +++ b/examples/custom-server/db/migrations/20210112001717_initial/migration.sql @@ -0,0 +1,32 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "name" TEXT, + "email" TEXT NOT NULL, + "hashedPassword" TEXT, + "role" TEXT NOT NULL DEFAULT 'user' +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "expiresAt" DATETIME, + "handle" TEXT NOT NULL, + "userId" INTEGER, + "hashedSessionToken" TEXT, + "antiCSRFToken" TEXT, + "publicData" TEXT, + "privateData" TEXT, + + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session.handle_unique" ON "Session"("handle"); diff --git a/examples/custom-server/db/schema.prisma b/examples/custom-server/db/schema.prisma new file mode 100644 index 0000000000..acc4880c90 --- /dev/null +++ b/examples/custom-server/db/schema.prisma @@ -0,0 +1,38 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +} + +generator client { + provider = "prisma-client-js" +} + +// -------------------------------------- + +model User { + id Int @default(autoincrement()) @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String? + email String @unique + hashedPassword String? + role String @default("user") + sessions Session[] +} + +model Session { + id Int @default(autoincrement()) @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + expiresAt DateTime? + handle String @unique + user User? @relation(fields: [userId], references: [id]) + userId Int? + hashedSessionToken String? + antiCSRFToken String? + publicData String? + privateData String? +} diff --git a/examples/custom-server/db/seeds.ts b/examples/custom-server/db/seeds.ts new file mode 100644 index 0000000000..cc39e51b95 --- /dev/null +++ b/examples/custom-server/db/seeds.ts @@ -0,0 +1,15 @@ +// import db from "./index" + +/* + * This seed function is executed when you run `blitz db seed`. + * + * Probably you want to use a library like https://chancejs.com + * to easily generate realistic data. + */ +const seed = async () => { + // for (let i = 0; i < 5; i++) { + // await db.project.create({ data: { name: "Project " + i } }) + // } +} + +export default seed diff --git a/examples/custom-server/integrations/.keep b/examples/custom-server/integrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/custom-server/jest.config.js b/examples/custom-server/jest.config.js new file mode 100644 index 0000000000..297905551f --- /dev/null +++ b/examples/custom-server/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: "blitz", +} diff --git a/examples/custom-server/package.json b/examples/custom-server/package.json new file mode 100644 index 0000000000..3f02f7959f --- /dev/null +++ b/examples/custom-server/package.json @@ -0,0 +1,59 @@ +{ + "name": "@examples/custom-server", + "version": "0.34.0-canary.0", + "scripts": { + "dev": "blitz dev", + "build": "blitz build", + "start": "blitz start", + "studio": "blitz prisma studio", + "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", + "test-watch": "jest --watch", + "cy-open": "cypress open", + "cy-run": "cypress run", + "test:migrate": "prisma generate && blitz prisma migrate deploy", + "test:jest": "jest --passWithNoTests", + "test-server": "blitz build && blitz start", + "test-run-e2e": "cross-env NODE_ENV=test PORT=3099 start-server-and-test test-server http://localhost:3099 cy-run", + "test:e2e": "yarn test-run-e2e || yarn test-run-e2e", + "test": "run-s test:*" + }, + "browserslist": [ + "defaults" + ], + "prisma": { + "schema": "db/schema.prisma" + }, + "prettier": { + "semi": false, + "printWidth": 100, + "bracketSpacing": false, + "trailingComma": "all" + }, + "dependencies": { + "@prisma/client": "2.24.1", + "blitz": "0.45.4", + "final-form": "4.20.1", + "prisma": "2.24.1", + "react": "0.0.0-experimental-6a589ad71", + "react-dom": "0.0.0-experimental-6a589ad71", + "react-final-form": "6.5.2", + "secure-password": "4.0.0" + }, + "devDependencies": { + "@cypress/skip-test": "2.6.0", + "@testing-library/react": "11.2.5", + "@testing-library/react-hooks": "^4.0.1", + "@types/react": "17.0.2", + "@types/secure-password": "3.1.0", + "cypress": "6.2.1", + "eslint": "7.21.0", + "husky": "5.1.2", + "lint-staged": "10.5.4", + "nodemon": "2.0.7", + "npm-run-all": "4.1.5", + "prettier": "2.2.1", + "pretty-quick": "3.1.0", + "start-server-and-test": "1.11.7" + }, + "private": true +} diff --git a/examples/custom-server/public/favicon.ico b/examples/custom-server/public/favicon.ico new file mode 100755 index 0000000000..a94b0f7a7f Binary files /dev/null and b/examples/custom-server/public/favicon.ico differ diff --git a/examples/custom-server/public/logo.png b/examples/custom-server/public/logo.png new file mode 100644 index 0000000000..6a982616ee Binary files /dev/null and b/examples/custom-server/public/logo.png differ diff --git a/examples/custom-server/server/index.ts b/examples/custom-server/server/index.ts new file mode 100644 index 0000000000..2515f2ed0c --- /dev/null +++ b/examples/custom-server/server/index.ts @@ -0,0 +1,25 @@ +import blitz from "blitz/custom-server" +import {createServer} from "http" +import {parse} from "url" +import {log} from "next/dist/server/lib/logging" + +const {PORT = "3000"} = process.env +const dev = process.env.NODE_ENV !== "production" +const app = blitz({dev}) +const handle = app.getRequestHandler() + +app.prepare().then(() => { + createServer((req, res) => { + const parsedUrl = parse(req.url!, true) + const {pathname} = parsedUrl + + if (pathname === "/hello") { + res.writeHead(200).end("world") + return + } + + handle(req, res, parsedUrl) + }).listen(PORT, () => { + log.success(`Ready on http://localhost:${PORT}`) + }) +}) diff --git a/examples/custom-server/test/.keep b/examples/custom-server/test/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/custom-server/test/setup.ts b/examples/custom-server/test/setup.ts new file mode 100644 index 0000000000..4ca742763e --- /dev/null +++ b/examples/custom-server/test/setup.ts @@ -0,0 +1,2 @@ +// This is the jest 'setupFilesAfterEnv' setup file +// It's a good place to set globals, add global before/after hooks, etc diff --git a/examples/custom-server/test/utils.tsx b/examples/custom-server/test/utils.tsx new file mode 100644 index 0000000000..09fa82e919 --- /dev/null +++ b/examples/custom-server/test/utils.tsx @@ -0,0 +1,109 @@ +import React from "react" +import {RouterContext, BlitzRouter, BlitzProvider} from "blitz" +import {render as defaultRender} from "@testing-library/react" +import {renderHook as defaultRenderHook} from "@testing-library/react-hooks" + +export * from "@testing-library/react" + +// -------------------------------------------------------------------------------- +// This file customizes the render() and renderHook() test functions provided +// by React testing library. It adds a router context wrapper with a mocked router. +// +// You should always import `render` and `renderHook` from this file +// +// This is the place to add any other context providers you need while testing. +// -------------------------------------------------------------------------------- + +// -------------------------------------------------- +// render() +// -------------------------------------------------- +// Override the default test render with our own +// +// You can override the router mock like this: +// +// const { baseElement } = render(, { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function render( + ui: RenderUI, + {wrapper, router, dehydratedState, ...options}: RenderOptions = {}, +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({children}) => ( + + + {children} + + + ) + } + return defaultRender(ui, {wrapper, ...options}) +} + +// -------------------------------------------------- +// renderHook() +// -------------------------------------------------- +// Override the default test renderHook with our own +// +// You can override the router mock like this: +// +// const result = renderHook(() => myHook(), { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function renderHook( + hook: RenderHook, + {wrapper, router, dehydratedState, ...options}: RenderHookOptions = {}, +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({children}) => ( + + + {children} + + + ) + } + return defaultRenderHook(hook, {wrapper, ...options}) +} + +export const mockRouter: BlitzRouter = { + basePath: "", + pathname: "/", + route: "/", + asPath: "/", + params: {}, + query: {}, + isReady: true, + isLocaleDomain: false, + isPreview: false, + push: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + isFallback: false, +} + +type DefaultParams = Parameters +type RenderUI = DefaultParams[0] +type RenderOptions = DefaultParams[1] & { + router?: Partial + dehydratedState?: unknown +} + +type DefaultHookParams = Parameters +type RenderHook = DefaultHookParams[0] +type RenderHookOptions = DefaultHookParams[1] & { + router?: Partial + dehydratedState?: unknown +} diff --git a/examples/custom-server/tsconfig.json b/examples/custom-server/tsconfig.json new file mode 100644 index 0000000000..48de028eef --- /dev/null +++ b/examples/custom-server/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": "./", + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "exclude": ["node_modules"], + "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"] +} diff --git a/examples/custom-server/types b/examples/custom-server/types new file mode 120000 index 0000000000..8788aa2845 --- /dev/null +++ b/examples/custom-server/types @@ -0,0 +1 @@ +../../types \ No newline at end of file diff --git a/examples/custom-server/types.ts b/examples/custom-server/types.ts new file mode 100644 index 0000000000..b11dc17f59 --- /dev/null +++ b/examples/custom-server/types.ts @@ -0,0 +1,22 @@ +import {DefaultCtx, SessionContext, SimpleRolesIsAuthorized} from "blitz" +import React from "react" + +declare module "blitz" { + export interface Ctx extends DefaultCtx { + session: SessionContext + } + export interface Session { + isAuthorized: SimpleRolesIsAuthorized + PublicData: { + userId: number + } + } +} + +// This should not be needed. Usually it isn't but for some reason in this example it is +declare module "react" { + interface StyleHTMLAttributes extends React.HTMLAttributes { + jsx?: boolean + global?: boolean + } +} diff --git a/examples/custom-server/utils/.keep b/examples/custom-server/utils/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/cypress/.editorconfig b/examples/cypress/.editorconfig new file mode 100644 index 0000000000..09d7a33a4f --- /dev/null +++ b/examples/cypress/.editorconfig @@ -0,0 +1,11 @@ +# https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/examples/cypress/.env b/examples/cypress/.env new file mode 100644 index 0000000000..5cd1aca530 --- /dev/null +++ b/examples/cypress/.env @@ -0,0 +1,3 @@ +# This env file should be checked into source control +# This is the place for default values for all environments +# Values in `.env.local` and `.env.production` will override these values diff --git a/examples/cypress/.eslintrc.js b/examples/cypress/.eslintrc.js new file mode 100644 index 0000000000..f845b10d52 --- /dev/null +++ b/examples/cypress/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["blitz"], +} diff --git a/examples/cypress/.gitignore b/examples/cypress/.gitignore new file mode 100644 index 0000000000..ee6565a01d --- /dev/null +++ b/examples/cypress/.gitignore @@ -0,0 +1,56 @@ +# dependencies +node_modules +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +.pnp.* +.npm +web_modules/ + +# blitz +/.blitz/ +/.next/ +*.sqlite +*.sqlite-journal +.now +.blitz** +blitz-log.log + +# misc +.DS_Store + +# local env files +.env.local +.env.*.local +.envrc + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +.coverage +*.lcov +.nyc_output +lib-cov + +# Caches +*.tsbuildinfo +.eslintcache +.node_repl_history +.yarn-integrity + +# Serverless directories +.serverless/ + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test diff --git a/examples/cypress/.npmrc b/examples/cypress/.npmrc new file mode 100644 index 0000000000..f7777f0b8c --- /dev/null +++ b/examples/cypress/.npmrc @@ -0,0 +1,7 @@ +save-exact=true +legacy-peer-deps=true + +public-hoist-pattern[]=next +public-hoist-pattern[]=secure-password +public-hoist-pattern[]=*jest* +public-hoist-pattern[]=@testing-library/* diff --git a/examples/cypress/.prettierignore b/examples/cypress/.prettierignore new file mode 100644 index 0000000000..b1d375c7de --- /dev/null +++ b/examples/cypress/.prettierignore @@ -0,0 +1,9 @@ +.gitkeep +.env* +*.ico +*.lock +db/migrations +.next +.blitz +.yarn +.pnp* diff --git a/examples/cypress/README.md b/examples/cypress/README.md new file mode 100644 index 0000000000..5e959db144 --- /dev/null +++ b/examples/cypress/README.md @@ -0,0 +1,25 @@ +# **cypress** + +This is an example [Cypress](https://www.cypress.io/) integration. + +## Getting Started + +Migrate the database: + +``` +blitz prisma migrate dev +``` + +Run e2e tests: + +``` +yarn test:e2e +``` + +Open Cypress dashboard: + +``` +yarn cypress:open +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/examples/cypress/app/auth/components/LoginForm.tsx b/examples/cypress/app/auth/components/LoginForm.tsx new file mode 100644 index 0000000000..4dc8ce4baf --- /dev/null +++ b/examples/cypress/app/auth/components/LoginForm.tsx @@ -0,0 +1,49 @@ +import { AuthenticationError, Link, useMutation } from "blitz" +import { LabeledTextField } from "app/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/core/components/Form" +import login from "app/auth/mutations/login" +import { Login } from "app/auth/validations" + +type LoginFormProps = { + onSuccess?: () => void +} + +export const LoginForm = (props: LoginFormProps) => { + const [loginMutation] = useMutation(login) + + return ( +
+

Login

+ +
{ + try { + await loginMutation(values) + props.onSuccess?.() + } catch (error: any) { + if (error instanceof AuthenticationError) { + return { [FORM_ERROR]: "Sorry, those credentials are invalid" } + } else { + return { + [FORM_ERROR]: + "Sorry, we had an unexpected error. Please try again. - " + error.toString(), + } + } + } + }} + > + + + + +
+ Or Sign Up +
+
+ ) +} + +export default LoginForm diff --git a/examples/cypress/app/auth/components/SignupForm.tsx b/examples/cypress/app/auth/components/SignupForm.tsx new file mode 100644 index 0000000000..a76d800dc9 --- /dev/null +++ b/examples/cypress/app/auth/components/SignupForm.tsx @@ -0,0 +1,43 @@ +import { useMutation } from "blitz" +import { LabeledTextField } from "app/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/core/components/Form" +import signup from "app/auth/mutations/signup" +import { Signup } from "app/auth/validations" + +type SignupFormProps = { + onSuccess?: () => void +} + +export const SignupForm = (props: SignupFormProps) => { + const [signupMutation] = useMutation(signup) + + return ( +
+

Create an Account

+ +
{ + try { + await signupMutation(values) + props.onSuccess?.() + } catch (error: any) { + if (error.code === "P2002" && error.meta?.target?.includes("email")) { + // This error comes from Prisma + return { email: "This email is already being used" } + } else { + return { [FORM_ERROR]: error.toString() } + } + } + }} + > + + + +
+ ) +} + +export default SignupForm diff --git a/examples/cypress/app/auth/mutations/changePassword.ts b/examples/cypress/app/auth/mutations/changePassword.ts new file mode 100644 index 0000000000..94f7d391d3 --- /dev/null +++ b/examples/cypress/app/auth/mutations/changePassword.ts @@ -0,0 +1,23 @@ +import { NotFoundError, SecurePassword, resolver } from "blitz" +import db from "db" +import { authenticateUser } from "./login" +import { ChangePassword } from "../validations" + +export default resolver.pipe( + resolver.zod(ChangePassword), + resolver.authorize(), + async ({ currentPassword, newPassword }, ctx) => { + const user = await db.user.findFirst({ where: { id: ctx.session.userId! } }) + if (!user) throw new NotFoundError() + + await authenticateUser(user.email, currentPassword) + + const hashedPassword = await SecurePassword.hash(newPassword.trim()) + await db.user.update({ + where: { id: user.id }, + data: { hashedPassword }, + }) + + return true + } +) diff --git a/examples/cypress/app/auth/mutations/forgotPassword.test.ts b/examples/cypress/app/auth/mutations/forgotPassword.test.ts new file mode 100644 index 0000000000..cdd8112c3c --- /dev/null +++ b/examples/cypress/app/auth/mutations/forgotPassword.test.ts @@ -0,0 +1,56 @@ +import { hash256, Ctx } from "blitz" +import forgotPassword from "./forgotPassword" +import db from "db" +import previewEmail from "preview-email" + +beforeEach(async () => { + await db.$reset() +}) + +const generatedToken = "plain-token" +jest.mock("next/stdlib-server", () => ({ + ...jest.requireActual("next/stdlib-server")!, + generateToken: () => generatedToken, +})) +jest.mock("preview-email", () => jest.fn()) + +describe("forgotPassword mutation", () => { + it("does not throw error if user doesn't exist", async () => { + await expect(forgotPassword({ email: "no-user@email.com" }, {} as Ctx)).resolves.not.toThrow() + }) + + it("works correctly", async () => { + // Create test user + const user = await db.user.create({ + data: { + email: "user@example.com", + tokens: { + // Create old token to ensure it's deleted + create: { + type: "RESET_PASSWORD", + hashedToken: "token", + expiresAt: new Date(), + sentTo: "user@example.com", + }, + }, + }, + include: { tokens: true }, + }) + + // Invoke the mutation + await forgotPassword({ email: user.email }, {} as Ctx) + + const tokens = await db.token.findMany({ where: { userId: user.id } }) + const token = tokens[0] + + // delete's existing tokens + expect(tokens.length).toBe(1) + + expect(token.id).not.toBe(user.tokens[0].id) + expect(token.type).toBe("RESET_PASSWORD") + expect(token.sentTo).toBe(user.email) + expect(token.hashedToken).toBe(hash256(generatedToken)) + expect(token.expiresAt > new Date()).toBe(true) + expect(previewEmail).toBeCalled() + }) +}) diff --git a/examples/cypress/app/auth/mutations/forgotPassword.ts b/examples/cypress/app/auth/mutations/forgotPassword.ts new file mode 100644 index 0000000000..1cbf2a33cb --- /dev/null +++ b/examples/cypress/app/auth/mutations/forgotPassword.ts @@ -0,0 +1,41 @@ +import { resolver, generateToken, hash256 } from "blitz" +import db from "db" +import { forgotPasswordMailer } from "mailers/forgotPasswordMailer" +import { ForgotPassword } from "../validations" + +const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4 + +export default resolver.pipe(resolver.zod(ForgotPassword), async ({ email }) => { + // 1. Get the user + const user = await db.user.findFirst({ where: { email: email.toLowerCase() } }) + + // 2. Generate the token and expiration date. + const token = generateToken() + const hashedToken = hash256(token) + const expiresAt = new Date() + expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS) + + // 3. If user with this email was found + if (user) { + // 4. Delete any existing password reset tokens + await db.token.deleteMany({ where: { type: "RESET_PASSWORD", userId: user.id } }) + // 5. Save this new token in the database. + await db.token.create({ + data: { + user: { connect: { id: user.id } }, + type: "RESET_PASSWORD", + expiresAt, + hashedToken, + sentTo: user.email, + }, + }) + // 6. Send the email + await forgotPasswordMailer({ to: user.email, token }).send() + } else { + // 7. If no user found wait the same time so attackers can't tell the difference + await new Promise((resolve) => setTimeout(resolve, 750)) + } + + // 8. Return the same result whether a password reset email was sent or not + return +}) diff --git a/examples/cypress/app/auth/mutations/login.ts b/examples/cypress/app/auth/mutations/login.ts new file mode 100644 index 0000000000..1b715ec4bb --- /dev/null +++ b/examples/cypress/app/auth/mutations/login.ts @@ -0,0 +1,31 @@ +import { resolver, SecurePassword, AuthenticationError } from "blitz" +import db from "db" +import { Login } from "../validations" +import { Role } from "types" + +export const authenticateUser = async (rawEmail: string, rawPassword: string) => { + const email = rawEmail.toLowerCase().trim() + const password = rawPassword.trim() + const user = await db.user.findFirst({ where: { email } }) + if (!user) throw new AuthenticationError() + + const result = await SecurePassword.verify(user.hashedPassword, password) + + if (result === SecurePassword.VALID_NEEDS_REHASH) { + // Upgrade hashed password with a more secure hash + const improvedHash = await SecurePassword.hash(password) + await db.user.update({ where: { id: user.id }, data: { hashedPassword: improvedHash } }) + } + + const { hashedPassword, ...rest } = user + return rest +} + +export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ctx) => { + // This throws an error if credentials are invalid + const user = await authenticateUser(email, password) + + await ctx.session.$create({ userId: user.id, role: user.role as Role }) + + return user +}) diff --git a/examples/cypress/app/auth/mutations/logout.ts b/examples/cypress/app/auth/mutations/logout.ts new file mode 100644 index 0000000000..c2f2fefd8d --- /dev/null +++ b/examples/cypress/app/auth/mutations/logout.ts @@ -0,0 +1,5 @@ +import { Ctx } from "blitz" + +export default async function logout(_: any, ctx: Ctx) { + return await ctx.session.$revoke() +} diff --git a/examples/cypress/app/auth/mutations/resetPassword.test.ts b/examples/cypress/app/auth/mutations/resetPassword.test.ts new file mode 100644 index 0000000000..d613716f53 --- /dev/null +++ b/examples/cypress/app/auth/mutations/resetPassword.test.ts @@ -0,0 +1,82 @@ +import resetPassword from "./resetPassword" +import db from "db" +import { hash256, SecurePassword } from "blitz" + +beforeEach(async () => { + await db.$reset() +}) + +const mockCtx: any = { + session: { + $create: jest.fn, + }, +} + +describe("resetPassword mutation", () => { + it("works correctly", async () => { + expect(true).toBe(true) + + // Create test user + const goodToken = "randomPasswordResetToken" + const expiredToken = "expiredRandomPasswordResetToken" + const future = new Date() + future.setHours(future.getHours() + 4) + const past = new Date() + past.setHours(past.getHours() - 4) + + const user = await db.user.create({ + data: { + email: "user@example.com", + tokens: { + // Create old token to ensure it's deleted + create: [ + { + type: "RESET_PASSWORD", + hashedToken: hash256(expiredToken), + expiresAt: past, + sentTo: "user@example.com", + }, + { + type: "RESET_PASSWORD", + hashedToken: hash256(goodToken), + expiresAt: future, + sentTo: "user@example.com", + }, + ], + }, + }, + include: { tokens: true }, + }) + + const newPassword = "newPassword" + + // Non-existent token + await expect( + resetPassword({ token: "no-token", password: "", passwordConfirmation: "" }, mockCtx) + ).rejects.toThrowError() + + // Expired token + await expect( + resetPassword( + { token: expiredToken, password: newPassword, passwordConfirmation: newPassword }, + mockCtx + ) + ).rejects.toThrowError() + + // Good token + await resetPassword( + { token: goodToken, password: newPassword, passwordConfirmation: newPassword }, + mockCtx + ) + + // Delete's the token + const numberOfTokens = await db.token.count({ where: { userId: user.id } }) + expect(numberOfTokens).toBe(0) + + // Updates user's password + const updatedUser = await db.user.findFirst({ where: { id: user.id } }) + expect(await SecurePassword.verify(updatedUser!.hashedPassword, newPassword)).toBe( + SecurePassword.VALID + ) + }) +}) diff --git a/examples/cypress/app/auth/mutations/resetPassword.ts b/examples/cypress/app/auth/mutations/resetPassword.ts new file mode 100644 index 0000000000..75d4d67626 --- /dev/null +++ b/examples/cypress/app/auth/mutations/resetPassword.ts @@ -0,0 +1,47 @@ +import { resolver, SecurePassword, hash256 } from "blitz" +import db from "db" +import { ResetPassword } from "../validations" +import login from "./login" + +export class ResetPasswordError extends Error { + name = "ResetPasswordError" + message = "Reset password link is invalid or it has expired." +} + +export default resolver.pipe(resolver.zod(ResetPassword), async ({ password, token }, ctx) => { + // 1. Try to find this token in the database + const hashedToken = hash256(token) + const possibleToken = await db.token.findFirst({ + where: { hashedToken, type: "RESET_PASSWORD" }, + include: { user: true }, + }) + + // 2. If token not found, error + if (!possibleToken) { + throw new ResetPasswordError() + } + const savedToken = possibleToken + + // 3. Delete token so it can't be used again + await db.token.delete({ where: { id: savedToken.id } }) + + // 4. If token has expired, error + if (savedToken.expiresAt < new Date()) { + throw new ResetPasswordError() + } + + // 5. Since token is valid, now we can update the user's password + const hashedPassword = await SecurePassword.hash(password.trim()) + const user = await db.user.update({ + where: { id: savedToken.userId }, + data: { hashedPassword }, + }) + + // 6. Revoke all existing login sessions for this user + await db.session.deleteMany({ where: { userId: user.id } }) + + // 7. Now log the user in with the new credentials + await login({ email: user.email, password }, ctx) + + return true +}) diff --git a/examples/cypress/app/auth/mutations/signup.ts b/examples/cypress/app/auth/mutations/signup.ts new file mode 100644 index 0000000000..4791a99f43 --- /dev/null +++ b/examples/cypress/app/auth/mutations/signup.ts @@ -0,0 +1,15 @@ +import { resolver, SecurePassword } from "blitz" +import db from "db" +import { Signup } from "app/auth/validations" +import { Role } from "types" + +export default resolver.pipe(resolver.zod(Signup), async ({ email, password }, ctx) => { + const hashedPassword = await SecurePassword.hash(password.trim()) + const user = await db.user.create({ + data: { email: email.toLowerCase().trim(), hashedPassword, role: "USER" }, + select: { id: true, name: true, email: true, role: true }, + }) + + await ctx.session.$create({ userId: user.id, role: user.role as Role }) + return user +}) diff --git a/examples/cypress/app/auth/pages/forgot-password.tsx b/examples/cypress/app/auth/pages/forgot-password.tsx new file mode 100644 index 0000000000..c2509982df --- /dev/null +++ b/examples/cypress/app/auth/pages/forgot-password.tsx @@ -0,0 +1,48 @@ +import { BlitzPage, useMutation } from "blitz" +import Layout from "app/core/layouts/Layout" +import { LabeledTextField } from "app/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/core/components/Form" +import { ForgotPassword } from "app/auth/validations" +import forgotPassword from "app/auth/mutations/forgotPassword" + +const ForgotPasswordPage: BlitzPage = () => { + const [forgotPasswordMutation, { isSuccess }] = useMutation(forgotPassword) + + return ( +
+

Forgot your password?

+ + {isSuccess ? ( +
+

Request Submitted

+

+ If your email is in our system, you will receive instructions to reset your password + shortly. +

+
+ ) : ( +
{ + try { + await forgotPasswordMutation(values) + } catch (error: any) { + return { + [FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.", + } + } + }} + > + + + )} +
+ ) +} + +ForgotPasswordPage.redirectAuthenticatedTo = "/" +ForgotPasswordPage.getLayout = (page) => {page} + +export default ForgotPasswordPage diff --git a/examples/cypress/app/auth/pages/login.tsx b/examples/cypress/app/auth/pages/login.tsx new file mode 100644 index 0000000000..de51b126df --- /dev/null +++ b/examples/cypress/app/auth/pages/login.tsx @@ -0,0 +1,23 @@ +import { useRouter, BlitzPage } from "blitz" +import Layout from "app/core/layouts/Layout" +import { LoginForm } from "app/auth/components/LoginForm" + +const LoginPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ { + const next = router.query.next ? decodeURIComponent(router.query.next as string) : "/" + router.push(next) + }} + /> +
+ ) +} + +LoginPage.redirectAuthenticatedTo = "/" +LoginPage.getLayout = (page) => {page} + +export default LoginPage diff --git a/examples/cypress/app/auth/pages/reset-password.tsx b/examples/cypress/app/auth/pages/reset-password.tsx new file mode 100644 index 0000000000..1833510611 --- /dev/null +++ b/examples/cypress/app/auth/pages/reset-password.tsx @@ -0,0 +1,61 @@ +import { BlitzPage, useRouterQuery, Link, useMutation } from "blitz" +import Layout from "app/core/layouts/Layout" +import { LabeledTextField } from "app/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/core/components/Form" +import { ResetPassword } from "app/auth/validations" +import resetPassword from "app/auth/mutations/resetPassword" + +import { Routes } from ".blitz" + +const ResetPasswordPage: BlitzPage = () => { + const query = useRouterQuery() + const [resetPasswordMutation, { isSuccess }] = useMutation(resetPassword) + + return ( +
+

Set a New Password

+ + {isSuccess ? ( +
+

Password Reset Successfully

+

+ Go to the homepage +

+
+ ) : ( +
{ + try { + await resetPasswordMutation(values) + } catch (error: any) { + if (error.name === "ResetPasswordError") { + return { + [FORM_ERROR]: error.message, + } + } else { + return { + [FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.", + } + } + } + }} + > + + + + )} +
+ ) +} + +ResetPasswordPage.redirectAuthenticatedTo = "/" +ResetPasswordPage.getLayout = (page) => {page} + +export default ResetPasswordPage diff --git a/examples/cypress/app/auth/pages/signup.tsx b/examples/cypress/app/auth/pages/signup.tsx new file mode 100644 index 0000000000..a08c6554e4 --- /dev/null +++ b/examples/cypress/app/auth/pages/signup.tsx @@ -0,0 +1,20 @@ +import { useRouter, BlitzPage } from "blitz" +import Layout from "app/core/layouts/Layout" +import { SignupForm } from "app/auth/components/SignupForm" + +import { Routes } from ".blitz" + +const SignupPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ router.push(Routes.Home())} /> +
+ ) +} + +SignupPage.redirectAuthenticatedTo = "/" +SignupPage.getLayout = (page) => {page} + +export default SignupPage diff --git a/examples/cypress/app/auth/validations.ts b/examples/cypress/app/auth/validations.ts new file mode 100644 index 0000000000..fe47769166 --- /dev/null +++ b/examples/cypress/app/auth/validations.ts @@ -0,0 +1,33 @@ +import { z } from "zod" + +const password = z.string().min(10).max(100) + +export const Signup = z.object({ + email: z.string().email(), + password, +}) + +export const Login = z.object({ + email: z.string().email(), + password: z.string(), +}) + +export const ForgotPassword = z.object({ + email: z.string().email(), +}) + +export const ResetPassword = z + .object({ + password: password, + passwordConfirmation: password, + token: z.string(), + }) + .refine((data) => data.password === data.passwordConfirmation, { + message: "Passwords don't match", + path: ["passwordConfirmation"], // set the path of the error + }) + +export const ChangePassword = z.object({ + currentPassword: z.string(), + newPassword: password, +}) diff --git a/examples/cypress/app/core/components/Form.tsx b/examples/cypress/app/core/components/Form.tsx new file mode 100644 index 0000000000..c90fa74a68 --- /dev/null +++ b/examples/cypress/app/core/components/Form.tsx @@ -0,0 +1,53 @@ +import { ReactNode, PropsWithoutRef } from "react" +import { Form as FinalForm, FormProps as FinalFormProps } from "react-final-form" +import { z } from "zod" +import { validateZodSchema } from "blitz" +export { FORM_ERROR } from "final-form" + +export interface FormProps> + extends Omit, "onSubmit"> { + /** All your form fields */ + children?: ReactNode + /** Text to display in the submit button */ + submitText?: string + schema?: S + onSubmit: FinalFormProps>["onSubmit"] + initialValues?: FinalFormProps>["initialValues"] +} + +export function Form>({ + children, + submitText, + schema, + initialValues, + onSubmit, + ...props +}: FormProps) { + return ( + ( +
+ {/* Form fields supplied as children are rendered here */} + {children} + + {submitError && ( +
+ {submitError} +
+ )} + + {submitText && ( + + )} +
+ )} + /> + ) +} + +export default Form diff --git a/examples/cypress/app/core/components/LabeledTextField.tsx b/examples/cypress/app/core/components/LabeledTextField.tsx new file mode 100644 index 0000000000..f426ba5ceb --- /dev/null +++ b/examples/cypress/app/core/components/LabeledTextField.tsx @@ -0,0 +1,49 @@ +import { forwardRef, ComponentPropsWithoutRef, PropsWithoutRef } from "react" +import { useField, UseFieldConfig } from "react-final-form" + +export interface LabeledTextFieldProps extends PropsWithoutRef { + /** Field name. */ + name: string + /** Field label. */ + label: string + /** Field type. Doesn't include radio buttons and checkboxes */ + type?: "text" | "password" | "email" | "number" + outerProps?: PropsWithoutRef + labelProps?: ComponentPropsWithoutRef<"label"> + fieldProps?: UseFieldConfig +} + +export const LabeledTextField = forwardRef( + ({ name, label, outerProps, fieldProps, labelProps, ...props }, ref) => { + const { + input, + meta: { touched, error, submitError, submitting }, + } = useField(name, { + parse: + props.type === "number" + ? (Number as any) + : // Converting `""` to `null` ensures empty values will be set to null in the DB + (v) => (v === "" ? null : v), + ...fieldProps, + }) + + const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError + + return ( +
+ + + {touched && normalizedError && ( +
+ {normalizedError} +
+ )} +
+ ) + } +) + +export default LabeledTextField diff --git a/examples/cypress/app/core/hooks/useCurrentUser.ts b/examples/cypress/app/core/hooks/useCurrentUser.ts new file mode 100644 index 0000000000..2938468c7b --- /dev/null +++ b/examples/cypress/app/core/hooks/useCurrentUser.ts @@ -0,0 +1,7 @@ +import { useQuery } from "blitz" +import getCurrentUser from "app/users/queries/getCurrentUser" + +export const useCurrentUser = () => { + const [user] = useQuery(getCurrentUser, null) + return user +} diff --git a/examples/cypress/app/core/layouts/Layout.tsx b/examples/cypress/app/core/layouts/Layout.tsx new file mode 100644 index 0000000000..d403d1fce6 --- /dev/null +++ b/examples/cypress/app/core/layouts/Layout.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from "react" +import { Head } from "blitz" + +type LayoutProps = { + title?: string + children: ReactNode +} + +const Layout = ({ title, children }: LayoutProps) => { + return ( + <> + + {title || "cypress"} + + + + {children} + + ) +} + +export default Layout diff --git a/examples/cypress/app/pages/404.tsx b/examples/cypress/app/pages/404.tsx new file mode 100644 index 0000000000..ab81189e6c --- /dev/null +++ b/examples/cypress/app/pages/404.tsx @@ -0,0 +1,19 @@ +import { Head, ErrorComponent } from "blitz" + +// ------------------------------------------------------ +// This page is rendered if a route match is not found +// ------------------------------------------------------ +export default function Page404() { + const statusCode = 404 + const title = "This page could not be found" + return ( + <> + + + {statusCode}: {title} + + + + + ) +} diff --git a/examples/cypress/app/pages/_app.tsx b/examples/cypress/app/pages/_app.tsx new file mode 100644 index 0000000000..aad82b3ff2 --- /dev/null +++ b/examples/cypress/app/pages/_app.tsx @@ -0,0 +1,40 @@ +import { + AppProps, + ErrorBoundary, + ErrorComponent, + AuthenticationError, + AuthorizationError, + ErrorFallbackProps, + useQueryErrorResetBoundary, +} from "blitz" +import LoginForm from "app/auth/components/LoginForm" + +export default function App({ Component, pageProps }: AppProps) { + const getLayout = Component.getLayout || ((page) => page) + + return ( + + {getLayout()} + + ) +} + +function RootErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) { + if (error instanceof AuthenticationError) { + return + } else if (error instanceof AuthorizationError) { + return ( + + ) + } else { + return ( + + ) + } +} diff --git a/examples/cypress/app/pages/_document.tsx b/examples/cypress/app/pages/_document.tsx new file mode 100644 index 0000000000..78b9fc4d99 --- /dev/null +++ b/examples/cypress/app/pages/_document.tsx @@ -0,0 +1,23 @@ +import { Document, Html, DocumentHead, Main, BlitzScript /*DocumentContext*/ } from "blitz" + +class MyDocument extends Document { + // Only uncomment if you need to customize this behaviour + // static async getInitialProps(ctx: DocumentContext) { + // const initialProps = await Document.getInitialProps(ctx) + // return {...initialProps} + // } + + render() { + return ( + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/cypress/app/pages/index.test.tsx b/examples/cypress/app/pages/index.test.tsx new file mode 100644 index 0000000000..0eca16119b --- /dev/null +++ b/examples/cypress/app/pages/index.test.tsx @@ -0,0 +1,27 @@ +import { render } from "test/utils" +import Home from "./index" + +jest.mock("next/data-client", () => ({ + ...jest.requireActual("next/data-client")!, + useQuery: () => [ + { + id: 1, + name: "User", + email: "user@email.com", + role: "user", + }, + ], +})) + +test("renders blitz documentation link", () => { + // This is an example of how to ensure a specific item is in the document + // But it's disabled by default (by test.skip) so the test doesn't fail + // when you remove the the default content from the page + + // This is an example on how to mock api hooks when testing + + const { getByText } = render() + const element = getByText(/powered by blitz/i) + // @ts-ignore + expect(element).toBeInTheDocument() +}) diff --git a/examples/cypress/app/pages/index.tsx b/examples/cypress/app/pages/index.tsx new file mode 100644 index 0000000000..1c91c8a13a --- /dev/null +++ b/examples/cypress/app/pages/index.tsx @@ -0,0 +1,274 @@ +import { Suspense } from "react" +import { Image, Link, BlitzPage, useMutation } from "blitz" +import Layout from "app/core/layouts/Layout" +import { useCurrentUser } from "app/core/hooks/useCurrentUser" +import logout from "app/auth/mutations/logout" +import logo from "public/logo.png" + +import { Routes } from ".blitz" + +/* + * This file is just for a pleasant getting started page for your new app. + * You can delete everything in here and start from scratch if you like. + */ + +const UserInfo = () => { + const currentUser = useCurrentUser() + const [logoutMutation] = useMutation(logout) + + if (currentUser) { + return ( + <> + +
+ User id: {currentUser.id} +
+ User role: {currentUser.role} +
+ + ) + } else { + return ( + <> + + + Sign Up + + + + + Login + + + + ) + } +} + +const Home: BlitzPage = () => { + return ( +
+
+
+ blitzjs +
+

+ Congrats! Your app is ready, including user sign-up and log-in. +

+
+ + + +
+

+ + To add a new model to your app,
+ run the following in your terminal: +
+

+
+          blitz generate all project name:string
+        
+
(And select Yes to run prisma migrate)
+
+

+ Then restart the server +

+
+            Ctrl + c
+          
+
+            blitz dev
+          
+

+ and go to{" "} + + /projects + +

+
+ +
+ + + + +
+ ) +} + +Home.suppressFirstRenderFlicker = true +Home.getLayout = (page) => {page} + +export default Home diff --git a/examples/cypress/app/users/queries/getCurrentUser.ts b/examples/cypress/app/users/queries/getCurrentUser.ts new file mode 100644 index 0000000000..9822e16bf6 --- /dev/null +++ b/examples/cypress/app/users/queries/getCurrentUser.ts @@ -0,0 +1,13 @@ +import { Ctx } from "blitz" +import db from "db" + +export default async function getCurrentUser(_ = null, { session }: Ctx) { + if (!session.userId) return null + + const user = await db.user.findFirst({ + where: { id: session.userId }, + select: { id: true, name: true, email: true, role: true }, + }) + + return user +} diff --git a/examples/cypress/babel.config.js b/examples/cypress/babel.config.js new file mode 100644 index 0000000000..dfdf62cea1 --- /dev/null +++ b/examples/cypress/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["blitz/babel"], + plugins: [], +} diff --git a/examples/cypress/blitz.config.ts b/examples/cypress/blitz.config.ts new file mode 100644 index 0000000000..5f01cf396d --- /dev/null +++ b/examples/cypress/blitz.config.ts @@ -0,0 +1,19 @@ +import { BlitzConfig, sessionMiddleware, simpleRolesIsAuthorized } from "blitz" + +const config: BlitzConfig = { + middleware: [ + sessionMiddleware({ + cookiePrefix: "cypress-example", + isAuthorized: simpleRolesIsAuthorized, + }), + ], + /* Uncomment this to customize the webpack config + webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + // Important: return the modified config + return config + }, + */ +} +module.exports = config diff --git a/examples/cypress/cypress.json b/examples/cypress/cypress.json new file mode 100644 index 0000000000..be748dca53 --- /dev/null +++ b/examples/cypress/cypress.json @@ -0,0 +1,11 @@ +{ + "baseUrl": "http://localhost:3099", + "integrationFolder": "test/e2e", + "fixturesFolder": false, + "nodeVersion": "system", + "defaultCommandTimeout": 12000, + "pageLoadTimeout": 120000, + "ignoreTestFiles": ["tsconfig.json"], + "chromeWebSecurity": false, + "video": false +} diff --git a/examples/cypress/cypress/.eslintrc.js b/examples/cypress/cypress/.eslintrc.js new file mode 100644 index 0000000000..c2a9e593d5 --- /dev/null +++ b/examples/cypress/cypress/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + plugins: ["cypress"], + env: { + "cypress/globals": true, + }, + extends: ["plugin:cypress/recommended"], + rules: { + "cypress/no-unnecessary-waiting": "off", + }, +} diff --git a/examples/cypress/cypress/global.d.ts b/examples/cypress/cypress/global.d.ts new file mode 100644 index 0000000000..13a74c3788 --- /dev/null +++ b/examples/cypress/cypress/global.d.ts @@ -0,0 +1,22 @@ +declare namespace Cypress { + export interface Chainable { + // task for totally resetting the database + task(event: "db:reset", options?: Partial): Chainable + // task for deleting db data without dropping tables + task(event: "db:clear", options?: Partial): Chainable + // task for seeding db with essential data for tests + task(event: "db:seed", options?: Partial): Chainable + + // task for creating a new factory + task( + event: "factory", + args: { name: string; attrs?: any }, + options?: Partial + ): Chainable + + /** + * Logs-in user by using API request + */ + login({ email: string, password: string }): Chainable + } +} diff --git a/examples/cypress/cypress/plugins/index.ts b/examples/cypress/cypress/plugins/index.ts new file mode 100644 index 0000000000..b7967503bf --- /dev/null +++ b/examples/cypress/cypress/plugins/index.ts @@ -0,0 +1,55 @@ +process.env.NODE_ENV = "test" + +import { loadEnvConfig } from "@blitzjs/env" +loadEnvConfig() + +import "./register-ts-paths" +import db from "db" +import seed from "db/seeds" +import { factory } from "test/factories" + +let dbSetup = false + +const pluginConfig: Cypress.PluginConfig = (on, _config) => { + on("task", { + factory, + "db:reset": async () => { + if (!dbSetup) { + try { + // Only need to do this once at startup + console.log("Resetting database...") + await db.$reset() + console.log("Database reset.") + dbSetup = true + } catch (error) { + console.error(error) + throw new Error("Failed to set up database in cypress/plugins/index.ts") + } + } + return true + }, + // for Postgres + // "db:clear": () => { + // Delete all data without dropping tables, so migration isn't required + // await db.$executeRaw` + // do + // $$ + // declare + // l_stmt text; + // begin + // select 'truncate ' || string_agg(format('%I.%I', schemaname, tablename), ',') + // into l_stmt from pg_tables + // where schemaname in ('public'); + // execute l_stmt; + // end; + // $$ + // ` + // return true + // }, + "db:seed": async () => { + await seed() + return true + }, + }) +} +export default pluginConfig diff --git a/examples/cypress/cypress/plugins/register-ts-paths.ts b/examples/cypress/cypress/plugins/register-ts-paths.ts new file mode 100644 index 0000000000..544c69d023 --- /dev/null +++ b/examples/cypress/cypress/plugins/register-ts-paths.ts @@ -0,0 +1,2 @@ +// @ts-ignore +require("tsconfig-paths").register() diff --git a/examples/cypress/cypress/support/commands.ts b/examples/cypress/cypress/support/commands.ts new file mode 100644 index 0000000000..5594db4a20 --- /dev/null +++ b/examples/cypress/cypress/support/commands.ts @@ -0,0 +1,10 @@ +import "@testing-library/cypress/add-commands" + +Cypress.Commands.add("login", ({ email, password }) => { + return cy.request("POST", `/api/rpc/login`, { + params: { + email, + password, + }, + }) +}) diff --git a/examples/cypress/cypress/support/index.ts b/examples/cypress/cypress/support/index.ts new file mode 100644 index 0000000000..98a43a84b5 --- /dev/null +++ b/examples/cypress/cypress/support/index.ts @@ -0,0 +1,24 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** +import "./commands" + +before(() => { + cy.task("db:reset") +}) + +beforeEach(() => { + // you can clear the database here + cy.task("db:seed") +}) diff --git a/examples/cypress/cypress/tsconfig.json b/examples/cypress/cypress/tsconfig.json new file mode 100644 index 0000000000..e45ed56173 --- /dev/null +++ b/examples/cypress/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["cypress", "@types/testing-library__cypress"], + "isolatedModules": false, + "tsBuildInfoFile": ".tsbuildinfo.test" + }, + "exclude": ["node_modules"], + "include": ["**/*.ts"] +} diff --git a/examples/cypress/db/index.ts b/examples/cypress/db/index.ts new file mode 100644 index 0000000000..a63b57b248 --- /dev/null +++ b/examples/cypress/db/index.ts @@ -0,0 +1,7 @@ +import { enhancePrisma } from "blitz" +import { PrismaClient } from "@prisma/client" + +const EnhancedPrisma = enhancePrisma(PrismaClient) + +export * from "@prisma/client" +export default new EnhancedPrisma() diff --git a/examples/cypress/db/migrations/20211012185114_init/migration.sql b/examples/cypress/db/migrations/20211012185114_init/migration.sql new file mode 100644 index 0000000000..0b34cc718b --- /dev/null +++ b/examples/cypress/db/migrations/20211012185114_init/migration.sql @@ -0,0 +1,47 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "name" TEXT, + "email" TEXT NOT NULL, + "hashedPassword" TEXT, + "role" TEXT NOT NULL DEFAULT 'USER' +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "expiresAt" DATETIME, + "handle" TEXT NOT NULL, + "hashedSessionToken" TEXT, + "antiCSRFToken" TEXT, + "publicData" TEXT, + "privateData" TEXT, + "userId" INTEGER, + FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Token" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "hashedToken" TEXT NOT NULL, + "type" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + "sentTo" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session.handle_unique" ON "Session"("handle"); + +-- CreateIndex +CREATE UNIQUE INDEX "Token.hashedToken_type_unique" ON "Token"("hashedToken", "type"); diff --git a/examples/cypress/db/migrations/migration_lock.toml b/examples/cypress/db/migrations/migration_lock.toml new file mode 100644 index 0000000000..e5e5c4705a --- /dev/null +++ b/examples/cypress/db/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/examples/cypress/db/schema.prisma b/examples/cypress/db/schema.prisma new file mode 100644 index 0000000000..83cd1b80f6 --- /dev/null +++ b/examples/cypress/db/schema.prisma @@ -0,0 +1,65 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +} + +generator client { + provider = "prisma-client-js" +} + +// -------------------------------------- + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String? + email String @unique + hashedPassword String? + role String @default("USER") + + tokens Token[] + sessions Session[] +} + +model Session { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + expiresAt DateTime? + handle String @unique + hashedSessionToken String? + antiCSRFToken String? + publicData String? + privateData String? + + user User? @relation(fields: [userId], references: [id]) + userId Int? +} + +model Token { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + hashedToken String + type String + // See note below about TokenType enum + // type TokenType + expiresAt DateTime + sentTo String + + user User @relation(fields: [userId], references: [id]) + userId Int + + @@unique([hashedToken, type]) +} + +// NOTE: It's highly recommended to use an enum for the token type +// but enums only work in Postgres. +// See: https://blitzjs.com/docs/database-overview#switch-to-postgre-sql +// enum TokenType { +// RESET_PASSWORD +// } diff --git a/examples/cypress/db/seeds.ts b/examples/cypress/db/seeds.ts new file mode 100644 index 0000000000..cc39e51b95 --- /dev/null +++ b/examples/cypress/db/seeds.ts @@ -0,0 +1,15 @@ +// import db from "./index" + +/* + * This seed function is executed when you run `blitz db seed`. + * + * Probably you want to use a library like https://chancejs.com + * to easily generate realistic data. + */ +const seed = async () => { + // for (let i = 0; i < 5; i++) { + // await db.project.create({ data: { name: "Project " + i } }) + // } +} + +export default seed diff --git a/examples/cypress/jest.config.ts b/examples/cypress/jest.config.ts new file mode 100644 index 0000000000..30fe3fc967 --- /dev/null +++ b/examples/cypress/jest.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@jest/types" + +const config: Config.InitialOptions = { + preset: "blitz", +} + +export default config diff --git a/examples/cypress/mailers/forgotPasswordMailer.ts b/examples/cypress/mailers/forgotPasswordMailer.ts new file mode 100644 index 0000000000..5005e42fe4 --- /dev/null +++ b/examples/cypress/mailers/forgotPasswordMailer.ts @@ -0,0 +1,45 @@ +/* TODO - You need to add a mailer integration in `integrations/` and import here. + * + * The integration file can be very simple. Instantiate the email client + * and then export it. That way you can import here and anywhere else + * and use it straight away. + */ + +type ResetPasswordMailer = { + to: string + token: string +} + +export function forgotPasswordMailer({ to, token }: ResetPasswordMailer) { + // In production, set APP_ORIGIN to your production server origin + const origin = process.env.APP_ORIGIN || process.env.BLITZ_DEV_SERVER_ORIGIN + const resetUrl = `${origin}/reset-password?token=${token}` + + const msg = { + from: "TODO@example.com", + to, + subject: "Your Password Reset Instructions", + html: ` +

Reset Your Password

+

NOTE: You must set up a production email integration in mailers/forgotPasswordMailer.ts

+ + + Click here to set a new password + + `, + } + + return { + async send() { + if (process.env.NODE_ENV === "production") { + // TODO - send the production email, like this: + // await postmark.sendEmail(msg) + throw new Error("No production email implementation in mailers/forgotPasswordMailer") + } else { + // Preview email in the browser + const previewEmail = (await import("preview-email")).default + await previewEmail(msg) + } + }, + } +} diff --git a/examples/cypress/package.json b/examples/cypress/package.json new file mode 100644 index 0000000000..b47f2be8a3 --- /dev/null +++ b/examples/cypress/package.json @@ -0,0 +1,62 @@ +{ + "name": "@examples/cypress", + "version": "0.41.1", + "scripts": { + "dev": "blitz dev", + "build": "blitz build", + "start": "blitz start", + "studio": "blitz prisma studio", + "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", + "cy:open": "cypress open", + "cy:run": "cypress run", + "test:server": "blitz prisma migrate deploy && blitz build && blitz start -p 3099", + "test:e2e": "cross-env NODE_ENV=test start-server-and-test test:server http://localhost:3099 cy:run", + "test:jest": "jest --passWithNoTests", + "test": "blitz codegen && prisma generate && yarn test:jest && yarn test:e2e" + }, + "prisma": { + "schema": "db/schema.prisma" + }, + "prettier": { + "semi": false, + "printWidth": 100 + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "eslint --fix" + ] + }, + "husky": { + "hooks": { + "pre-commit": "tsc && lint-staged && pretty-quick --staged", + "pre-push": "npm run lint && npm run test" + } + }, + "dependencies": { + "@prisma/client": "2.24.1", + "blitz": "0.45.4", + "final-form": "4.20.1", + "react": "0.0.0-experimental-6a589ad71", + "react-dom": "0.0.0-experimental-6a589ad71", + "react-final-form": "6.5.2", + "zod": "3.10.1" + }, + "devDependencies": { + "@testing-library/cypress": "8.0.1", + "@types/preview-email": "2.0.0", + "@types/react": "17.0.2", + "@types/testing-library__cypress": "5.0.9", + "chance": "1.1.8", + "cross-env": "7.0.3", + "cypress": "6.2.1", + "eslint": "7.21.0", + "husky": "5.1.2", + "lint-staged": "10.5.4", + "prettier": "2.2.1", + "pretty-quick": "3.1.0", + "preview-email": "3.0.3", + "prisma": "2.24.1", + "start-server-and-test": "1.11.7" + }, + "private": true +} diff --git a/examples/cypress/public/favicon.ico b/examples/cypress/public/favicon.ico new file mode 100755 index 0000000000..c7bd1c095d Binary files /dev/null and b/examples/cypress/public/favicon.ico differ diff --git a/examples/cypress/public/logo.png b/examples/cypress/public/logo.png new file mode 100644 index 0000000000..6a982616ee Binary files /dev/null and b/examples/cypress/public/logo.png differ diff --git a/examples/cypress/test/e2e/.eslintrc.js b/examples/cypress/test/e2e/.eslintrc.js new file mode 100644 index 0000000000..49eb3030c8 --- /dev/null +++ b/examples/cypress/test/e2e/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["../../cypress/.eslintrc.js"], +} diff --git a/examples/cypress/test/e2e/login.e2e.ts b/examples/cypress/test/e2e/login.e2e.ts new file mode 100644 index 0000000000..ce70626059 --- /dev/null +++ b/examples/cypress/test/e2e/login.e2e.ts @@ -0,0 +1,35 @@ +import { User } from "test/factories" + +describe("Login", () => { + describe("with an email that doesnt exist", () => { + it("shows an error", () => { + const email = "nowayshouldIexist@example.com" + const password = "test1234" + + cy.visit("/login").wait(100) + + cy.findByLabelText(/email/i).type(email) + cy.findByLabelText(/password/i).type(password) + cy.findAllByRole("button", { name: /login/i }).click() + + cy.findByText(/invalid/i).should("exist") + }) + }) + + describe("with valid credentials", () => { + it("logs in", () => { + const attrs = { password: "superstrongpassword" } + + cy.visit("/login").wait(100) + + cy.task("factory", { name: "user", attrs }).then((user: User) => { + cy.findByLabelText(/email/i).type(user.email) + cy.findByLabelText(/password/i).type(attrs.password) + cy.findAllByRole("button", { name: /login/i }).click() + + cy.location("pathname").should("equal", "/") + cy.findByText(/logout/i).should("exist") + }) + }) + }) +}) diff --git a/examples/cypress/test/e2e/signup.e2e.ts b/examples/cypress/test/e2e/signup.e2e.ts new file mode 100644 index 0000000000..1b22ea75bc --- /dev/null +++ b/examples/cypress/test/e2e/signup.e2e.ts @@ -0,0 +1,14 @@ +describe("Signup", () => { + it("creates new account", () => { + const attrs = { email: "blitz@example.com", password: "superstrongpassword" } + + cy.visit("/signup").wait(100) + + cy.findByLabelText(/email/i).type(attrs.email) + cy.findByLabelText(/password/i).type(attrs.password) + cy.findAllByRole("button", { name: /create account/i }).click() + + cy.location("pathname").should("equal", "/") + cy.findByText(/logout/i).should("exist") + }) +}) diff --git a/examples/cypress/test/e2e/tsconfig.json b/examples/cypress/test/e2e/tsconfig.json new file mode 100644 index 0000000000..da1fd184db --- /dev/null +++ b/examples/cypress/test/e2e/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["cypress", "@types/testing-library__cypress"], + "isolatedModules": false + }, + "exclude": ["node_modules"], + "include": ["**/*.ts", "../../cypress/**/*.ts"] +} diff --git a/examples/cypress/test/factories/index.ts b/examples/cypress/test/factories/index.ts new file mode 100644 index 0000000000..e6d3d2a32d --- /dev/null +++ b/examples/cypress/test/factories/index.ts @@ -0,0 +1,16 @@ +import { user } from "./user" +export * from "./user" + +export const Factories = { + user, +} + +export function factory({ + name, + attrs, +}: { + name: T + attrs: Parameters[0] +}) { + return Factories[name](attrs) +} diff --git a/examples/cypress/test/factories/user.ts b/examples/cypress/test/factories/user.ts new file mode 100644 index 0000000000..16ea486560 --- /dev/null +++ b/examples/cypress/test/factories/user.ts @@ -0,0 +1,22 @@ +import { PromiseReturnType } from "next/types/utils" +import signup from "app/auth/mutations/signup" +import Chance from "chance" + +const chance = new Chance() + +const ctx = { + session: { $create: () => {} }, +} + +type UserAttributes = { + email?: string + password: string +} + +export const user = async ({ email = chance.email(), password }: UserAttributes) => { + const user = await signup({ email, password }, ctx as any) + + return user +} + +export type User = PromiseReturnType diff --git a/examples/cypress/test/setup.ts b/examples/cypress/test/setup.ts new file mode 100644 index 0000000000..c278ddb9e6 --- /dev/null +++ b/examples/cypress/test/setup.ts @@ -0,0 +1,4 @@ +// This is the jest 'setupFilesAfterEnv' setup file +// It's a good place to set globals, add global before/after hooks, etc + +export {} // so TS doesn't complain diff --git a/examples/cypress/test/utils.tsx b/examples/cypress/test/utils.tsx new file mode 100644 index 0000000000..6d52b9baa7 --- /dev/null +++ b/examples/cypress/test/utils.tsx @@ -0,0 +1,105 @@ +import { RouterContext, BlitzRouter, BlitzProvider } from "blitz" +import { render as defaultRender } from "@testing-library/react" +import { renderHook as defaultRenderHook } from "@testing-library/react-hooks" + +export * from "@testing-library/react" + +// -------------------------------------------------------------------------------- +// This file customizes the render() and renderHook() test functions provided +// by React testing library. It adds a router context wrapper with a mocked router. +// +// You should always import `render` and `renderHook` from this file +// +// This is the place to add any other context providers you need while testing. +// -------------------------------------------------------------------------------- + +// -------------------------------------------------- +// render() +// -------------------------------------------------- +// Override the default test render with our own +// +// You can override the router mock like this: +// +// const { baseElement } = render(, { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function render( + ui: RenderUI, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }) => ( + + + {children} + + + ) + } + return defaultRender(ui, { wrapper, ...options }) +} + +// -------------------------------------------------- +// renderHook() +// -------------------------------------------------- +// Override the default test renderHook with our own +// +// You can override the router mock like this: +// +// const result = renderHook(() => myHook(), { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function renderHook( + hook: RenderHook, + { wrapper, router, dehydratedState, ...options }: RenderHookOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }) => ( + + + {children} + + + ) + } + return defaultRenderHook(hook, { wrapper, ...options }) +} + +export const mockRouter: BlitzRouter = { + basePath: "", + pathname: "/", + route: "/", + asPath: "/", + params: {}, + query: {}, + isReady: true, + isLocaleDomain: false, + isPreview: false, + push: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + isFallback: false, +} + +type DefaultParams = Parameters +type RenderUI = DefaultParams[0] +type RenderOptions = DefaultParams[1] & { router?: Partial; dehydratedState?: unknown } + +type DefaultHookParams = Parameters +type RenderHook = DefaultHookParams[0] +type RenderHookOptions = DefaultHookParams[1] & { + router?: Partial + dehydratedState?: unknown +} diff --git a/examples/cypress/tsconfig.json b/examples/cypress/tsconfig.json new file mode 100644 index 0000000000..670aedfca5 --- /dev/null +++ b/examples/cypress/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "types": ["jest", "@testing-library/react", "@testing-library/jest-dom"], + "baseUrl": "./", + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "exclude": ["node_modules", "**/*.e2e.ts", "cypress"], + "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"] +} diff --git a/examples/cypress/types.ts b/examples/cypress/types.ts new file mode 100644 index 0000000000..284b103fc9 --- /dev/null +++ b/examples/cypress/types.ts @@ -0,0 +1,25 @@ +import { DefaultCtx, SessionContext, SimpleRolesIsAuthorized } from "blitz" +import { User } from "db" + +export type Role = "ADMIN" | "USER" + +declare module "blitz" { + export interface Ctx extends DefaultCtx { + session: SessionContext + } + export interface Session { + isAuthorized: SimpleRolesIsAuthorized + PublicData: { + userId: User["id"] + role: Role + } + } +} + +// This should not be needed. Usually it isn't but for some reason in this example it is +declare module "react" { + interface StyleHTMLAttributes extends React.HTMLAttributes { + jsx?: boolean + global?: boolean + } +} diff --git a/examples/fauna/.env b/examples/fauna/.env new file mode 100644 index 0000000000..9e261f1218 --- /dev/null +++ b/examples/fauna/.env @@ -0,0 +1,2 @@ +# This env file should be checked into source control +# This is the place for default values that should be used in all environments diff --git a/examples/fauna/.eslintrc.js b/examples/fauna/.eslintrc.js new file mode 100644 index 0000000000..71f356cbaf --- /dev/null +++ b/examples/fauna/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + env: { + es2020: true, + }, + extends: ["react-app", "plugin:jsx-a11y/recommended"], + plugins: ["jsx-a11y"], + rules: { + "import/no-anonymous-default-export": "error", + "import/no-webpack-loader-syntax": "off", + "react/react-in-jsx-scope": "off", // React is always in scope with Blitz + "jsx-a11y/anchor-is-valid": "off", //Doesn't play well with Blitz/Next usage + }, +} diff --git a/examples/fauna/.gitignore b/examples/fauna/.gitignore new file mode 100644 index 0000000000..3665bf5ce0 --- /dev/null +++ b/examples/fauna/.gitignore @@ -0,0 +1,52 @@ +# dependencies +node_modules +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* +.npm +web_modules/ + +# blitz +/.blitz/ +/.next/ +*.sqlite +.now +.blitz-console-history +blitz-log.log + +# misc +.DS_Store + +# local env files +.env.local +.env.*.local +.envrc + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +.coverage +*.lcov +.nyc_output +lib-cov + +# Caches +*.tsbuildinfo +.eslintcache +.node_repl_history +.yarn-integrity + +# Serverless directories +.serverless/ + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test diff --git a/examples/fauna/.npmrc b/examples/fauna/.npmrc new file mode 100644 index 0000000000..1b78f1c6f2 --- /dev/null +++ b/examples/fauna/.npmrc @@ -0,0 +1,2 @@ +save-exact=true +legacy-peer-deps=true diff --git a/examples/fauna/.prettierignore b/examples/fauna/.prettierignore new file mode 100644 index 0000000000..c61fdf72dc --- /dev/null +++ b/examples/fauna/.prettierignore @@ -0,0 +1,5 @@ +.gitkeep +.env* +*.ico +*.lock +db/migrations diff --git a/examples/fauna/README.md b/examples/fauna/README.md new file mode 100644 index 0000000000..c1f0aa21e5 --- /dev/null +++ b/examples/fauna/README.md @@ -0,0 +1,42 @@ +# Blitz Fauna Example + +## Intro + +This example shows how to use [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=BlitzJS&utm_medium=sponsorship&utm_campaign=BlitzJS_Sponsorship_2020) instead of Prisma and Postgres. + +The bulk of the integration is in the following files: + +- `blitz.config.js` +- `db/index.ts` + +And then also the queries and mutations use Fauna. + +By far the main integration work is providing the auth session hooks for reading and writing session data to Fauna. All this is in `blitz.config.js`. + +This example use the Fauna GraphQL API since it's more familiar to most people than FQL. + +## Getting Started + +1. Sign up for a Fauna account +2. Create a new database +3. Click on the GraphQL menu item +4. Upload the graphql schema located at `db/schema.graphql` +5. Click on the Security menu item +6. Create a new auth key, and add the auth key to `.env.local` like this: +``` +FAUNA_SECRET=YOUR_AUTH_KEY +``` + +7. Add the Fauna GraphQL URL to `.env.local`. This URL depends on your [database region](https://docs.fauna.com/fauna/current/api/graphql/endpoints). For instance, for US databases it's `https://graphql.us.fauna.com/graphql` + +``` +FAUNA_GRAPHQL_URL=YOUR_FAUNA_GRAPHQL_URL +``` + +8. Start the dev server + +``` +yarn blitz dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/examples/fauna/app/auth/auth-utils.ts b/examples/fauna/app/auth/auth-utils.ts new file mode 100644 index 0000000000..fdf70aa8d7 --- /dev/null +++ b/examples/fauna/app/auth/auth-utils.ts @@ -0,0 +1,47 @@ +import { SecurePassword, AuthenticationError } from "blitz" +import db from "db" +import { gql } from "graphql-request" + +export const authenticateUser = async (email: string, password: string) => { + const { user } = await db.request( + gql` + query getUser($email: String!) { + user: findUserByEmail(email: $email) { + id: _id + email + name + role + hashedPassword + } + } + `, + { email: email.toLowerCase() } + ) + + if (!user || !user.hashedPassword) throw new AuthenticationError() + + const result = await SecurePassword.verify(user.hashedPassword, password) + + if (result === SecurePassword.VALID_NEEDS_REHASH) { + // Upgrade hashed password with a more secure hash + const improvedHash = await SecurePassword.hash(password) + await db.request( + gql` + mutation UpdateUser($data: UserInput!) { + updateUser(data: $data) { + id: _id + } + } + `, + { + data: { + id: user.id, + hashedPassword: improvedHash, + }, + } + ) + } + + const { hashedPassword, ...rest } = user + return rest +} diff --git a/examples/fauna/app/auth/components/LoginForm.tsx b/examples/fauna/app/auth/components/LoginForm.tsx new file mode 100644 index 0000000000..78069a9f8d --- /dev/null +++ b/examples/fauna/app/auth/components/LoginForm.tsx @@ -0,0 +1,49 @@ +import { Link, useMutation } from "blitz" +import { LabeledTextField } from "app/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/components/Form" +import login from "app/auth/mutations/login" +import { LoginInput } from "app/auth/validations" + +type LoginFormProps = { + onSuccess?: () => void +} + +export const LoginForm = (props: LoginFormProps) => { + const [loginMutation] = useMutation(login) + + return ( +
+

Login

+ +
{ + try { + await loginMutation(values) + props.onSuccess?.() + } catch (error) { + if (error.name === "AuthenticationError") { + return { [FORM_ERROR]: "Sorry, those credentials are invalid" } + } else { + return { + [FORM_ERROR]: + "Sorry, we had an unexpected error. Please try again. - " + error.toString(), + } + } + } + }} + > + + + + +
+ Or Sign Up +
+
+ ) +} + +export default LoginForm diff --git a/examples/fauna/app/auth/components/SignupForm.tsx b/examples/fauna/app/auth/components/SignupForm.tsx new file mode 100644 index 0000000000..3674c8e6f8 --- /dev/null +++ b/examples/fauna/app/auth/components/SignupForm.tsx @@ -0,0 +1,43 @@ +import { useMutation } from "blitz" +import { LabeledTextField } from "app/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/components/Form" +import signup from "app/auth/mutations/signup" +import { SignupInput } from "app/auth/validations" + +type SignupFormProps = { + onSuccess?: () => void +} + +export const SignupForm = (props: SignupFormProps) => { + const [signupMutation] = useMutation(signup) + + return ( +
+

Create an Account

+ +
{ + try { + await signupMutation(values) + props.onSuccess?.() + } catch (error) { + if (error.code === "P2002" && error.meta?.target?.includes("email")) { + // This error comes from Prisma + return { email: "This email is already being used" } + } else { + return { [FORM_ERROR]: error.toString() } + } + } + }} + > + + + +
+ ) +} + +export default SignupForm diff --git a/examples/fauna/app/auth/mutations/login.ts b/examples/fauna/app/auth/mutations/login.ts new file mode 100644 index 0000000000..b82a9a5aca --- /dev/null +++ b/examples/fauna/app/auth/mutations/login.ts @@ -0,0 +1,15 @@ +import { Ctx } from "blitz" +import { authenticateUser } from "app/auth/auth-utils" +import { LoginInput, LoginInputType } from "../validations" + +export default async function login(input: LoginInputType, { session }: Ctx) { + // This throws an error if input is invalid + const { email, password } = LoginInput.parse(input) + + // This throws an error if credentials are invalid + const user = await authenticateUser(email, password) + + await session.$create({ userId: user.id }) + + return user +} diff --git a/examples/fauna/app/auth/mutations/logout.ts b/examples/fauna/app/auth/mutations/logout.ts new file mode 100644 index 0000000000..f72c0bb165 --- /dev/null +++ b/examples/fauna/app/auth/mutations/logout.ts @@ -0,0 +1,5 @@ +import { Ctx } from "blitz" + +export default async function logout(_: any, { session }: Ctx) { + return await session.$revoke() +} diff --git a/examples/fauna/app/auth/mutations/signup.ts b/examples/fauna/app/auth/mutations/signup.ts new file mode 100644 index 0000000000..f7be4f0cb1 --- /dev/null +++ b/examples/fauna/app/auth/mutations/signup.ts @@ -0,0 +1,29 @@ +import { Ctx, SecurePassword } from "blitz" +import db from "db" +import { SignupInput, SignupInputType } from "app/auth/validations" +import { gql } from "graphql-request" + +export default async function signup(input: SignupInputType, { session }: Ctx) { + // This throws an error if input is invalid + const { email, password } = SignupInput.parse(input) + + const hashedPassword = await SecurePassword.hash(password) + const { user } = await db.request( + gql` + mutation createUser($email: String!, $hashedPassword: String, $role: String!) { + user: createUser(data: { email: $email, hashedPassword: $hashedPassword, role: $role }) { + id: _id + email + name + role + } + } + `, + { email: email.toLowerCase(), hashedPassword, role: "user" } + ) + console.log("Create user result:", user) + + await session.$create({ userId: user.id }) + + return user +} diff --git a/examples/fauna/app/auth/pages/login.tsx b/examples/fauna/app/auth/pages/login.tsx new file mode 100644 index 0000000000..c7ef917c7d --- /dev/null +++ b/examples/fauna/app/auth/pages/login.tsx @@ -0,0 +1,17 @@ +import { useRouter, BlitzPage } from "blitz" +import Layout from "app/layouts/Layout" +import { LoginForm } from "app/auth/components/LoginForm" + +const LoginPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ router.push("/")} /> +
+ ) +} + +LoginPage.getLayout = (page) => {page} + +export default LoginPage diff --git a/examples/fauna/app/auth/pages/signup.tsx b/examples/fauna/app/auth/pages/signup.tsx new file mode 100644 index 0000000000..e80df13f6c --- /dev/null +++ b/examples/fauna/app/auth/pages/signup.tsx @@ -0,0 +1,17 @@ +import { useRouter, BlitzPage } from "blitz" +import Layout from "app/layouts/Layout" +import { SignupForm } from "app/auth/components/SignupForm" + +const SignupPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ router.push("/")} /> +
+ ) +} + +SignupPage.getLayout = (page) => {page} + +export default SignupPage diff --git a/examples/fauna/app/auth/validations.ts b/examples/fauna/app/auth/validations.ts new file mode 100644 index 0000000000..db3ed82879 --- /dev/null +++ b/examples/fauna/app/auth/validations.ts @@ -0,0 +1,13 @@ +import { z } from "zod" + +export const SignupInput = z.object({ + email: z.string().email(), + password: z.string().min(10).max(100), +}) +export type SignupInputType = z.infer + +export const LoginInput = z.object({ + email: z.string().email(), + password: z.string(), +}) +export type LoginInputType = z.infer diff --git a/examples/fauna/app/components/.keep b/examples/fauna/app/components/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/fauna/app/components/Form.tsx b/examples/fauna/app/components/Form.tsx new file mode 100644 index 0000000000..72d35b3b83 --- /dev/null +++ b/examples/fauna/app/components/Form.tsx @@ -0,0 +1,58 @@ +import { ReactNode, PropsWithoutRef } from "react" +import { Form as FinalForm, FormProps as FinalFormProps } from "react-final-form" +import { z } from "zod" +import { validateZodSchema } from "blitz" +export { FORM_ERROR } from "final-form" + +type FormProps> = { + /** All your form fields */ + children: ReactNode + /** Text to display in the submit button */ + submitText?: string + schema?: S + onSubmit: FinalFormProps>["onSubmit"] + initialValues?: FinalFormProps>["initialValues"] +} & Omit, "onSubmit"> + +export function Form>({ + children, + submitText, + schema, + initialValues, + onSubmit, + ...props +}: FormProps) { + return ( + ( +
+ {/* Form fields supplied as children are rendered here */} + {children} + + {submitError && ( +
+ {submitError} +
+ )} + + {submitText && ( + + )} + + +
+ )} + /> + ) +} + +export default Form diff --git a/examples/fauna/app/components/LabeledTextField.tsx b/examples/fauna/app/components/LabeledTextField.tsx new file mode 100644 index 0000000000..bc4803b856 --- /dev/null +++ b/examples/fauna/app/components/LabeledTextField.tsx @@ -0,0 +1,57 @@ +import React, { PropsWithoutRef } from "react" +import { useField } from "react-final-form" + +export interface LabeledTextFieldProps extends PropsWithoutRef { + /** Field name. */ + name: string + /** Field label. */ + label: string + /** Field type. Doesn't include radio buttons and checkboxes */ + type?: "text" | "password" | "email" | "number" + outerProps?: PropsWithoutRef +} + +export const LabeledTextField = React.forwardRef( + ({ name, label, outerProps, ...props }, ref) => { + const { + input, + meta: { touched, error, submitError, submitting }, + } = useField(name) + + const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError + + return ( +
+ + + {touched && normalizedError && ( +
+ {normalizedError} +
+ )} + + +
+ ) + } +) + +export default LabeledTextField diff --git a/examples/fauna/app/hooks/useCurrentUser.ts b/examples/fauna/app/hooks/useCurrentUser.ts new file mode 100644 index 0000000000..2938468c7b --- /dev/null +++ b/examples/fauna/app/hooks/useCurrentUser.ts @@ -0,0 +1,7 @@ +import { useQuery } from "blitz" +import getCurrentUser from "app/users/queries/getCurrentUser" + +export const useCurrentUser = () => { + const [user] = useQuery(getCurrentUser, null) + return user +} diff --git a/examples/fauna/app/layouts/.keep b/examples/fauna/app/layouts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/fauna/app/layouts/Layout.tsx b/examples/fauna/app/layouts/Layout.tsx new file mode 100644 index 0000000000..04693d0e82 --- /dev/null +++ b/examples/fauna/app/layouts/Layout.tsx @@ -0,0 +1,16 @@ +import { Head, BlitzLayout } from "blitz" + +const Layout: BlitzLayout<{ title?: string }> = ({ title, children }) => { + return ( + <> + + {title || "fauna"} + + + + {children} + + ) +} + +export default Layout diff --git a/examples/fauna/app/pages/404.tsx b/examples/fauna/app/pages/404.tsx new file mode 100644 index 0000000000..ab81189e6c --- /dev/null +++ b/examples/fauna/app/pages/404.tsx @@ -0,0 +1,19 @@ +import { Head, ErrorComponent } from "blitz" + +// ------------------------------------------------------ +// This page is rendered if a route match is not found +// ------------------------------------------------------ +export default function Page404() { + const statusCode = 404 + const title = "This page could not be found" + return ( + <> + + + {statusCode}: {title} + + + + + ) +} diff --git a/examples/fauna/app/pages/_app.tsx b/examples/fauna/app/pages/_app.tsx new file mode 100644 index 0000000000..e7c090a676 --- /dev/null +++ b/examples/fauna/app/pages/_app.tsx @@ -0,0 +1,39 @@ +import { + AppProps, + ErrorBoundary, + ErrorFallbackProps, + ErrorComponent, + useQueryErrorResetBoundary, +} from "blitz" +import LoginForm from "app/auth/components/LoginForm" + +export default function App({ Component, pageProps }: AppProps) { + const getLayout = Component.getLayout || ((page) => page) + const { reset } = useQueryErrorResetBoundary() + + return ( + + {getLayout()} + + ) +} + +function RootErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) { + if (error?.name === "AuthenticationError") { + return + } else if (error?.name === "AuthorizationError") { + return ( + + ) + } else { + return ( + + ) + } +} diff --git a/examples/fauna/app/pages/_document.tsx b/examples/fauna/app/pages/_document.tsx new file mode 100644 index 0000000000..78b9fc4d99 --- /dev/null +++ b/examples/fauna/app/pages/_document.tsx @@ -0,0 +1,23 @@ +import { Document, Html, DocumentHead, Main, BlitzScript /*DocumentContext*/ } from "blitz" + +class MyDocument extends Document { + // Only uncomment if you need to customize this behaviour + // static async getInitialProps(ctx: DocumentContext) { + // const initialProps = await Document.getInitialProps(ctx) + // return {...initialProps} + // } + + render() { + return ( + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/fauna/app/pages/index.test.tsx b/examples/fauna/app/pages/index.test.tsx new file mode 100644 index 0000000000..1b6f2e6044 --- /dev/null +++ b/examples/fauna/app/pages/index.test.tsx @@ -0,0 +1,25 @@ +import { render } from "test/utils" + +import Home from "./index" +import { useCurrentUser } from "app/hooks/useCurrentUser" + +jest.mock("app/hooks/useCurrentUser") +const mockUseCurrentUser = useCurrentUser as jest.MockedFunction + +test.skip("renders blitz documentation link", () => { + // This is an example of how to ensure a specific item is in the document + // But it's disabled by default (by test.skip) so the test doesn't fail + // when you remove the the default content from the page + + // This is an example on how to mock api hooks when testing + mockUseCurrentUser.mockReturnValue({ + id: 1, + name: "User", + email: "user@email.com", + role: "user", + }) + + const { getByText } = render() + const linkElement = getByText(/Documentation/i) + expect(linkElement).toBeInTheDocument() +}) diff --git a/examples/fauna/app/pages/index.tsx b/examples/fauna/app/pages/index.tsx new file mode 100644 index 0000000000..20c6111623 --- /dev/null +++ b/examples/fauna/app/pages/index.tsx @@ -0,0 +1,243 @@ +import { Link, BlitzPage, useMutation } from "blitz" +import Layout from "app/layouts/Layout" +import logout from "app/auth/mutations/logout" +import { useCurrentUser } from "app/hooks/useCurrentUser" +import { Suspense } from "react" + +/* + * This file is just for a pleasant getting started page for your new app. + * You can delete everything in here and start from scratch if you like. + */ + +const UserInfo = () => { + const currentUser = useCurrentUser() + const [logoutMutation] = useMutation(logout) + + if (currentUser) { + return ( + <> + +
+ User id: {currentUser.id} +
+ User role: {currentUser.role} +
+ + ) + } else { + return ( + <> + + + Sign Up + + + + + Login + + + + ) + } +} + +const Home: BlitzPage = () => { + return ( +
+
+
+ blitz.js +
+

+ Congrats! Your app is ready, including user sign-up and log-in. +

+
+ + + +
+ +
+ + + + +
+ ) +} + +Home.getLayout = (page) => {page} + +export default Home diff --git a/examples/fauna/app/users/queries/getCurrentUser.ts b/examples/fauna/app/users/queries/getCurrentUser.ts new file mode 100644 index 0000000000..bf06411c20 --- /dev/null +++ b/examples/fauna/app/users/queries/getCurrentUser.ts @@ -0,0 +1,23 @@ +import { Ctx } from "blitz" +import db from "db" +import { gql } from "graphql-request" + +export default async function getCurrentUser(_ = null, { session }: Ctx) { + if (!session.userId) return null + + const { user } = await db.request( + gql` + query getUser($id: ID!) { + user: findUserByID(id: $id) { + id: _id + email + name + role + } + } + `, + { id: session.userId } + ) + + return user +} diff --git a/examples/fauna/babel.config.js b/examples/fauna/babel.config.js new file mode 100644 index 0000000000..dfdf62cea1 --- /dev/null +++ b/examples/fauna/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["blitz/babel"], + plugins: [], +} diff --git a/examples/fauna/blitz.config.js b/examples/fauna/blitz.config.js new file mode 100644 index 0000000000..715f18b79b --- /dev/null +++ b/examples/fauna/blitz.config.js @@ -0,0 +1,162 @@ +const { sessionMiddleware, simpleRolesIsAuthorized } = require("blitz") +const { GraphQLClient, gql } = require("graphql-request") + +const graphQLClient = new GraphQLClient(process.env.FAUNA_GRAPHQL_URL, { + headers: { + authorization: "Bearer " + process.env.FAUNA_SECRET, + }, +}) + +const normalizeSession = (faunaSession) => { + if (!faunaSession) return null + const { user, expiresAt, ...rest } = faunaSession + return { + ...rest, + userId: user.id, + expiresAt: new Date(expiresAt), + } +} + +module.exports = { + middleware: [ + sessionMiddleware({ + cookiePrefix: "blitz-fauna-example", + isAuthorized: simpleRolesIsAuthorized, + getSession: async (handle) => { + const { findSessionByHandle: session } = await graphQLClient.request( + gql` + query getSession($handle: String!) { + findSessionByHandle(handle: $handle) { + id: _id + publicData + privateData + antiCSRFToken + expiresAt + hashedSessionToken + handle + user { + id: _id + } + } + } + `, + { handle: handle } + ) + if (!session) return null + const { user, expiresAt, ...rest } = session + return { + ...rest, + userId: user.id, + expiresAt: new Date(expiresAt), + } + }, + // getSessions: (userId) => getDb().session.findMany({ where: { userId } }), + createSession: async (session) => { + const { userId, ...sessionInput } = session + const userInput = { connect: userId } + + const { createSession: sessionRes } = await graphQLClient.request( + gql` + mutation CreateSession($data: SessionInput!) { + createSession(data: $data) { + id: _id + publicData + privateData + antiCSRFToken + expiresAt + hashedSessionToken + handle + user { + id: _id + } + } + } + `, + { + data: { + ...sessionInput, + expiresAt: sessionInput.expiresAt.toISOString(), + user: userInput, + }, + } + ) + return normalizeSession(sessionRes) + }, + updateSession: async (sessionHandle, session) => { + const { findSessionByHandle: existingSession } = await graphQLClient.request( + gql` + query getSession($handle: String!) { + findSessionByHandle(handle: $handle) { + id: _id + } + } + `, + { handle: sessionHandle } + ) + + const { userId, handle, ...sessionInput } = session + + const { updateSession: sessionRes } = await graphQLClient.request( + gql` + mutation UpdateSession($data: SessionInput!) { + updateSession(data: $data) { + id: _id + publicData + privateData + antiCSRFToken + expiresAt + hashedSessionToken + handle + user { + id: _id + } + } + } + `, + { + data: { + ...sessionInput, + id: existingSession.id, + expiresAt: sessionInput.expiresAt.toISOString(), + }, + } + ) + return normalizeSession(sessionRes) + }, + deleteSession: async (handle) => { + const { findSessionByHandle: existingSession } = await graphQLClient.request( + gql` + query getSession($handle: String!) { + findSessionByHandle(handle: $handle) { + id: _id + } + } + `, + { handle: handle } + ) + + await graphQLClient.request( + gql` + mutation DeleteSession($id ID!) { + deleteSession(id: $id) { + id: _id + handle + } + } + `, + { + id: existingSession.id, + } + ) + }, + }), + ], + /* Uncomment this to customize the webpack config + webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + // Important: return the modified config + return config + }, + */ +} diff --git a/examples/fauna/db/index.ts b/examples/fauna/db/index.ts new file mode 100644 index 0000000000..c842922aca --- /dev/null +++ b/examples/fauna/db/index.ts @@ -0,0 +1,9 @@ +import { GraphQLClient } from "graphql-request" + +const graphQLClient = new GraphQLClient(process.env.FAUNA_GRAPHQL_URL, { + headers: { + authorization: "Bearer " + process.env.FAUNA_SECRET, + }, +}) + +export default graphQLClient diff --git a/examples/fauna/db/schema.graphql b/examples/fauna/db/schema.graphql new file mode 100644 index 0000000000..a99ffc8428 --- /dev/null +++ b/examples/fauna/db/schema.graphql @@ -0,0 +1,23 @@ +type User { + name: String + email: String! @unique + hashedPassword: String + role: String! + sessions: [Session!] @relation +} + +type Session { + expiresAt: Time + handle: String! @unique + user: User + hashedSessionToken: String + antiCSRFToken: String + publicData: String + privateData: String +} + +type Query { + allUsers: [User!] + findUserByEmail(email: String!): User @index(name: "unique_User_email") + findSessionByHandle(handle: String!): Session @index(name: "unique_Session_handle") +} diff --git a/examples/fauna/db/seeds.ts b/examples/fauna/db/seeds.ts new file mode 100644 index 0000000000..cc39e51b95 --- /dev/null +++ b/examples/fauna/db/seeds.ts @@ -0,0 +1,15 @@ +// import db from "./index" + +/* + * This seed function is executed when you run `blitz db seed`. + * + * Probably you want to use a library like https://chancejs.com + * to easily generate realistic data. + */ +const seed = async () => { + // for (let i = 0; i < 5; i++) { + // await db.project.create({ data: { name: "Project " + i } }) + // } +} + +export default seed diff --git a/examples/fauna/integrations/.keep b/examples/fauna/integrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/fauna/jest.config.js b/examples/fauna/jest.config.js new file mode 100644 index 0000000000..afe237a1c2 --- /dev/null +++ b/examples/fauna/jest.config.js @@ -0,0 +1,30 @@ +const { pathsToModuleNameMapper } = require("ts-jest/utils") +const { compilerOptions } = require("./tsconfig") + +module.exports = { + // Test setup file + setupFilesAfterEnv: ["/test/setup.ts"], + // Add type checking to TypeScript test files + preset: "ts-jest", + testEnvironment: "jest-environment-jsdom-fourteen", + // Automatically clear mock calls and instances between every test + clearMocks: true, + testPathIgnorePatterns: ["/node_modules/", "/.blitz/", "/.next/", "/db/migrations"], + transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$"], + transform: { + "^.+\\.(ts|tsx)$": "babel-jest", + }, + // This makes absolute imports work + moduleDirectories: ["node_modules", "."], + modulePathIgnorePatterns: [".blitz"], + moduleNameMapper: { + // This ensures any path aliases in tsconfig also work in jest + ...pathsToModuleNameMapper(compilerOptions.paths || {}), + "\\.(css|less|sass|scss)$": "identity-obj-proxy", + "\\.(gif|ttf|eot|svg|png|jpg|jpeg)$": "/test/__mocks__/fileMock.js", + }, + watchPlugins: ["jest-watch-typeahead/filename", "jest-watch-typeahead/testname"], + // Coverage output + coverageDirectory: ".coverage", + collectCoverageFrom: ["**/*.{js,jsx,ts,tsx}", "!**/*.d.ts", "!**/node_modules/**"], +} diff --git a/examples/fauna/package.json b/examples/fauna/package.json new file mode 100644 index 0000000000..e9baf6e66a --- /dev/null +++ b/examples/fauna/package.json @@ -0,0 +1,55 @@ +{ + "name": "@examples/fauna", + "version": "0.34.0-canary.0", + "scripts": { + "dev": "blitz dev", + "build": "blitz build", + "start": "blitz start", + "studio": "blitz prisma studio", + "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", + "test": "echo \"No tests yet\"" + }, + "browserslist": [ + "defaults" + ], + "prettier": { + "semi": false, + "printWidth": 100 + }, + "husky": { + "hooks": { + "pre-commit": "tsc && lint-staged && pretty-quick --staged", + "pre-push": "npm run lint && npm run test" + } + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "eslint --fix" + ] + }, + "dependencies": { + "blitz": "0.45.4", + "final-form": "4.20.1", + "graphql": "15.5.0", + "graphql-request": "3.4.0", + "react": "0.0.0-experimental-6a589ad71", + "react-dom": "0.0.0-experimental-6a589ad71", + "react-final-form": "6.5.2" + }, + "devDependencies": { + "@testing-library/react": "11.2.5", + "@testing-library/react-hooks": "^4.0.1", + "@types/react": "17.0.2", + "@types/secure-password": "3.1.0", + "eslint": "7.21.0", + "eslint-config-react-app": "~6.0.0", + "eslint-plugin-flowtype": "~5.2.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.23.1", + "eslint-plugin-react-hooks": "^4.2.0", + "husky": "5.1.2", + "start-server-and-test": "1.11.7" + }, + "private": true +} diff --git a/examples/fauna/public/favicon.ico b/examples/fauna/public/favicon.ico new file mode 100755 index 0000000000..a94b0f7a7f Binary files /dev/null and b/examples/fauna/public/favicon.ico differ diff --git a/examples/fauna/public/logo.png b/examples/fauna/public/logo.png new file mode 100644 index 0000000000..6a982616ee Binary files /dev/null and b/examples/fauna/public/logo.png differ diff --git a/examples/fauna/test/.keep b/examples/fauna/test/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/fauna/test/__mocks__/fileMock.js b/examples/fauna/test/__mocks__/fileMock.js new file mode 100644 index 0000000000..ebf20155e6 --- /dev/null +++ b/examples/fauna/test/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = "test-file-stub" diff --git a/examples/fauna/test/setup.ts b/examples/fauna/test/setup.ts new file mode 100644 index 0000000000..ecbde24ef3 --- /dev/null +++ b/examples/fauna/test/setup.ts @@ -0,0 +1,8 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import "@testing-library/jest-dom/extend-expect" +import { loadEnvConfig } from "@blitzjs/env" + +loadEnvConfig() diff --git a/examples/fauna/test/utils.tsx b/examples/fauna/test/utils.tsx new file mode 100644 index 0000000000..988f2ed189 --- /dev/null +++ b/examples/fauna/test/utils.tsx @@ -0,0 +1,108 @@ +import { RouterContext, BlitzRouter, BlitzProvider } from "blitz" +import { render as defaultRender } from "@testing-library/react" +import { renderHook as defaultRenderHook } from "@testing-library/react-hooks" + +export * from "@testing-library/react" + +// -------------------------------------------------------------------------------- +// This file customizes the render() and renderHook() test functions provided +// by React testing library. It adds a router context wrapper with a mocked router. +// +// You should always import `render` and `renderHook` from this file +// +// This is the place to add any other context providers you need while testing. +// -------------------------------------------------------------------------------- + +type DefaultParams = Parameters +type RenderUI = DefaultParams[0] +type RenderOptions = DefaultParams[1] & { + router?: Partial + dehydratedState?: unknown +} + +type DefaultHookParams = Parameters +type RenderHook = DefaultHookParams[0] +type RenderHookOptions = DefaultHookParams[1] & { + router?: Partial + dehydratedState?: unknown +} + +// -------------------------------------------------- +// render() +// -------------------------------------------------- +// Override the default test render with our own +// +// You can override the router mock like this: +// +// const { baseElement } = render(, { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function render( + ui: RenderUI, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }) => ( + + + {children} + + + ) + } + return defaultRender(ui, { wrapper, ...options }) +} + +// -------------------------------------------------- +// renderHook() +// -------------------------------------------------- +// Override the default test renderHook with our own +// +// You can override the router mock like this: +// +// const result = renderHook(() => myHook(), { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function renderHook( + hook: RenderHook, + { wrapper, router, dehydratedState, ...options }: RenderHookOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }) => ( + + + {children} + + + ) + } + return defaultRenderHook(hook, { wrapper, ...options }) +} + +export const mockRouter: BlitzRouter = { + basePath: "", + pathname: "/", + route: "/", + asPath: "/", + params: {}, + query: {}, + isReady: true, + isLocaleDomain: false, + isPreview: false, + push: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + isFallback: false, +} diff --git a/examples/fauna/tsconfig.json b/examples/fauna/tsconfig.json new file mode 100644 index 0000000000..f7cd39f52e --- /dev/null +++ b/examples/fauna/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": "./", + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "exclude": ["node_modules"], + "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"] +} diff --git a/examples/fauna/types b/examples/fauna/types new file mode 120000 index 0000000000..8788aa2845 --- /dev/null +++ b/examples/fauna/types @@ -0,0 +1 @@ +../../types \ No newline at end of file diff --git a/examples/fauna/types.ts b/examples/fauna/types.ts new file mode 100644 index 0000000000..4b3cee2549 --- /dev/null +++ b/examples/fauna/types.ts @@ -0,0 +1,13 @@ +import { DefaultCtx, SessionContext, SimpleRolesIsAuthorized } from "blitz" + +declare module "blitz" { + export interface Ctx extends DefaultCtx { + session: SessionContext + } + export interface Session { + isAuthorized: SimpleRolesIsAuthorized + PublicData: { + userId: string + } + } +} diff --git a/examples/fauna/utils/.keep b/examples/fauna/utils/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/i18n-next-rosetta/.editorconfig b/examples/i18n-next-rosetta/.editorconfig new file mode 100644 index 0000000000..09d7a33a4f --- /dev/null +++ b/examples/i18n-next-rosetta/.editorconfig @@ -0,0 +1,11 @@ +# https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/examples/i18n-next-rosetta/.env b/examples/i18n-next-rosetta/.env new file mode 100644 index 0000000000..5cd1aca530 --- /dev/null +++ b/examples/i18n-next-rosetta/.env @@ -0,0 +1,3 @@ +# This env file should be checked into source control +# This is the place for default values for all environments +# Values in `.env.local` and `.env.production` will override these values diff --git a/examples/i18n-next-rosetta/.eslintrc.js b/examples/i18n-next-rosetta/.eslintrc.js new file mode 100644 index 0000000000..f845b10d52 --- /dev/null +++ b/examples/i18n-next-rosetta/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["blitz"], +} diff --git a/examples/i18n-next-rosetta/.gitignore b/examples/i18n-next-rosetta/.gitignore new file mode 100644 index 0000000000..ee6565a01d --- /dev/null +++ b/examples/i18n-next-rosetta/.gitignore @@ -0,0 +1,56 @@ +# dependencies +node_modules +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +.pnp.* +.npm +web_modules/ + +# blitz +/.blitz/ +/.next/ +*.sqlite +*.sqlite-journal +.now +.blitz** +blitz-log.log + +# misc +.DS_Store + +# local env files +.env.local +.env.*.local +.envrc + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +.coverage +*.lcov +.nyc_output +lib-cov + +# Caches +*.tsbuildinfo +.eslintcache +.node_repl_history +.yarn-integrity + +# Serverless directories +.serverless/ + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test diff --git a/examples/i18n-next-rosetta/.husky/.gitignore b/examples/i18n-next-rosetta/.husky/.gitignore new file mode 100644 index 0000000000..31354ec138 --- /dev/null +++ b/examples/i18n-next-rosetta/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/examples/i18n-next-rosetta/.husky/pre-commit b/examples/i18n-next-rosetta/.husky/pre-commit new file mode 100755 index 0000000000..dd4268e690 --- /dev/null +++ b/examples/i18n-next-rosetta/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged +npx pretty-quick --staged diff --git a/examples/i18n-next-rosetta/.husky/pre-push b/examples/i18n-next-rosetta/.husky/pre-push new file mode 100755 index 0000000000..4918980b1d --- /dev/null +++ b/examples/i18n-next-rosetta/.husky/pre-push @@ -0,0 +1,6 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx tsc +npm run lint +npm run test diff --git a/examples/i18n-next-rosetta/.npmrc b/examples/i18n-next-rosetta/.npmrc new file mode 100644 index 0000000000..f7777f0b8c --- /dev/null +++ b/examples/i18n-next-rosetta/.npmrc @@ -0,0 +1,7 @@ +save-exact=true +legacy-peer-deps=true + +public-hoist-pattern[]=next +public-hoist-pattern[]=secure-password +public-hoist-pattern[]=*jest* +public-hoist-pattern[]=@testing-library/* diff --git a/examples/i18n-next-rosetta/.prettierignore b/examples/i18n-next-rosetta/.prettierignore new file mode 100644 index 0000000000..b1d375c7de --- /dev/null +++ b/examples/i18n-next-rosetta/.prettierignore @@ -0,0 +1,9 @@ +.gitkeep +.env* +*.ico +*.lock +db/migrations +.next +.blitz +.yarn +.pnp* diff --git a/examples/i18n-next-rosetta/README.md b/examples/i18n-next-rosetta/README.md new file mode 100644 index 0000000000..f1a2b970a3 --- /dev/null +++ b/examples/i18n-next-rosetta/README.md @@ -0,0 +1,132 @@ +# Blitz localization with next-rosetta Example + +This example shows how to use [next-rosetta](https://www.npmjs.com/package/next-rosetta) to translate your application. It has English and French locales and need to be adapted to your own use case. + +## Getting started + +### Install dependencies + +``` +# with npm +npm install next-rosetta + +# with yarn +yarn add next-rosetta +``` + +### Configure the international routing in the `blitz.config.ts` file + +```ts +// blitz.config.ts +import { BlitzConfig } from "blitz" + +const config: BlitzConfig = { + i18n: { + // These are all the locales you want to support + locales: ["en", "fr"], + // This is the default locale you want to be used when visiting + // a path without locale prefix e.g. `/users` + defaultLocale: "en", + }, +} +module.exports = config +``` + +### Create locales + +Create a directory named `i18n` in your project. If you are using TypeScript you can define the type schema and create every locale based on that interface. An example: + +```ts +export interface MyLocale { + locale: string + home: { + logout: string + userId: string + userRole: string + signUp: string + login: string + welcome: { + part1: string + part2: string + } +... +``` + +Then you can add files with translations. Here's an English example: + +```ts +import type { MyLocale } from "." + +export const table: MyLocale = { + locale: "English", + home: { + logout: "Logout", + userId: "User Id", + userRole: "User Role", + signUp: "Sign Up", + login: "Login", + welcome: { + part1: "Congrats!", + part2: " Your app is ready, including user sign-up and log-in.", + }, +... +``` + +### Add the i18n provider in `app/pages/_app.tsx` + +```diff +import { I18nProvider } from "next-rosetta" + +export default function App({ Component, pageProps }: AppProps) { + const getLayout = Component.getLayout || ((page) => page) + + return ( ++ + + {getLayout()} + ++ + ) +} +``` + +### Use `next-rosetta` in your components + +Here is an example if you are using `getStaticProps`: + +```ts +// app/pages/index.tsx +import type { GetStaticProps } from "next" +import { useI18n, I18nProps } from "next-rosetta" + +import type { MyLocale } from "../i18n" + +function HomePage() { + const { t } = useI18n() + return ( +
+

{t("title")}

+

{t("welcome", { name: "John" })}

+ +
+ ) +} + +// You can use I18nProps for type-safety (optional) +export const getStaticProps: GetStaticProps> = async (context) => { + const locale = context.locale || context.defaultLocale + const { table = {} } = await import(`../i18n/${locale}`) // Import locale + return { props: { table } } // Passed to `app/pages/_app.tsx` +} +``` + +Take a look at the following files to see how `next-rosetta` can be used in a Blitz application: + +- `app/pages/index.tsx` +- `app/auth/components/LoginForm.tsx` +- `app/auth/pages/login.tsx` + +Refer to the [docs](https://github.com/useflyyer/next-rosetta) for more examples. diff --git a/examples/i18n-next-rosetta/app/api/.keep b/examples/i18n-next-rosetta/app/api/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/i18n-next-rosetta/app/auth/components/LoginForm.tsx b/examples/i18n-next-rosetta/app/auth/components/LoginForm.tsx new file mode 100644 index 0000000000..e52fd7dbc5 --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/components/LoginForm.tsx @@ -0,0 +1,61 @@ +import { AuthenticationError, Link, useMutation, Routes } from "blitz" +import { LabeledTextField } from "app/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/core/components/Form" +import login from "app/auth/mutations/login" +import { Login } from "app/auth/validations" +import { useI18n } from "next-rosetta" +import type { MyLocale } from "app/core/i18n" + +type LoginFormProps = { + onSuccess?: () => void +} + +export const LoginForm = (props: LoginFormProps) => { + const [loginMutation] = useMutation(login) + const { t } = useI18n() + + return ( +
+

{t("loginPage.login")}

+ +
{ + try { + await loginMutation(values) + props.onSuccess?.() + } catch (error: any) { + if (error instanceof AuthenticationError) { + return { [FORM_ERROR]: t("loginPage.credentialError") } + } else { + return { + [FORM_ERROR]: t("loginPage.unexpectedError", { message: error.toString() }), + } + } + } + }} + > + + + + + +
+ {t("loginPage.Or")} {t("loginPage.signUp")} +
+
+ ) +} + +export default LoginForm diff --git a/examples/i18n-next-rosetta/app/auth/components/SignupForm.tsx b/examples/i18n-next-rosetta/app/auth/components/SignupForm.tsx new file mode 100644 index 0000000000..a76d800dc9 --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/components/SignupForm.tsx @@ -0,0 +1,43 @@ +import { useMutation } from "blitz" +import { LabeledTextField } from "app/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/core/components/Form" +import signup from "app/auth/mutations/signup" +import { Signup } from "app/auth/validations" + +type SignupFormProps = { + onSuccess?: () => void +} + +export const SignupForm = (props: SignupFormProps) => { + const [signupMutation] = useMutation(signup) + + return ( +
+

Create an Account

+ +
{ + try { + await signupMutation(values) + props.onSuccess?.() + } catch (error: any) { + if (error.code === "P2002" && error.meta?.target?.includes("email")) { + // This error comes from Prisma + return { email: "This email is already being used" } + } else { + return { [FORM_ERROR]: error.toString() } + } + } + }} + > + + + +
+ ) +} + +export default SignupForm diff --git a/examples/i18n-next-rosetta/app/auth/mutations/changePassword.ts b/examples/i18n-next-rosetta/app/auth/mutations/changePassword.ts new file mode 100644 index 0000000000..94f7d391d3 --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/mutations/changePassword.ts @@ -0,0 +1,23 @@ +import { NotFoundError, SecurePassword, resolver } from "blitz" +import db from "db" +import { authenticateUser } from "./login" +import { ChangePassword } from "../validations" + +export default resolver.pipe( + resolver.zod(ChangePassword), + resolver.authorize(), + async ({ currentPassword, newPassword }, ctx) => { + const user = await db.user.findFirst({ where: { id: ctx.session.userId! } }) + if (!user) throw new NotFoundError() + + await authenticateUser(user.email, currentPassword) + + const hashedPassword = await SecurePassword.hash(newPassword.trim()) + await db.user.update({ + where: { id: user.id }, + data: { hashedPassword }, + }) + + return true + } +) diff --git a/examples/i18n-next-rosetta/app/auth/mutations/forgotPassword.test.ts b/examples/i18n-next-rosetta/app/auth/mutations/forgotPassword.test.ts new file mode 100644 index 0000000000..cdd8112c3c --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/mutations/forgotPassword.test.ts @@ -0,0 +1,56 @@ +import { hash256, Ctx } from "blitz" +import forgotPassword from "./forgotPassword" +import db from "db" +import previewEmail from "preview-email" + +beforeEach(async () => { + await db.$reset() +}) + +const generatedToken = "plain-token" +jest.mock("next/stdlib-server", () => ({ + ...jest.requireActual("next/stdlib-server")!, + generateToken: () => generatedToken, +})) +jest.mock("preview-email", () => jest.fn()) + +describe("forgotPassword mutation", () => { + it("does not throw error if user doesn't exist", async () => { + await expect(forgotPassword({ email: "no-user@email.com" }, {} as Ctx)).resolves.not.toThrow() + }) + + it("works correctly", async () => { + // Create test user + const user = await db.user.create({ + data: { + email: "user@example.com", + tokens: { + // Create old token to ensure it's deleted + create: { + type: "RESET_PASSWORD", + hashedToken: "token", + expiresAt: new Date(), + sentTo: "user@example.com", + }, + }, + }, + include: { tokens: true }, + }) + + // Invoke the mutation + await forgotPassword({ email: user.email }, {} as Ctx) + + const tokens = await db.token.findMany({ where: { userId: user.id } }) + const token = tokens[0] + + // delete's existing tokens + expect(tokens.length).toBe(1) + + expect(token.id).not.toBe(user.tokens[0].id) + expect(token.type).toBe("RESET_PASSWORD") + expect(token.sentTo).toBe(user.email) + expect(token.hashedToken).toBe(hash256(generatedToken)) + expect(token.expiresAt > new Date()).toBe(true) + expect(previewEmail).toBeCalled() + }) +}) diff --git a/examples/i18n-next-rosetta/app/auth/mutations/forgotPassword.ts b/examples/i18n-next-rosetta/app/auth/mutations/forgotPassword.ts new file mode 100644 index 0000000000..1cbf2a33cb --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/mutations/forgotPassword.ts @@ -0,0 +1,41 @@ +import { resolver, generateToken, hash256 } from "blitz" +import db from "db" +import { forgotPasswordMailer } from "mailers/forgotPasswordMailer" +import { ForgotPassword } from "../validations" + +const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4 + +export default resolver.pipe(resolver.zod(ForgotPassword), async ({ email }) => { + // 1. Get the user + const user = await db.user.findFirst({ where: { email: email.toLowerCase() } }) + + // 2. Generate the token and expiration date. + const token = generateToken() + const hashedToken = hash256(token) + const expiresAt = new Date() + expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS) + + // 3. If user with this email was found + if (user) { + // 4. Delete any existing password reset tokens + await db.token.deleteMany({ where: { type: "RESET_PASSWORD", userId: user.id } }) + // 5. Save this new token in the database. + await db.token.create({ + data: { + user: { connect: { id: user.id } }, + type: "RESET_PASSWORD", + expiresAt, + hashedToken, + sentTo: user.email, + }, + }) + // 6. Send the email + await forgotPasswordMailer({ to: user.email, token }).send() + } else { + // 7. If no user found wait the same time so attackers can't tell the difference + await new Promise((resolve) => setTimeout(resolve, 750)) + } + + // 8. Return the same result whether a password reset email was sent or not + return +}) diff --git a/examples/i18n-next-rosetta/app/auth/mutations/login.ts b/examples/i18n-next-rosetta/app/auth/mutations/login.ts new file mode 100644 index 0000000000..1b715ec4bb --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/mutations/login.ts @@ -0,0 +1,31 @@ +import { resolver, SecurePassword, AuthenticationError } from "blitz" +import db from "db" +import { Login } from "../validations" +import { Role } from "types" + +export const authenticateUser = async (rawEmail: string, rawPassword: string) => { + const email = rawEmail.toLowerCase().trim() + const password = rawPassword.trim() + const user = await db.user.findFirst({ where: { email } }) + if (!user) throw new AuthenticationError() + + const result = await SecurePassword.verify(user.hashedPassword, password) + + if (result === SecurePassword.VALID_NEEDS_REHASH) { + // Upgrade hashed password with a more secure hash + const improvedHash = await SecurePassword.hash(password) + await db.user.update({ where: { id: user.id }, data: { hashedPassword: improvedHash } }) + } + + const { hashedPassword, ...rest } = user + return rest +} + +export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ctx) => { + // This throws an error if credentials are invalid + const user = await authenticateUser(email, password) + + await ctx.session.$create({ userId: user.id, role: user.role as Role }) + + return user +}) diff --git a/examples/i18n-next-rosetta/app/auth/mutations/logout.ts b/examples/i18n-next-rosetta/app/auth/mutations/logout.ts new file mode 100644 index 0000000000..c2f2fefd8d --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/mutations/logout.ts @@ -0,0 +1,5 @@ +import { Ctx } from "blitz" + +export default async function logout(_: any, ctx: Ctx) { + return await ctx.session.$revoke() +} diff --git a/examples/i18n-next-rosetta/app/auth/mutations/resetPassword.test.ts b/examples/i18n-next-rosetta/app/auth/mutations/resetPassword.test.ts new file mode 100644 index 0000000000..d613716f53 --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/mutations/resetPassword.test.ts @@ -0,0 +1,82 @@ +import resetPassword from "./resetPassword" +import db from "db" +import { hash256, SecurePassword } from "blitz" + +beforeEach(async () => { + await db.$reset() +}) + +const mockCtx: any = { + session: { + $create: jest.fn, + }, +} + +describe("resetPassword mutation", () => { + it("works correctly", async () => { + expect(true).toBe(true) + + // Create test user + const goodToken = "randomPasswordResetToken" + const expiredToken = "expiredRandomPasswordResetToken" + const future = new Date() + future.setHours(future.getHours() + 4) + const past = new Date() + past.setHours(past.getHours() - 4) + + const user = await db.user.create({ + data: { + email: "user@example.com", + tokens: { + // Create old token to ensure it's deleted + create: [ + { + type: "RESET_PASSWORD", + hashedToken: hash256(expiredToken), + expiresAt: past, + sentTo: "user@example.com", + }, + { + type: "RESET_PASSWORD", + hashedToken: hash256(goodToken), + expiresAt: future, + sentTo: "user@example.com", + }, + ], + }, + }, + include: { tokens: true }, + }) + + const newPassword = "newPassword" + + // Non-existent token + await expect( + resetPassword({ token: "no-token", password: "", passwordConfirmation: "" }, mockCtx) + ).rejects.toThrowError() + + // Expired token + await expect( + resetPassword( + { token: expiredToken, password: newPassword, passwordConfirmation: newPassword }, + mockCtx + ) + ).rejects.toThrowError() + + // Good token + await resetPassword( + { token: goodToken, password: newPassword, passwordConfirmation: newPassword }, + mockCtx + ) + + // Delete's the token + const numberOfTokens = await db.token.count({ where: { userId: user.id } }) + expect(numberOfTokens).toBe(0) + + // Updates user's password + const updatedUser = await db.user.findFirst({ where: { id: user.id } }) + expect(await SecurePassword.verify(updatedUser!.hashedPassword, newPassword)).toBe( + SecurePassword.VALID + ) + }) +}) diff --git a/examples/i18n-next-rosetta/app/auth/mutations/resetPassword.ts b/examples/i18n-next-rosetta/app/auth/mutations/resetPassword.ts new file mode 100644 index 0000000000..75d4d67626 --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/mutations/resetPassword.ts @@ -0,0 +1,47 @@ +import { resolver, SecurePassword, hash256 } from "blitz" +import db from "db" +import { ResetPassword } from "../validations" +import login from "./login" + +export class ResetPasswordError extends Error { + name = "ResetPasswordError" + message = "Reset password link is invalid or it has expired." +} + +export default resolver.pipe(resolver.zod(ResetPassword), async ({ password, token }, ctx) => { + // 1. Try to find this token in the database + const hashedToken = hash256(token) + const possibleToken = await db.token.findFirst({ + where: { hashedToken, type: "RESET_PASSWORD" }, + include: { user: true }, + }) + + // 2. If token not found, error + if (!possibleToken) { + throw new ResetPasswordError() + } + const savedToken = possibleToken + + // 3. Delete token so it can't be used again + await db.token.delete({ where: { id: savedToken.id } }) + + // 4. If token has expired, error + if (savedToken.expiresAt < new Date()) { + throw new ResetPasswordError() + } + + // 5. Since token is valid, now we can update the user's password + const hashedPassword = await SecurePassword.hash(password.trim()) + const user = await db.user.update({ + where: { id: savedToken.userId }, + data: { hashedPassword }, + }) + + // 6. Revoke all existing login sessions for this user + await db.session.deleteMany({ where: { userId: user.id } }) + + // 7. Now log the user in with the new credentials + await login({ email: user.email, password }, ctx) + + return true +}) diff --git a/examples/i18n-next-rosetta/app/auth/mutations/signup.ts b/examples/i18n-next-rosetta/app/auth/mutations/signup.ts new file mode 100644 index 0000000000..4791a99f43 --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/mutations/signup.ts @@ -0,0 +1,15 @@ +import { resolver, SecurePassword } from "blitz" +import db from "db" +import { Signup } from "app/auth/validations" +import { Role } from "types" + +export default resolver.pipe(resolver.zod(Signup), async ({ email, password }, ctx) => { + const hashedPassword = await SecurePassword.hash(password.trim()) + const user = await db.user.create({ + data: { email: email.toLowerCase().trim(), hashedPassword, role: "USER" }, + select: { id: true, name: true, email: true, role: true }, + }) + + await ctx.session.$create({ userId: user.id, role: user.role as Role }) + return user +}) diff --git a/examples/i18n-next-rosetta/app/auth/pages/forgot-password.tsx b/examples/i18n-next-rosetta/app/auth/pages/forgot-password.tsx new file mode 100644 index 0000000000..c2509982df --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/pages/forgot-password.tsx @@ -0,0 +1,48 @@ +import { BlitzPage, useMutation } from "blitz" +import Layout from "app/core/layouts/Layout" +import { LabeledTextField } from "app/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/core/components/Form" +import { ForgotPassword } from "app/auth/validations" +import forgotPassword from "app/auth/mutations/forgotPassword" + +const ForgotPasswordPage: BlitzPage = () => { + const [forgotPasswordMutation, { isSuccess }] = useMutation(forgotPassword) + + return ( +
+

Forgot your password?

+ + {isSuccess ? ( +
+

Request Submitted

+

+ If your email is in our system, you will receive instructions to reset your password + shortly. +

+
+ ) : ( +
{ + try { + await forgotPasswordMutation(values) + } catch (error: any) { + return { + [FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.", + } + } + }} + > + + + )} +
+ ) +} + +ForgotPasswordPage.redirectAuthenticatedTo = "/" +ForgotPasswordPage.getLayout = (page) => {page} + +export default ForgotPasswordPage diff --git a/examples/i18n-next-rosetta/app/auth/pages/login.tsx b/examples/i18n-next-rosetta/app/auth/pages/login.tsx new file mode 100644 index 0000000000..db293b6c1c --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/pages/login.tsx @@ -0,0 +1,32 @@ +import { useRouter, BlitzPage } from "blitz" +import Layout from "app/core/layouts/Layout" +import { LoginForm } from "app/auth/components/LoginForm" +import type { GetStaticProps } from "blitz" +import { I18nProps } from "next-rosetta" +import type { MyLocale } from "app/core/i18n" + +const LoginPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ { + const next = router.query.next ? decodeURIComponent(router.query.next as string) : "/" + router.push(next) + }} + /> +
+ ) +} + +LoginPage.redirectAuthenticatedTo = "/" +LoginPage.getLayout = (page) => {page} + +export const getStaticProps: GetStaticProps> = async (context) => { + const locale = context.locale || context.defaultLocale + const { table = {} } = await import(`app/core/i18n/${locale}`) // Import locale + return { props: { table } } // Passed to `/pages/_app.tsx` +} + +export default LoginPage diff --git a/examples/i18n-next-rosetta/app/auth/pages/reset-password.tsx b/examples/i18n-next-rosetta/app/auth/pages/reset-password.tsx new file mode 100644 index 0000000000..64c1e451fd --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/pages/reset-password.tsx @@ -0,0 +1,60 @@ +import { BlitzPage, useRouterQuery, Link, useMutation } from "blitz" +import Layout from "app/core/layouts/Layout" +import { LabeledTextField } from "app/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "app/core/components/Form" +import { ResetPassword } from "app/auth/validations" +import resetPassword from "app/auth/mutations/resetPassword" +import { Routes } from ".blitz" + +const ResetPasswordPage: BlitzPage = () => { + const query = useRouterQuery() + const [resetPasswordMutation, { isSuccess }] = useMutation(resetPassword) + + return ( +
+

Set a New Password

+ + {isSuccess ? ( +
+

Password Reset Successfully

+

+ Go to the homepage +

+
+ ) : ( +
{ + try { + await resetPasswordMutation(values) + } catch (error: any) { + if (error.name === "ResetPasswordError") { + return { + [FORM_ERROR]: error.message, + } + } else { + return { + [FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.", + } + } + } + }} + > + + + + )} +
+ ) +} + +ResetPasswordPage.redirectAuthenticatedTo = "/" +ResetPasswordPage.getLayout = (page) => {page} + +export default ResetPasswordPage diff --git a/examples/i18n-next-rosetta/app/auth/pages/signup.tsx b/examples/i18n-next-rosetta/app/auth/pages/signup.tsx new file mode 100644 index 0000000000..d04bcab44e --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/pages/signup.tsx @@ -0,0 +1,19 @@ +import { useRouter, BlitzPage } from "blitz" +import Layout from "app/core/layouts/Layout" +import { SignupForm } from "app/auth/components/SignupForm" +import { Routes } from ".blitz" + +const SignupPage: BlitzPage = () => { + const router = useRouter() + + return ( +
+ router.push(Routes.Home())} /> +
+ ) +} + +SignupPage.redirectAuthenticatedTo = "/" +SignupPage.getLayout = (page) => {page} + +export default SignupPage diff --git a/examples/i18n-next-rosetta/app/auth/validations.ts b/examples/i18n-next-rosetta/app/auth/validations.ts new file mode 100644 index 0000000000..fe47769166 --- /dev/null +++ b/examples/i18n-next-rosetta/app/auth/validations.ts @@ -0,0 +1,33 @@ +import { z } from "zod" + +const password = z.string().min(10).max(100) + +export const Signup = z.object({ + email: z.string().email(), + password, +}) + +export const Login = z.object({ + email: z.string().email(), + password: z.string(), +}) + +export const ForgotPassword = z.object({ + email: z.string().email(), +}) + +export const ResetPassword = z + .object({ + password: password, + passwordConfirmation: password, + token: z.string(), + }) + .refine((data) => data.password === data.passwordConfirmation, { + message: "Passwords don't match", + path: ["passwordConfirmation"], // set the path of the error + }) + +export const ChangePassword = z.object({ + currentPassword: z.string(), + newPassword: password, +}) diff --git a/examples/i18n-next-rosetta/app/core/components/Form.tsx b/examples/i18n-next-rosetta/app/core/components/Form.tsx new file mode 100644 index 0000000000..eb45dc3b9f --- /dev/null +++ b/examples/i18n-next-rosetta/app/core/components/Form.tsx @@ -0,0 +1,59 @@ +import { ReactNode, PropsWithoutRef } from "react" +import { Form as FinalForm, FormProps as FinalFormProps } from "react-final-form" +import { z } from "zod" +import { validateZodSchema } from "blitz" +export { FORM_ERROR } from "final-form" + +export interface FormProps> + extends Omit, "onSubmit"> { + /** All your form fields */ + children?: ReactNode + /** Text to display in the submit button */ + submitText?: string + schema?: S + onSubmit: FinalFormProps>["onSubmit"] + initialValues?: FinalFormProps>["initialValues"] +} + +export function Form>({ + children, + submitText, + schema, + initialValues, + onSubmit, + ...props +}: FormProps) { + return ( + ( +
+ {/* Form fields supplied as children are rendered here */} + {children} + + {submitError && ( +
+ {submitError} +
+ )} + + {submitText && ( + + )} + + +
+ )} + /> + ) +} + +export default Form diff --git a/examples/i18n-next-rosetta/app/core/components/LabeledTextField.tsx b/examples/i18n-next-rosetta/app/core/components/LabeledTextField.tsx new file mode 100644 index 0000000000..7dc98b89c9 --- /dev/null +++ b/examples/i18n-next-rosetta/app/core/components/LabeledTextField.tsx @@ -0,0 +1,66 @@ +import { forwardRef, ComponentPropsWithoutRef, PropsWithoutRef } from "react" +import { useField, UseFieldConfig } from "react-final-form" + +export interface LabeledTextFieldProps extends PropsWithoutRef { + /** Field name. */ + name: string + /** Field label. */ + label: string + /** Field type. Doesn't include radio buttons and checkboxes */ + type?: "text" | "password" | "email" | "number" + outerProps?: PropsWithoutRef + labelProps?: ComponentPropsWithoutRef<"label"> + fieldProps?: UseFieldConfig +} + +export const LabeledTextField = forwardRef( + ({ name, label, outerProps, fieldProps, labelProps, ...props }, ref) => { + const { + input, + meta: { touched, error, submitError, submitting }, + } = useField(name, { + parse: + props.type === "number" + ? (Number as any) + : // Converting `""` to `null` ensures empty values will be set to null in the DB + (v) => (v === "" ? null : v), + ...fieldProps, + }) + + const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError + + return ( +
+ + + {touched && normalizedError && ( +
+ {normalizedError} +
+ )} + + +
+ ) + } +) + +export default LabeledTextField diff --git a/examples/i18n-next-rosetta/app/core/hooks/useCurrentUser.ts b/examples/i18n-next-rosetta/app/core/hooks/useCurrentUser.ts new file mode 100644 index 0000000000..2938468c7b --- /dev/null +++ b/examples/i18n-next-rosetta/app/core/hooks/useCurrentUser.ts @@ -0,0 +1,7 @@ +import { useQuery } from "blitz" +import getCurrentUser from "app/users/queries/getCurrentUser" + +export const useCurrentUser = () => { + const [user] = useQuery(getCurrentUser, null) + return user +} diff --git a/examples/i18n-next-rosetta/app/core/i18n/en.tsx b/examples/i18n-next-rosetta/app/core/i18n/en.tsx new file mode 100644 index 0000000000..a35cf01a3c --- /dev/null +++ b/examples/i18n-next-rosetta/app/core/i18n/en.tsx @@ -0,0 +1,42 @@ +import type { MyLocale } from "." + +export const table: MyLocale = { + locale: "English", + home: { + logout: "Logout", + userId: "User Id", + userRole: "User Role", + signUp: "Sign Up", + login: "Login", + welcome: { + part1: "Congrats!", + part2: " Your app is ready, including user sign-up and log-in.", + }, + paragraph: { + line1: "To add a new model to your app,", + line2: "run the following in your terminal:", + }, + code: "blitz generate all project name:string", + parenthesis: "(And select Yes to run prisma migrate)", + restart: { + part1: "Then ", + part2: "restart the server", + }, + goto: "and go to", + project: "/projects", + documentationLink: "Documentation", + githubLink: "Github Repo", + discordLink: "Discord Community", + footer: "Powered by Blitz.js", + }, + loginPage: { + login: "Login", + credentialError: "Sorry, those credentials are invalid", + unexpectedError: "Sorry, we had an unexpected error. Please try again. - {{message}}", + forgotPassword: "Forgot your password?", + signUp: "Sign Up", + email: "Email", + password: "Password", + or: "Or", + }, +} diff --git a/examples/i18n-next-rosetta/app/core/i18n/fr.tsx b/examples/i18n-next-rosetta/app/core/i18n/fr.tsx new file mode 100644 index 0000000000..2a07ad50d2 --- /dev/null +++ b/examples/i18n-next-rosetta/app/core/i18n/fr.tsx @@ -0,0 +1,43 @@ +import type { MyLocale } from "." + +export const table: MyLocale = { + locale: "Français", + home: { + logout: "Deconnexion", + userId: "Id", + userRole: "Role", + signUp: "Créez un compte", + login: "Connexion", + welcome: { + part1: "Bravo!", + part2: + " Votre application est prête, incluant la création de compte utilisateurs et la connexion.", + }, + paragraph: { + line1: "Pour rajouter un modèle à votre application,", + line2: "exécutez ce qui suit dans votre terminal:", + }, + code: "blitz generate all project name:string", + parenthesis: '(Et sélectionnez "Yes to run prisma migrate")', + restart: { + part1: "Ensuite ", + part2: "redémarrez le serveur", + }, + goto: "et allez sur", + project: "/projects", + documentationLink: "Documentation", + githubLink: "Github", + discordLink: "Communauté Discord", + footer: "Crée avec Blitz.js", + }, + loginPage: { + login: "Connexion", + credentialError: "Désolé, l'identifiant et le mot de passe sont invalide.", + unexpectedError: "Désolé, erreur inatendue. Essayez à nouveau. - {{message}}", + forgotPassword: "Vous avez oublié votre mot de passe?", + signUp: "Enregistrez-vous", + email: "Courriel", + password: "Mot de passe", + or: "Ou", + }, +} diff --git a/examples/i18n-next-rosetta/app/core/i18n/index.tsx b/examples/i18n-next-rosetta/app/core/i18n/index.tsx new file mode 100644 index 0000000000..d2d0927b87 --- /dev/null +++ b/examples/i18n-next-rosetta/app/core/i18n/index.tsx @@ -0,0 +1,42 @@ +// Check: https://github.com/useflyyer/next-rosetta + +export interface MyLocale { + locale: string + home: { + logout: string + userId: string + userRole: string + signUp: string + login: string + welcome: { + part1: string + part2: string + } + paragraph: { + line1: string + line2: string + } + code: string + parenthesis: string + restart: { + part1: string + part2: string + } + goto: string + project: string + documentationLink: string + githubLink: string + discordLink: string + footer: string + } + loginPage: { + login: string + credentialError: string + unexpectedError: string + forgotPassword: string + signUp: string + email: string + password: string + or: string + } +} diff --git a/examples/i18n-next-rosetta/app/core/layouts/Layout.tsx b/examples/i18n-next-rosetta/app/core/layouts/Layout.tsx new file mode 100644 index 0000000000..b21cab1f0b --- /dev/null +++ b/examples/i18n-next-rosetta/app/core/layouts/Layout.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from "react" +import { Head } from "blitz" + +type LayoutProps = { + title?: string + children: ReactNode +} + +const Layout = ({ title, children }: LayoutProps) => { + return ( + <> + + {title || "blitzI18nRosetta"} + + + + {children} + + ) +} + +export default Layout diff --git a/examples/i18n-next-rosetta/app/pages/404.tsx b/examples/i18n-next-rosetta/app/pages/404.tsx new file mode 100644 index 0000000000..ab81189e6c --- /dev/null +++ b/examples/i18n-next-rosetta/app/pages/404.tsx @@ -0,0 +1,19 @@ +import { Head, ErrorComponent } from "blitz" + +// ------------------------------------------------------ +// This page is rendered if a route match is not found +// ------------------------------------------------------ +export default function Page404() { + const statusCode = 404 + const title = "This page could not be found" + return ( + <> + + + {statusCode}: {title} + + + + + ) +} diff --git a/examples/i18n-next-rosetta/app/pages/_app.tsx b/examples/i18n-next-rosetta/app/pages/_app.tsx new file mode 100644 index 0000000000..dd43122b07 --- /dev/null +++ b/examples/i18n-next-rosetta/app/pages/_app.tsx @@ -0,0 +1,43 @@ +import { + AppProps, + ErrorBoundary, + ErrorComponent, + AuthenticationError, + AuthorizationError, + ErrorFallbackProps, + useQueryErrorResetBoundary, +} from "blitz" +import LoginForm from "app/auth/components/LoginForm" +import { I18nProvider } from "next-rosetta" + +export default function App({ Component, pageProps }: AppProps) { + const getLayout = Component.getLayout || ((page) => page) + + return ( + + + {getLayout()} + + + ) +} + +function RootErrorFallback({ error, resetErrorBoundary }: ErrorFallbackProps) { + if (error instanceof AuthenticationError) { + return + } else if (error instanceof AuthorizationError) { + return ( + + ) + } else { + return ( + + ) + } +} diff --git a/examples/i18n-next-rosetta/app/pages/_document.tsx b/examples/i18n-next-rosetta/app/pages/_document.tsx new file mode 100644 index 0000000000..78b9fc4d99 --- /dev/null +++ b/examples/i18n-next-rosetta/app/pages/_document.tsx @@ -0,0 +1,23 @@ +import { Document, Html, DocumentHead, Main, BlitzScript /*DocumentContext*/ } from "blitz" + +class MyDocument extends Document { + // Only uncomment if you need to customize this behaviour + // static async getInitialProps(ctx: DocumentContext) { + // const initialProps = await Document.getInitialProps(ctx) + // return {...initialProps} + // } + + render() { + return ( + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/i18n-next-rosetta/app/pages/index.test.tsx b/examples/i18n-next-rosetta/app/pages/index.test.tsx new file mode 100644 index 0000000000..9374c60249 --- /dev/null +++ b/examples/i18n-next-rosetta/app/pages/index.test.tsx @@ -0,0 +1,25 @@ +import { render } from "test/utils" + +import Home from "./index" +import { useCurrentUser } from "app/core/hooks/useCurrentUser" + +jest.mock("app/core/hooks/useCurrentUser") +const mockUseCurrentUser = useCurrentUser as jest.MockedFunction + +test.skip("renders blitz documentation link", () => { + // This is an example of how to ensure a specific item is in the document + // But it's disabled by default (by test.skip) so the test doesn't fail + // when you remove the the default content from the page + + // This is an example on how to mock api hooks when testing + mockUseCurrentUser.mockReturnValue({ + id: 1, + name: "User", + email: "user@email.com", + role: "user", + }) + + const { getByText } = render() + const linkElement = getByText(/Documentation/i) + expect(linkElement).toBeInTheDocument() +}) diff --git a/examples/i18n-next-rosetta/app/pages/index.tsx b/examples/i18n-next-rosetta/app/pages/index.tsx new file mode 100644 index 0000000000..555c88e41f --- /dev/null +++ b/examples/i18n-next-rosetta/app/pages/index.tsx @@ -0,0 +1,301 @@ +import { Suspense } from "react" +import { Image, Link, BlitzPage, useMutation } from "blitz" +import Layout from "app/core/layouts/Layout" +import { useCurrentUser } from "app/core/hooks/useCurrentUser" +import logout from "app/auth/mutations/logout" +import logo from "public/logo.png" +import type { GetStaticProps } from "blitz" +import { useI18n, I18nProps } from "next-rosetta" +import type { MyLocale } from "app/core/i18n" +import { Routes } from ".blitz" + +/* + * This file is just for a pleasant getting started page for your new app. + * You can delete everything in here and start from scratch if you like. + */ + +const UserInfo = () => { + const currentUser = useCurrentUser() + const [logoutMutation] = useMutation(logout) + const { t } = useI18n() + + if (currentUser) { + return ( + <> + +
+ {t("home.userId")}: {currentUser.id} +
+ {t("home.userRole")}: {currentUser.role} +
+ + ) + } else { + return ( + <> + + + {t("home.signUp")} + + + + + {t("home.login")} + + + + ) + } +} + +const LangSelect = () => { + return ( +
+ + English + {" "} + + Français + +
+ ) +} + +const Home: BlitzPage = () => { + const { t } = useI18n() + return ( +
+ +
+
+ blitzjs +
+

+ {t("home.welcome.part1")} + {t("home.welcome.part2")} +

+
+ + + +
+

+ + {t("home.paragraph.line1")} +
+ {t("home.paragraph.line2")} +
+

+
+          {t("home.code")}
+        
+
{t("home.parenthesis")}
+
+

+ {t("home.restart.part1")} + {t("home.restart.part2")} +

+
+            Ctrl + c
+          
+
+            blitz dev
+          
+

+ {t("home.goto")}{" "} + + {t("home.project")} + +

+
+ +
+ + + + +
+ ) +} + +Home.suppressFirstRenderFlicker = true +Home.getLayout = (page) => {page} + +export const getStaticProps: GetStaticProps> = async (context) => { + const locale = context.locale || context.defaultLocale + const { table = {} } = await import(`app/core/i18n/${locale}`) // Import locale + return { props: { table } } // Passed to `/pages/_app.tsx` +} + +export default Home diff --git a/examples/i18n-next-rosetta/app/users/queries/getCurrentUser.ts b/examples/i18n-next-rosetta/app/users/queries/getCurrentUser.ts new file mode 100644 index 0000000000..9822e16bf6 --- /dev/null +++ b/examples/i18n-next-rosetta/app/users/queries/getCurrentUser.ts @@ -0,0 +1,13 @@ +import { Ctx } from "blitz" +import db from "db" + +export default async function getCurrentUser(_ = null, { session }: Ctx) { + if (!session.userId) return null + + const user = await db.user.findFirst({ + where: { id: session.userId }, + select: { id: true, name: true, email: true, role: true }, + }) + + return user +} diff --git a/examples/i18n-next-rosetta/babel.config.js b/examples/i18n-next-rosetta/babel.config.js new file mode 100644 index 0000000000..dfdf62cea1 --- /dev/null +++ b/examples/i18n-next-rosetta/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["blitz/babel"], + plugins: [], +} diff --git a/examples/i18n-next-rosetta/blitz.config.ts b/examples/i18n-next-rosetta/blitz.config.ts new file mode 100644 index 0000000000..62ad0cb6d5 --- /dev/null +++ b/examples/i18n-next-rosetta/blitz.config.ts @@ -0,0 +1,26 @@ +import { BlitzConfig, sessionMiddleware, simpleRolesIsAuthorized } from "blitz" + +const config: BlitzConfig = { + middleware: [ + sessionMiddleware({ + cookiePrefix: "i18n-next-rosetta-examples", + isAuthorized: simpleRolesIsAuthorized, + }), + ], + /* Uncomment this to customize the webpack config + webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + // Important: return the modified config + return config + }, + */ + i18n: { + // These are all the locales you want to support + locales: ["en", "fr"], + // This is the default locale you want to be used when visiting + // a path without locale prefix e.g. `/users` + defaultLocale: "en", + }, +} +module.exports = config diff --git a/examples/i18n-next-rosetta/db/index.ts b/examples/i18n-next-rosetta/db/index.ts new file mode 100644 index 0000000000..a63b57b248 --- /dev/null +++ b/examples/i18n-next-rosetta/db/index.ts @@ -0,0 +1,7 @@ +import { enhancePrisma } from "blitz" +import { PrismaClient } from "@prisma/client" + +const EnhancedPrisma = enhancePrisma(PrismaClient) + +export * from "@prisma/client" +export default new EnhancedPrisma() diff --git a/examples/i18n-next-rosetta/db/migrations/.keep b/examples/i18n-next-rosetta/db/migrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/i18n-next-rosetta/db/migrations/20211019122458_initial_migration/migration.sql b/examples/i18n-next-rosetta/db/migrations/20211019122458_initial_migration/migration.sql new file mode 100644 index 0000000000..b6eb866e1b --- /dev/null +++ b/examples/i18n-next-rosetta/db/migrations/20211019122458_initial_migration/migration.sql @@ -0,0 +1,47 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "name" TEXT, + "email" TEXT NOT NULL, + "hashedPassword" TEXT, + "role" TEXT NOT NULL DEFAULT 'USER' +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "expiresAt" DATETIME, + "handle" TEXT NOT NULL, + "hashedSessionToken" TEXT, + "antiCSRFToken" TEXT, + "publicData" TEXT, + "privateData" TEXT, + "userId" INTEGER, + CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Token" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "hashedToken" TEXT NOT NULL, + "type" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + "sentTo" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_handle_key" ON "Session"("handle"); + +-- CreateIndex +CREATE UNIQUE INDEX "Token_hashedToken_type_key" ON "Token"("hashedToken", "type"); diff --git a/examples/i18n-next-rosetta/db/migrations/migration_lock.toml b/examples/i18n-next-rosetta/db/migrations/migration_lock.toml new file mode 100644 index 0000000000..e5e5c4705a --- /dev/null +++ b/examples/i18n-next-rosetta/db/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/examples/i18n-next-rosetta/db/schema.prisma b/examples/i18n-next-rosetta/db/schema.prisma new file mode 100644 index 0000000000..83cd1b80f6 --- /dev/null +++ b/examples/i18n-next-rosetta/db/schema.prisma @@ -0,0 +1,65 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +} + +generator client { + provider = "prisma-client-js" +} + +// -------------------------------------- + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String? + email String @unique + hashedPassword String? + role String @default("USER") + + tokens Token[] + sessions Session[] +} + +model Session { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + expiresAt DateTime? + handle String @unique + hashedSessionToken String? + antiCSRFToken String? + publicData String? + privateData String? + + user User? @relation(fields: [userId], references: [id]) + userId Int? +} + +model Token { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + hashedToken String + type String + // See note below about TokenType enum + // type TokenType + expiresAt DateTime + sentTo String + + user User @relation(fields: [userId], references: [id]) + userId Int + + @@unique([hashedToken, type]) +} + +// NOTE: It's highly recommended to use an enum for the token type +// but enums only work in Postgres. +// See: https://blitzjs.com/docs/database-overview#switch-to-postgre-sql +// enum TokenType { +// RESET_PASSWORD +// } diff --git a/examples/i18n-next-rosetta/db/seeds.ts b/examples/i18n-next-rosetta/db/seeds.ts new file mode 100644 index 0000000000..cc39e51b95 --- /dev/null +++ b/examples/i18n-next-rosetta/db/seeds.ts @@ -0,0 +1,15 @@ +// import db from "./index" + +/* + * This seed function is executed when you run `blitz db seed`. + * + * Probably you want to use a library like https://chancejs.com + * to easily generate realistic data. + */ +const seed = async () => { + // for (let i = 0; i < 5; i++) { + // await db.project.create({ data: { name: "Project " + i } }) + // } +} + +export default seed diff --git a/examples/i18n-next-rosetta/integrations/.keep b/examples/i18n-next-rosetta/integrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/i18n-next-rosetta/jest.config.ts b/examples/i18n-next-rosetta/jest.config.ts new file mode 100644 index 0000000000..30fe3fc967 --- /dev/null +++ b/examples/i18n-next-rosetta/jest.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@jest/types" + +const config: Config.InitialOptions = { + preset: "blitz", +} + +export default config diff --git a/examples/i18n-next-rosetta/mailers/.keep b/examples/i18n-next-rosetta/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/i18n-next-rosetta/mailers/forgotPasswordMailer.ts b/examples/i18n-next-rosetta/mailers/forgotPasswordMailer.ts new file mode 100644 index 0000000000..5005e42fe4 --- /dev/null +++ b/examples/i18n-next-rosetta/mailers/forgotPasswordMailer.ts @@ -0,0 +1,45 @@ +/* TODO - You need to add a mailer integration in `integrations/` and import here. + * + * The integration file can be very simple. Instantiate the email client + * and then export it. That way you can import here and anywhere else + * and use it straight away. + */ + +type ResetPasswordMailer = { + to: string + token: string +} + +export function forgotPasswordMailer({ to, token }: ResetPasswordMailer) { + // In production, set APP_ORIGIN to your production server origin + const origin = process.env.APP_ORIGIN || process.env.BLITZ_DEV_SERVER_ORIGIN + const resetUrl = `${origin}/reset-password?token=${token}` + + const msg = { + from: "TODO@example.com", + to, + subject: "Your Password Reset Instructions", + html: ` +

Reset Your Password

+

NOTE: You must set up a production email integration in mailers/forgotPasswordMailer.ts

+ + + Click here to set a new password + + `, + } + + return { + async send() { + if (process.env.NODE_ENV === "production") { + // TODO - send the production email, like this: + // await postmark.sendEmail(msg) + throw new Error("No production email implementation in mailers/forgotPasswordMailer") + } else { + // Preview email in the browser + const previewEmail = (await import("preview-email")).default + await previewEmail(msg) + } + }, + } +} diff --git a/examples/i18n-next-rosetta/package.json b/examples/i18n-next-rosetta/package.json new file mode 100644 index 0000000000..be63cd02f9 --- /dev/null +++ b/examples/i18n-next-rosetta/package.json @@ -0,0 +1,49 @@ +{ + "name": "@examples/i18n-next-rosetta", + "version": "1.0.0", + "scripts": { + "dev": "blitz dev", + "build": "blitz build", + "start": "blitz start", + "studio": "blitz prisma studio", + "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", + "test": "prisma generate && blitz codegen && yarn test:jest", + "test:jest": "jest", + "test:watch": "jest --watch", + "prepare": "husky install" + }, + "prisma": { + "schema": "db/schema.prisma" + }, + "prettier": { + "semi": false, + "printWidth": 100 + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "eslint --fix" + ] + }, + "dependencies": { + "@prisma/client": "2.24.1", + "blitz": "0.45.4", + "final-form": "4.20.1", + "next-rosetta": "1.3.1", + "react": "0.0.0-experimental-6a589ad71", + "react-dom": "0.0.0-experimental-6a589ad71", + "react-final-form": "6.5.2", + "zod": "3.10.1" + }, + "devDependencies": { + "@types/preview-email": "2.0.0", + "@types/react": "17.0.2", + "eslint": "7.21.0", + "husky": "5.1.2", + "lint-staged": "10.5.4", + "prettier": "2.2.1", + "pretty-quick": "3.1.0", + "preview-email": "3.0.3", + "prisma": "2.24.1" + }, + "private": true +} diff --git a/examples/i18n-next-rosetta/public/favicon.ico b/examples/i18n-next-rosetta/public/favicon.ico new file mode 100755 index 0000000000..c7bd1c095d Binary files /dev/null and b/examples/i18n-next-rosetta/public/favicon.ico differ diff --git a/examples/i18n-next-rosetta/public/logo.png b/examples/i18n-next-rosetta/public/logo.png new file mode 100644 index 0000000000..6a982616ee Binary files /dev/null and b/examples/i18n-next-rosetta/public/logo.png differ diff --git a/examples/i18n-next-rosetta/test/setup.ts b/examples/i18n-next-rosetta/test/setup.ts new file mode 100644 index 0000000000..c278ddb9e6 --- /dev/null +++ b/examples/i18n-next-rosetta/test/setup.ts @@ -0,0 +1,4 @@ +// This is the jest 'setupFilesAfterEnv' setup file +// It's a good place to set globals, add global before/after hooks, etc + +export {} // so TS doesn't complain diff --git a/examples/i18n-next-rosetta/test/utils.tsx b/examples/i18n-next-rosetta/test/utils.tsx new file mode 100644 index 0000000000..6d52b9baa7 --- /dev/null +++ b/examples/i18n-next-rosetta/test/utils.tsx @@ -0,0 +1,105 @@ +import { RouterContext, BlitzRouter, BlitzProvider } from "blitz" +import { render as defaultRender } from "@testing-library/react" +import { renderHook as defaultRenderHook } from "@testing-library/react-hooks" + +export * from "@testing-library/react" + +// -------------------------------------------------------------------------------- +// This file customizes the render() and renderHook() test functions provided +// by React testing library. It adds a router context wrapper with a mocked router. +// +// You should always import `render` and `renderHook` from this file +// +// This is the place to add any other context providers you need while testing. +// -------------------------------------------------------------------------------- + +// -------------------------------------------------- +// render() +// -------------------------------------------------- +// Override the default test render with our own +// +// You can override the router mock like this: +// +// const { baseElement } = render(, { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function render( + ui: RenderUI, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }) => ( + + + {children} + + + ) + } + return defaultRender(ui, { wrapper, ...options }) +} + +// -------------------------------------------------- +// renderHook() +// -------------------------------------------------- +// Override the default test renderHook with our own +// +// You can override the router mock like this: +// +// const result = renderHook(() => myHook(), { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function renderHook( + hook: RenderHook, + { wrapper, router, dehydratedState, ...options }: RenderHookOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }) => ( + + + {children} + + + ) + } + return defaultRenderHook(hook, { wrapper, ...options }) +} + +export const mockRouter: BlitzRouter = { + basePath: "", + pathname: "/", + route: "/", + asPath: "/", + params: {}, + query: {}, + isReady: true, + isLocaleDomain: false, + isPreview: false, + push: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + isFallback: false, +} + +type DefaultParams = Parameters +type RenderUI = DefaultParams[0] +type RenderOptions = DefaultParams[1] & { router?: Partial; dehydratedState?: unknown } + +type DefaultHookParams = Parameters +type RenderHook = DefaultHookParams[0] +type RenderHookOptions = DefaultHookParams[1] & { + router?: Partial + dehydratedState?: unknown +} diff --git a/examples/i18n-next-rosetta/tsconfig.json b/examples/i18n-next-rosetta/tsconfig.json new file mode 100644 index 0000000000..8889c01427 --- /dev/null +++ b/examples/i18n-next-rosetta/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": "./", + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "exclude": ["node_modules", "**/*.e2e.ts", "cypress"], + "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"] +} diff --git a/examples/i18n-next-rosetta/types.ts b/examples/i18n-next-rosetta/types.ts new file mode 100644 index 0000000000..82209331c2 --- /dev/null +++ b/examples/i18n-next-rosetta/types.ts @@ -0,0 +1,18 @@ +import { DefaultCtx, SessionContext, SimpleRolesIsAuthorized } from "blitz" +import { User } from "db" + +// Note: You should switch to Postgres and then use a DB enum for role type +export type Role = "ADMIN" | "USER" + +declare module "blitz" { + export interface Ctx extends DefaultCtx { + session: SessionContext + } + export interface Session { + isAuthorized: SimpleRolesIsAuthorized + PublicData: { + userId: User["id"] + role: Role + } + } +} diff --git a/examples/no-prisma/.babelrc.js b/examples/no-prisma/.babelrc.js new file mode 100644 index 0000000000..a14498291c --- /dev/null +++ b/examples/no-prisma/.babelrc.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["next/babel"], + plugins: [], +} diff --git a/examples/no-prisma/.eslintrc.js b/examples/no-prisma/.eslintrc.js new file mode 100644 index 0000000000..71f356cbaf --- /dev/null +++ b/examples/no-prisma/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + env: { + es2020: true, + }, + extends: ["react-app", "plugin:jsx-a11y/recommended"], + plugins: ["jsx-a11y"], + rules: { + "import/no-anonymous-default-export": "error", + "import/no-webpack-loader-syntax": "off", + "react/react-in-jsx-scope": "off", // React is always in scope with Blitz + "jsx-a11y/anchor-is-valid": "off", //Doesn't play well with Blitz/Next usage + }, +} diff --git a/examples/no-prisma/.gitignore b/examples/no-prisma/.gitignore new file mode 100644 index 0000000000..6eed138b71 --- /dev/null +++ b/examples/no-prisma/.gitignore @@ -0,0 +1,55 @@ +# dependencies +node_modules +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* +.npm +web_modules/ + +# blitz +/.blitz/ +/.next/ +*.sqlite +.now +.blitz-console-history +blitz-log.log + +# misc +.DS_Store + +# local env files +.env +.envrc +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +coverage +*.lcov +.nyc_output +lib-cov + +# Caches +*.tsbuildinfo +.eslintcache +.node_repl_history +.yarn-integrity + +# Serverless directories +.serverless/ + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test diff --git a/examples/no-prisma/.npmrc b/examples/no-prisma/.npmrc new file mode 100644 index 0000000000..cffe8cdef1 --- /dev/null +++ b/examples/no-prisma/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/examples/no-prisma/.prettierignore b/examples/no-prisma/.prettierignore new file mode 100644 index 0000000000..c61fdf72dc --- /dev/null +++ b/examples/no-prisma/.prettierignore @@ -0,0 +1,5 @@ +.gitkeep +.env* +*.ico +*.lock +db/migrations diff --git a/examples/no-prisma/README.md b/examples/no-prisma/README.md new file mode 100644 index 0000000000..a04ef974fd --- /dev/null +++ b/examples/no-prisma/README.md @@ -0,0 +1,20 @@ +# no-prisma + +## Getting Started + +1. Add this code to db/schema.prisma: + +``` +model Project { + id Int @default(autoincrement()) @id + name String +} +``` + +3. Start the dev server + +``` +blitz dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/examples/no-prisma/app/components/.keep b/examples/no-prisma/app/components/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/no-prisma/app/layouts/.keep b/examples/no-prisma/app/layouts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/no-prisma/app/mutations/addGIF.ts b/examples/no-prisma/app/mutations/addGIF.ts new file mode 100644 index 0000000000..a5ec409ed1 --- /dev/null +++ b/examples/no-prisma/app/mutations/addGIF.ts @@ -0,0 +1,6 @@ +import db from "db" +import { GIFModel } from "db/GIFModel" + +export default async function addGIF(url: string) { + await db("gifs").insert({ url }) +} diff --git a/examples/no-prisma/app/mutations/addRating.ts b/examples/no-prisma/app/mutations/addRating.ts new file mode 100644 index 0000000000..0d2706c110 --- /dev/null +++ b/examples/no-prisma/app/mutations/addRating.ts @@ -0,0 +1,6 @@ +import RatingModel from "db/RatingModel" +import db from "db" + +export default async function addRating(rating: RatingModel) { + await db("ratings").insert(rating) +} diff --git a/examples/no-prisma/app/pages/_app.tsx b/examples/no-prisma/app/pages/_app.tsx new file mode 100644 index 0000000000..9dadda2420 --- /dev/null +++ b/examples/no-prisma/app/pages/_app.tsx @@ -0,0 +1,5 @@ +import { AppProps } from "blitz" + +export default function MyApp({ Component, pageProps }: AppProps) { + return +} diff --git a/examples/no-prisma/app/pages/_document.tsx b/examples/no-prisma/app/pages/_document.tsx new file mode 100644 index 0000000000..78b9fc4d99 --- /dev/null +++ b/examples/no-prisma/app/pages/_document.tsx @@ -0,0 +1,23 @@ +import { Document, Html, DocumentHead, Main, BlitzScript /*DocumentContext*/ } from "blitz" + +class MyDocument extends Document { + // Only uncomment if you need to customize this behaviour + // static async getInitialProps(ctx: DocumentContext) { + // const initialProps = await Document.getInitialProps(ctx) + // return {...initialProps} + // } + + render() { + return ( + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/no-prisma/app/pages/index.module.css b/examples/no-prisma/app/pages/index.module.css new file mode 100644 index 0000000000..0bcbeab666 --- /dev/null +++ b/examples/no-prisma/app/pages/index.module.css @@ -0,0 +1,32 @@ +.main { + margin: 10px; +} + +.gif_list { + list-style-type: none; + margin: 0; + padding: 0; +} + +.gif_list > li { + border: solid black 1px; + border-radius: 10px; + padding: 10px; + margin: 20px; + width: fit-content; +} + +.gif_list > li > img { + border-radius: 10px; + margin-bottom: 10px; + display: block; +} + +.gif_list > li > span { + margin-left: 5px; + margin-right: 10px; +} + +.gif_list > li > span > select { + float: right; +} diff --git a/examples/no-prisma/app/pages/index.tsx b/examples/no-prisma/app/pages/index.tsx new file mode 100644 index 0000000000..dc6fb7731e --- /dev/null +++ b/examples/no-prisma/app/pages/index.tsx @@ -0,0 +1,81 @@ +import { Suspense, useState } from "react" +import { useQuery } from "blitz" +import getGIFs from "app/queries/getGIFs" +import addRating from "app/mutations/addRating" +import addGIF from "app/mutations/addGIF" +import styles from "./index.module.css" + +function GIFRater() { + const [gifs, gifsQuery] = useQuery(getGIFs, {}) + + const [gifTitle, setGifTitle] = useState() + + return ( +
+

The best GIFs of all time:

+ +
+ setGifTitle(evt.target.value)} + placeholder="Submit another GIF" + type="url" + > + +
+ +
    + {gifs.map((gif) => ( +
  • + GIF + + Rating: {gif.rating.toFixed(1)} + + +
  • + ))} +
+
+ ) +} + +function Home() { + return ( + + + + ) +} + +export default Home diff --git a/examples/no-prisma/app/queries/getGIFs.ts b/examples/no-prisma/app/queries/getGIFs.ts new file mode 100644 index 0000000000..34f5de8ceb --- /dev/null +++ b/examples/no-prisma/app/queries/getGIFs.ts @@ -0,0 +1,10 @@ +import db from "db" +import { GIFModel } from "db/GIFModel" + +export default async function getGIFs(): Promise<{ url: string; rating: number }[]> { + return await db("gifs") + .select("url") + .leftJoin("ratings", "ratings.gif_url", "gifs.url") + .groupBy("url") + .avg("rating as rating") +} diff --git a/examples/no-prisma/blitz.config.js b/examples/no-prisma/blitz.config.js new file mode 100644 index 0000000000..3b17b02ef1 --- /dev/null +++ b/examples/no-prisma/blitz.config.js @@ -0,0 +1,13 @@ +module.exports = { + webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + // Important: return the modified config + return config + }, + webpackDevMiddleware: (config) => { + // Perform customizations to webpack dev middleware config + // Important: return the modified config + return config + }, +} diff --git a/examples/no-prisma/db/GIFModel.ts b/examples/no-prisma/db/GIFModel.ts new file mode 100644 index 0000000000..50710c0085 --- /dev/null +++ b/examples/no-prisma/db/GIFModel.ts @@ -0,0 +1,3 @@ +export interface GIFModel { + url: string +} diff --git a/examples/no-prisma/db/RatingModel.ts b/examples/no-prisma/db/RatingModel.ts new file mode 100644 index 0000000000..94c896beea --- /dev/null +++ b/examples/no-prisma/db/RatingModel.ts @@ -0,0 +1,4 @@ +export default interface RatingModel { + gif_url: string + rating: 1 | 2 | 3 | 4 | 5 +} diff --git a/examples/no-prisma/db/createDatabase.ts b/examples/no-prisma/db/createDatabase.ts new file mode 100644 index 0000000000..2ed46754d0 --- /dev/null +++ b/examples/no-prisma/db/createDatabase.ts @@ -0,0 +1,35 @@ +import type Knex from "knex" +import { GIFModel } from "./GIFModel" +import RatingModel from "./RatingModel" + +export async function createDatabase(knex: Knex) { + await knex.schema.createTableIfNotExists("gifs", (table) => { + table.string("url").primary().notNullable() + }) + + await knex.schema.createTableIfNotExists("ratings", (table) => { + table.increments() + table.string("gif_url").references("gifs.url").notNullable() + table.enum("rating", [1, 2, 3, 4, 5]).notNullable() + }) + + try { + await knex("gifs").insert([ + { url: "https://media.giphy.com/media/kreQ1pqlSzftm/giphy.gif" }, + { url: "https://media.giphy.com/media/10vk5L8tHKN1UQ/giphy.gif" }, + ]) + } catch (error) { + // ignore + } + + const [{ count }] = await knex("ratings").count("id as count") + if (count === 0) { + await knex("ratings").insert([ + { gif_url: "https://media.giphy.com/media/kreQ1pqlSzftm/giphy.gif", rating: 4 }, + { gif_url: "https://media.giphy.com/media/kreQ1pqlSzftm/giphy.gif", rating: 5 }, + { gif_url: "https://media.giphy.com/media/10vk5L8tHKN1UQ/giphy.gif", rating: 3 }, + { gif_url: "https://media.giphy.com/media/10vk5L8tHKN1UQ/giphy.gif", rating: 2 }, + { gif_url: "https://media.giphy.com/media/10vk5L8tHKN1UQ/giphy.gif", rating: 5 }, + ]) + } +} diff --git a/examples/no-prisma/db/index.ts b/examples/no-prisma/db/index.ts new file mode 100644 index 0000000000..93d5027beb --- /dev/null +++ b/examples/no-prisma/db/index.ts @@ -0,0 +1,35 @@ +import Knex from "knex" +import { createDatabase } from "./createDatabase" + +let knex: Knex | undefined = undefined + +function getKnex() { + return Knex({ + client: "sqlite3", + connection: { + filename: "./mydb.sqlite", + }, + }) +} + +if (process.env.NODE_ENV === "production") { + knex = getKnex() +} else { + // Ensure the knex instance is re-used during hot-reloading + // Otherwise, a new client will be created on every reload + globalThis["knex"] = globalThis["knex"] || getKnex() + knex = globalThis["knex"] +} + +export default knex + +let alreadyCreatedDatabase = false + +export async function connect() { + if (alreadyCreatedDatabase) { + return + } + + await createDatabase(knex) + alreadyCreatedDatabase = true +} diff --git a/examples/no-prisma/integrations/.keep b/examples/no-prisma/integrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/no-prisma/package.json b/examples/no-prisma/package.json new file mode 100644 index 0000000000..63403eeb02 --- /dev/null +++ b/examples/no-prisma/package.json @@ -0,0 +1,50 @@ +{ + "name": "@examples/no-prisma", + "version": "0.34.0-canary.0", + "scripts": { + "dev": "blitz dev", + "build": "blitz build", + "start": "blitz start", + "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", + "test": "echo \"No tests yet\"" + }, + "browserslist": [ + "defaults" + ], + "prettier": { + "semi": false, + "printWidth": 100 + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged && pretty-quick --staged" + } + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "eslint --fix" + ] + }, + "dependencies": { + "blitz": "0.45.4", + "knex": "0.21.16", + "react": "0.0.0-experimental-6a589ad71", + "react-dom": "0.0.0-experimental-6a589ad71", + "sqlite3": "5.0.2" + }, + "devDependencies": { + "@types/react": "17.0.2", + "eslint": "7.21.0", + "eslint-config-react-app": "~6.0.0", + "eslint-plugin-flowtype": "~5.2.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.23.1", + "eslint-plugin-react-hooks": "^4.2.0", + "husky": "5.1.2", + "lint-staged": "10.5.4", + "prettier": "2.2.1", + "pretty-quick": "3.1.0" + }, + "private": true +} diff --git a/examples/no-prisma/public/favicon.ico b/examples/no-prisma/public/favicon.ico new file mode 100755 index 0000000000..a94b0f7a7f Binary files /dev/null and b/examples/no-prisma/public/favicon.ico differ diff --git a/examples/no-prisma/public/logo.png b/examples/no-prisma/public/logo.png new file mode 100644 index 0000000000..6a982616ee Binary files /dev/null and b/examples/no-prisma/public/logo.png differ diff --git a/examples/no-prisma/tsconfig.json b/examples/no-prisma/tsconfig.json new file mode 100644 index 0000000000..c7046f8a3f --- /dev/null +++ b/examples/no-prisma/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": "./", + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "exclude": ["node_modules"], + "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"] +} diff --git a/examples/no-prisma/types b/examples/no-prisma/types new file mode 120000 index 0000000000..8788aa2845 --- /dev/null +++ b/examples/no-prisma/types @@ -0,0 +1 @@ +../../types \ No newline at end of file diff --git a/examples/no-prisma/utils/.keep b/examples/no-prisma/utils/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/plain-js/.babelrc.js b/examples/plain-js/.babelrc.js new file mode 100644 index 0000000000..a14498291c --- /dev/null +++ b/examples/plain-js/.babelrc.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["next/babel"], + plugins: [], +} diff --git a/examples/plain-js/.eslintrc.js b/examples/plain-js/.eslintrc.js new file mode 100644 index 0000000000..71f356cbaf --- /dev/null +++ b/examples/plain-js/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + env: { + es2020: true, + }, + extends: ["react-app", "plugin:jsx-a11y/recommended"], + plugins: ["jsx-a11y"], + rules: { + "import/no-anonymous-default-export": "error", + "import/no-webpack-loader-syntax": "off", + "react/react-in-jsx-scope": "off", // React is always in scope with Blitz + "jsx-a11y/anchor-is-valid": "off", //Doesn't play well with Blitz/Next usage + }, +} diff --git a/examples/plain-js/.gitignore b/examples/plain-js/.gitignore new file mode 100644 index 0000000000..6eed138b71 --- /dev/null +++ b/examples/plain-js/.gitignore @@ -0,0 +1,55 @@ +# dependencies +node_modules +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* +.npm +web_modules/ + +# blitz +/.blitz/ +/.next/ +*.sqlite +.now +.blitz-console-history +blitz-log.log + +# misc +.DS_Store + +# local env files +.env +.envrc +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +coverage +*.lcov +.nyc_output +lib-cov + +# Caches +*.tsbuildinfo +.eslintcache +.node_repl_history +.yarn-integrity + +# Serverless directories +.serverless/ + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test diff --git a/examples/plain-js/.npmrc b/examples/plain-js/.npmrc new file mode 100644 index 0000000000..cffe8cdef1 --- /dev/null +++ b/examples/plain-js/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/examples/plain-js/.prettierignore b/examples/plain-js/.prettierignore new file mode 100644 index 0000000000..fc61fdc4dc --- /dev/null +++ b/examples/plain-js/.prettierignore @@ -0,0 +1,5 @@ +.gitkeep +.env +*.ico +*.lock +db/migrations diff --git a/examples/plain-js/README.md b/examples/plain-js/README.md new file mode 100644 index 0000000000..d90ea0176c --- /dev/null +++ b/examples/plain-js/README.md @@ -0,0 +1,26 @@ +# plain-js + +## Getting Started + +1. Add this code to db/schema.prisma: + +``` +model Project { + id Int @default(autoincrement()) @id + name String +} +``` + +2. DB migrate + +``` +blitz prisma migrate dev +``` + +3. Start the dev server + +``` +blitz dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/examples/plain-js/app/components/.keep b/examples/plain-js/app/components/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/plain-js/app/layouts/.keep b/examples/plain-js/app/layouts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/plain-js/app/pages/_app.js b/examples/plain-js/app/pages/_app.js new file mode 100644 index 0000000000..63ae4ea8f7 --- /dev/null +++ b/examples/plain-js/app/pages/_app.js @@ -0,0 +1,3 @@ +export default function MyApp({Component, pageProps}) { + return +} diff --git a/examples/plain-js/app/pages/_document.js b/examples/plain-js/app/pages/_document.js new file mode 100644 index 0000000000..ac9c8fdd3a --- /dev/null +++ b/examples/plain-js/app/pages/_document.js @@ -0,0 +1,29 @@ +import { + Document, + Html, + DocumentHead, + Main, + BlitzScript, + /*DocumentContext*/ +} from "blitz" + +class MyDocument extends Document { + // Only uncomment if you need to customize this behaviour + // static async getInitialProps(ctx: DocumentContext) { + // const initialProps = await Document.getInitialProps(ctx) + // return {...initialProps} + // } + render() { + return ( + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/plain-js/app/pages/index.js b/examples/plain-js/app/pages/index.js new file mode 100644 index 0000000000..158af0b9fe --- /dev/null +++ b/examples/plain-js/app/pages/index.js @@ -0,0 +1,205 @@ +import {Head, Link} from "blitz" +const modelSnippet = `model Project { + id Int @default(autoincrement()) @id + name String +}` +const migrateSnippet = `$ blitz prisma migrate dev +$ blitz generate all project` + +const Home = () => ( +
+ + plain-js + + + +
+
+ blitz.js +
+

+ 1. Add this code to db/schema.prisma: +

+
+        {modelSnippet}
+      
+

2. Run these commands in your terminal:

+
+        {migrateSnippet}
+      
+ +

+ 3. Go to{" "} + + /projects + +

+ +
+ + + + + + +
+) + +export default Home diff --git a/examples/plain-js/app/projects/mutations/createProject.js b/examples/plain-js/app/projects/mutations/createProject.js new file mode 100644 index 0000000000..21de311d28 --- /dev/null +++ b/examples/plain-js/app/projects/mutations/createProject.js @@ -0,0 +1,5 @@ +import db from "db" +export default async function createProject(args) { + const project = await db.project.create(args) + return project +} diff --git a/examples/plain-js/app/projects/mutations/deleteProject.js b/examples/plain-js/app/projects/mutations/deleteProject.js new file mode 100644 index 0000000000..8518cf6083 --- /dev/null +++ b/examples/plain-js/app/projects/mutations/deleteProject.js @@ -0,0 +1,5 @@ +import db from "db" +export default async function deleteProject(args) { + const project = await db.project.delete(args) + return project +} diff --git a/examples/plain-js/app/projects/mutations/updateProject.js b/examples/plain-js/app/projects/mutations/updateProject.js new file mode 100644 index 0000000000..0494a9238d --- /dev/null +++ b/examples/plain-js/app/projects/mutations/updateProject.js @@ -0,0 +1,7 @@ +import db from "db" +export default async function updateProject(args) { + // Don't allow updating ID + delete args.data.id + const project = await db.project.update(args) + return project +} diff --git a/examples/plain-js/app/projects/pages/projects/[id].js b/examples/plain-js/app/projects/pages/projects/[id].js new file mode 100644 index 0000000000..3c00e72d3b --- /dev/null +++ b/examples/plain-js/app/projects/pages/projects/[id].js @@ -0,0 +1,64 @@ +import {Suspense} from "react" +import {Head, Link, useRouter, useQuery} from "blitz" +import getProject from "app/projects/queries/getProject" +import deleteProject from "app/projects/mutations/deleteProject" +export const Project = () => { + const router = useRouter() + const id = parseInt(router?.query.id) + const [project] = useQuery(getProject, { + where: { + id, + }, + }) + return ( +
+

Project {project.id}

+
{JSON.stringify(project)}
+ + + Edit + + + +
+ ) +} + +const ShowProjectPage = () => { + return ( +
+ + Project + + + +
+

+ + Projects + +

+ + Loading...
}> + + +
+ + ) +} + +export default ShowProjectPage diff --git a/examples/plain-js/app/projects/pages/projects/[id]/edit.js b/examples/plain-js/app/projects/pages/projects/[id]/edit.js new file mode 100644 index 0000000000..b8bd2b9380 --- /dev/null +++ b/examples/plain-js/app/projects/pages/projects/[id]/edit.js @@ -0,0 +1,68 @@ +import {Suspense} from "react" +import {Head, Link, useRouter, useQuery} from "blitz" +import getProject from "app/projects/queries/getProject" +import updateProject from "app/projects/mutations/updateProject" +export const EditProject = () => { + const router = useRouter() + const id = parseInt(router?.query.id) + const [project] = useQuery(getProject, { + where: { + id, + }, + }) + return ( +
+

Edit Project {project.id}

+
{JSON.stringify(project)}
+ +
{ + event.preventDefault() + + try { + const updated = await updateProject({ + where: { + id: project.id, + }, + data: { + name: "MyNewName", + }, + }) + alert("Success!" + JSON.stringify(updated)) + router.push(`/projects/${updated.id}`) + } catch (error) { + alert("Error creating project " + JSON.stringify(error, null, 2)) + } + }} + > +
Put your form fields here. But for now, just click submit
+ +
+
+ ) +} + +const EditProjectPage = () => { + return ( +
+ + Edit Project + + + +
+ Loading...
}> + + + +

+ + Projects + +

+
+ + ) +} + +export default EditProjectPage diff --git a/examples/plain-js/app/projects/pages/projects/index.js b/examples/plain-js/app/projects/pages/projects/index.js new file mode 100644 index 0000000000..fe6016bda4 --- /dev/null +++ b/examples/plain-js/app/projects/pages/projects/index.js @@ -0,0 +1,45 @@ +import {Suspense} from "react" +import {Head, Link, useQuery} from "blitz" +import getProjects from "app/projects/queries/getProjects" + +export const ProjectsList = () => { + const [projects] = useQuery(getProjects, {}) + return ( + + ) +} + +const ProjectsPage = () => { + return ( +
+ + Projects + + + +
+

Projects

+ +

+ + Create Project + +

+ + Loading...
}> + + +
+ + ) +} + +export default ProjectsPage diff --git a/examples/plain-js/app/projects/pages/projects/new.js b/examples/plain-js/app/projects/pages/projects/new.js new file mode 100644 index 0000000000..04a3488ee8 --- /dev/null +++ b/examples/plain-js/app/projects/pages/projects/new.js @@ -0,0 +1,47 @@ +import {Head, Link, useRouter} from "blitz" +import createProject from "app/projects/mutations/createProject" + +const NewProjectPage = () => { + const router = useRouter() + return ( +
+ + New Project + + + +
+

Create New Project

+ +
{ + event.preventDefault() + + try { + const project = await createProject({ + data: { + name: "MyName", + }, + }) + alert("Success!" + JSON.stringify(project)) + router.push(`/projects/${project.id}`) + } catch (error) { + alert("Error creating project " + JSON.stringify(error, null, 2)) + } + }} + > +
Put your form fields here. But for now, just click submit
+ +
+ +

+ + Projects + +

+
+
+ ) +} + +export default NewProjectPage diff --git a/examples/plain-js/app/projects/queries/getProject.js b/examples/plain-js/app/projects/queries/getProject.js new file mode 100644 index 0000000000..22d2f4d22a --- /dev/null +++ b/examples/plain-js/app/projects/queries/getProject.js @@ -0,0 +1,5 @@ +import db from "db" +export default async function getProject(args) { + const project = await db.project.findFirst(args) + return project +} diff --git a/examples/plain-js/app/projects/queries/getProjects.js b/examples/plain-js/app/projects/queries/getProjects.js new file mode 100644 index 0000000000..787d2990c8 --- /dev/null +++ b/examples/plain-js/app/projects/queries/getProjects.js @@ -0,0 +1,5 @@ +import db from "db" +export default async function getProjects(args) { + const projects = await db.project.findMany(args) + return projects +} diff --git a/examples/plain-js/blitz.config.js b/examples/plain-js/blitz.config.js new file mode 100644 index 0000000000..80bd1073ce --- /dev/null +++ b/examples/plain-js/blitz.config.js @@ -0,0 +1,13 @@ +module.exports = { + webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + // Important: return the modified config + return config + }, + webpackDevMiddleware: (config) => { + // Perform customizations to webpack dev middleware config + // Important: return the modified config + return config + }, +} diff --git a/examples/plain-js/db/index.js b/examples/plain-js/db/index.js new file mode 100644 index 0000000000..7cfdf51d3b --- /dev/null +++ b/examples/plain-js/db/index.js @@ -0,0 +1,15 @@ +import {PrismaClient} from "@prisma/client" +export * from "@prisma/client" + +let prisma + +if (process.env.NODE_ENV === "production") { + prisma = new PrismaClient() +} else { + // Ensure the prisma instance is re-used during hot-reloading + // Otherwise, a new client will be created on every reload + globalThis.prisma = globalThis.prisma || new PrismaClient() + prisma = globalThis.prisma +} + +export default prisma diff --git a/examples/plain-js/db/migrations/.keep b/examples/plain-js/db/migrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/plain-js/db/migrations/20200514183443/README.md b/examples/plain-js/db/migrations/20200514183443/README.md new file mode 100644 index 0000000000..6824915f0c --- /dev/null +++ b/examples/plain-js/db/migrations/20200514183443/README.md @@ -0,0 +1,58 @@ +# Migration `20200514183443` + +This migration has been generated by Brandon Bayer at 5/14/2020, 6:34:43 PM. +You can check out the [state of the schema](./schema.prisma) after the migration. + +## Database Steps + +```sql +PRAGMA foreign_keys=OFF; + +CREATE TABLE "quaint"."Project" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL +) + +PRAGMA "quaint".foreign_key_check; + +PRAGMA foreign_keys=ON; +``` + +## Changes + +```diff +diff --git schema.prisma schema.prisma +migration ..20200514183443 +--- datamodel.dml ++++ datamodel.dml +@@ -1,0 +1,27 @@ ++// This is your Prisma schema file, ++// learn more about it in the docs: https://pris.ly/d/prisma-schema ++ ++datasource sqlite { ++ provider = "sqlite" ++ url = "file:./db.sqlite" ++} ++ ++// SQLite is easy to start with, but if you use Postgres in production ++// you should also use it in development with the following: ++//datasource postgresql { ++// provider = "postgresql" ++// url = env("DATABASE_URL") ++//} ++ ++generator client { ++ provider = "prisma-client-js" ++} ++ ++ ++// -------------------------------------- ++ ++model Project { ++ id Int @default(autoincrement()) @id ++ name String ++} ++ +``` + + diff --git a/examples/plain-js/db/migrations/20200514183443/schema.prisma b/examples/plain-js/db/migrations/20200514183443/schema.prisma new file mode 100644 index 0000000000..9d41a5e861 --- /dev/null +++ b/examples/plain-js/db/migrations/20200514183443/schema.prisma @@ -0,0 +1,27 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource sqlite { + provider = "sqlite" + url = "***" +} + +// SQLite is easy to start with, but if you use Postgres in production +// you should also use it in development with the following: +//datasource postgresql { +// provider = "postgresql" +// url = "***" +//} + +generator client { + provider = "prisma-client-js" +} + + +// -------------------------------------- + +model Project { + id Int @default(autoincrement()) @id + name String +} + diff --git a/examples/plain-js/db/migrations/20200514183443/steps.json b/examples/plain-js/db/migrations/20200514183443/steps.json new file mode 100644 index 0000000000..d2e5363240 --- /dev/null +++ b/examples/plain-js/db/migrations/20200514183443/steps.json @@ -0,0 +1,81 @@ +{ + "version": "0.3.14-fixed", + "steps": [ + { + "tag": "CreateSource", + "source": "sqlite" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Source", + "source": "sqlite" + }, + "argument": "provider", + "value": "\"sqlite\"" + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Source", + "source": "sqlite" + }, + "argument": "url", + "value": "\"file:./db.sqlite\"" + }, + { + "tag": "CreateModel", + "model": "Project" + }, + { + "tag": "CreateField", + "model": "Project", + "field": "id", + "type": "Int", + "arity": "Required" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Project", + "field": "id" + }, + "directive": "default" + } + }, + { + "tag": "CreateArgument", + "location": { + "tag": "Directive", + "path": { + "tag": "Field", + "model": "Project", + "field": "id" + }, + "directive": "default" + }, + "argument": "", + "value": "autoincrement()" + }, + { + "tag": "CreateDirective", + "location": { + "path": { + "tag": "Field", + "model": "Project", + "field": "id" + }, + "directive": "id" + } + }, + { + "tag": "CreateField", + "model": "Project", + "field": "name", + "type": "String", + "arity": "Required" + } + ] +} \ No newline at end of file diff --git a/examples/plain-js/db/migrations/migrate.lock b/examples/plain-js/db/migrations/migrate.lock new file mode 100644 index 0000000000..2ca6e58efd --- /dev/null +++ b/examples/plain-js/db/migrations/migrate.lock @@ -0,0 +1,6 @@ +# IF THERE'S A GIT CONFLICT IN THIS FILE, DON'T SOLVE IT MANUALLY! +# INSTEAD EXECUTE `prisma migrate fix` +# Prisma Migrate lockfile v1 +# Read more about conflict resolution here: TODO + +20200514183443 \ No newline at end of file diff --git a/examples/plain-js/db/schema.prisma b/examples/plain-js/db/schema.prisma new file mode 100644 index 0000000000..41ef563423 --- /dev/null +++ b/examples/plain-js/db/schema.prisma @@ -0,0 +1,27 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource sqlite { + provider = "sqlite" + url = "file:./db.sqlite" +} + +// SQLite is easy to start with, but if you use Postgres in production +// you should also use it in development with the following: +//datasource postgresql { +// provider = "postgresql" +// url = env("DATABASE_URL") +//} + +generator client { + provider = "prisma-client-js" +} + + +// -------------------------------------- + +model Project { + id Int @default(autoincrement()) @id + name String +} + diff --git a/examples/plain-js/integrations/.keep b/examples/plain-js/integrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/plain-js/jobs/.keep b/examples/plain-js/jobs/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/plain-js/jsconfig.json b/examples/plain-js/jsconfig.json new file mode 100644 index 0000000000..36aa1a4dc2 --- /dev/null +++ b/examples/plain-js/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "baseUrl": "." + } +} diff --git a/examples/plain-js/package.json b/examples/plain-js/package.json new file mode 100644 index 0000000000..62956e2a11 --- /dev/null +++ b/examples/plain-js/package.json @@ -0,0 +1,55 @@ +{ + "name": "@examples/plain-js", + "version": "0.34.0-canary.0", + "scripts": { + "dev": "blitz dev", + "build": "blitz prisma migrate deploy && blitz build", + "start": "blitz start", + "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", + "test": "echo \"DISABLED\"" + }, + "browserslist": [ + "defaults" + ], + "prisma": { + "schema": "db/schema.prisma" + }, + "prettier": { + "semi": false, + "printWidth": 100, + "bracketSpacing": false, + "trailingComma": "all" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged && pretty-quick --staged" + } + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "eslint --fix", + "git add" + ] + }, + "dependencies": { + "@prisma/client": "2.24.1", + "blitz": "0.45.4", + "prisma": "2.24.1", + "react": "0.0.0-experimental-6a589ad71", + "react-dom": "0.0.0-experimental-6a589ad71" + }, + "devDependencies": { + "eslint": "7.21.0", + "eslint-config-react-app": "~6.0.0", + "eslint-plugin-flowtype": "~5.2.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.23.1", + "eslint-plugin-react-hooks": "^4.2.0", + "husky": "5.1.2", + "lint-staged": "10.5.4", + "prettier": "2.2.1", + "pretty-quick": "3.1.0" + }, + "private": true +} diff --git a/examples/plain-js/public/favicon.ico b/examples/plain-js/public/favicon.ico new file mode 100755 index 0000000000..a94b0f7a7f Binary files /dev/null and b/examples/plain-js/public/favicon.ico differ diff --git a/examples/plain-js/public/logo.png b/examples/plain-js/public/logo.png new file mode 100644 index 0000000000..8f3a5a5dee Binary files /dev/null and b/examples/plain-js/public/logo.png differ diff --git a/examples/plain-js/utils/.keep b/examples/plain-js/utils/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/playwright/.editorconfig b/examples/playwright/.editorconfig new file mode 100644 index 0000000000..09d7a33a4f --- /dev/null +++ b/examples/playwright/.editorconfig @@ -0,0 +1,11 @@ +# https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/examples/playwright/.env b/examples/playwright/.env new file mode 100644 index 0000000000..5cd1aca530 --- /dev/null +++ b/examples/playwright/.env @@ -0,0 +1,3 @@ +# This env file should be checked into source control +# This is the place for default values for all environments +# Values in `.env.local` and `.env.production` will override these values diff --git a/examples/playwright/.env.test b/examples/playwright/.env.test new file mode 100644 index 0000000000..788257052b --- /dev/null +++ b/examples/playwright/.env.test @@ -0,0 +1,5 @@ +# SQLite is ready to go out of the box, but you can switch to Postgres +# by first changing the provider from "sqlite" to "postgres" in the Prisma +# schema file and by second swapping the DATABASE_URL below. +DATABASE_URL="file:./db_test.sqlite" +# DATABASE_URL=postgresql://jonas@localhost:5432/playwright_test \ No newline at end of file diff --git a/examples/playwright/.eslintrc.js b/examples/playwright/.eslintrc.js new file mode 100644 index 0000000000..ceca39456b --- /dev/null +++ b/examples/playwright/.eslintrc.js @@ -0,0 +1 @@ +//module.exports = require("@blitzjs/next/eslint") diff --git a/examples/playwright/.gitignore b/examples/playwright/.gitignore new file mode 100644 index 0000000000..0fd15e974c --- /dev/null +++ b/examples/playwright/.gitignore @@ -0,0 +1,59 @@ +# dependencies +node_modules +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +.pnp.* +.npm +web_modules/ + +# blitz +/.blitz/ +/.next/ +*.sqlite +*.sqlite-journal +.now +.blitz** +blitz-log.log + +# misc +.DS_Store + +# local env files +.env.local +.env.*.local +.envrc + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +.coverage +*.lcov +.nyc_output +lib-cov + +# Caches +*.tsbuildinfo +.eslintcache +.node_repl_history +.yarn-integrity + +# Serverless directories +.serverless/ + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test +/test-results/ +/playwright-report/ +/state.json diff --git a/examples/playwright/.npmrc b/examples/playwright/.npmrc new file mode 100644 index 0000000000..c786194329 --- /dev/null +++ b/examples/playwright/.npmrc @@ -0,0 +1,11 @@ +save-exact=true +legacy-peer-deps=true +strict-peer-dependencies=false +side-effects-cache=false + +public-hoist-pattern[]=@tanstack/react-query +public-hoist-pattern[]=next +public-hoist-pattern[]=secure-password +public-hoist-pattern[]=*jest* +public-hoist-pattern[]=@testing-library/* +# Needed for Blitz to work properly. Don't remove the lines above. diff --git a/examples/playwright/.prettierignore b/examples/playwright/.prettierignore new file mode 100644 index 0000000000..89ef2512f7 --- /dev/null +++ b/examples/playwright/.prettierignore @@ -0,0 +1,10 @@ +.gitkeep +.env* +*.ico +*.lock +db/migrations +.next +.yarn +.pnp.* +node_modules +README.md diff --git a/examples/playwright/README.md b/examples/playwright/README.md new file mode 100644 index 0000000000..a9c16848f9 --- /dev/null +++ b/examples/playwright/README.md @@ -0,0 +1,173 @@ +[![Blitz.js](https://raw.githubusercontent.com/blitz-js/art/master/github-cover-photo.png)](https://blitzjs.com) + +This is a [Blitz.js](https://github.com/blitz-js/blitz) app. + +# ****name**** + +## Getting Started + +Run your app in the development mode. + +``` +blitz dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +## Environment Variables + +Ensure the `.env.local` file has required environment variables: + +``` +DATABASE_URL=postgresql://@localhost:5432/playwright +``` + +Ensure the `.env.test.local` file has required environment variables: + +``` +DATABASE_URL=postgresql://@localhost:5432/playwright_test +``` + +## Tests + +Runs your tests using Jest. + +``` +yarn test +``` + +Blitz comes with a test setup using [Vitest](https://vitest.dev/) and [react-testing-library](https://testing-library.com/). + +## Commands + +Blitz comes with a powerful CLI that is designed to make development easy and fast. You can install it with `npm i -g blitz` + +``` + blitz [COMMAND] + + dev Start a development server + build Create a production build + start Start a production server + export Export your Blitz app as a static application + prisma Run prisma commands + generate Generate new files for your Blitz project + console Run the Blitz console REPL + install Install a recipe + help Display help for blitz + test Run project tests +``` + +You can read more about it on the [CLI Overview](https://blitzjs.com/docs/cli-overview) documentation. + +## What's included? + +Here is the starting structure of your app. + +``` +playwright +├── src/ +│ ├── api/ +│ ├── auth/ +│ │ ├── components/ +│ │ │ ├── LoginForm.tsx +│ │ │ └── SignupForm.tsx +│ │ ├── mutations/ +│ │ │ ├── changePassword.ts +│ │ │ ├── forgotPassword.test.ts +│ │ │ ├── forgotPassword.ts +│ │ │ ├── login.ts +│ │ │ ├── logout.ts +│ │ │ ├── resetPassword.test.ts +│ │ │ ├── resetPassword.ts +│ │ │ └── signup.ts +│ │ ├── pages/ +│ │ │ ├── forgot-password.tsx +│ │ │ ├── login.tsx +│ │ │ ├── reset-password.tsx +│ │ │ └── signup.tsx +│ │ └── validations.ts +│ ├── core/ +│ │ ├── components/ +│ │ │ ├── Form.tsx +│ │ │ └── LabeledTextField.tsx +│ │ └── layouts/ +│ │ └── Layout.tsx +│ ├── pages/ +│ │ ├── _app.tsx +│ │ ├── _document.tsx +│ │ ├── 404.tsx +│ │ ├── index.test.tsx +│ │ └── index.tsx +│ └── users/ +│ ├── hooks/ +│ │ └── useCurrentUser.ts +│ └── queries/ +│ └── getCurrentUser.ts +├── db/ +│ ├── migrations/ +│ ├── index.ts +│ ├── schema.prisma +│ └── seeds.ts +├── integrations/ +├── mailers/ +│ └── forgotPasswordMailer.ts +├── public/ +│ ├── favicon.ico +│ └── logo.png +├── test/ +│ ├── setup.ts +│ └── utils.tsx +├── .eslintrc.js +├── babel.config.js +├── blitz.config.ts +├── vitest.config.ts +├── package.json +├── README.md +├── tsconfig.json +└── types.ts +``` + +These files are: + +- The `src/` folder is a container for most of your project. This is where you’ll put any pages or API routes. + +- `db/` is where your database configuration goes. If you’re writing models or checking migrations, this is where to go. + +- `public/` is a folder where you will put any static assets. If you have images, files, or videos which you want to use in your app, this is where to put them. + +- `integrations/` is a folder to put all third-party integrations like with Stripe, Sentry, etc. + +- `test/` is a folder where you can put test utilities and integration tests. + +- `package.json` contains information about your dependencies and devDependencies. If you’re using a tool like `npm` or `yarn`, you won’t have to worry about this much. + +- `tsconfig.json` is our recommended setup for TypeScript. + +- `.babel.config.js`, `.eslintrc.js`, `.env`, etc. ("dotfiles") are configuration files for various bits of JavaScript tooling. + +- `blitz.config.ts` is for advanced custom configuration of Blitz. [Here you can learn how to use it](https://blitzjs.com/docs/blitz-config). + +- `vitest.config.ts` contains config for Vitest tests. You can [customize it if needed](https://vitejs.dev/config/). + +You can read more about it in the [File Structure](https://blitzjs.com/docs/file-structure) section of the documentation. + +### Tools included + +Blitz comes with a set of tools that corrects and formats your code, facilitating its future maintenance. You can modify their options and even uninstall them. + +- **ESLint**: It lints your code: searches for bad practices and tell you about it. You can customize it via the `.eslintrc.js`, and you can install (or even write) plugins to have it the way you like it. It already comes with the [`blitz`](https://github.com/blitz-js/blitz/tree/canary/packages/eslint-config) config, but you can remove it safely. [Learn More](https://blitzjs.com/docs/eslint-config). +- **Husky**: It adds [githooks](https://git-scm.com/docs/githooks), little pieces of code that get executed when certain Git events are triggerd. For example, `pre-commit` is triggered just before a commit is created. You can see the current hooks inside `.husky/`. If are having problems commiting and pushing, check out ther [troubleshooting](https://typicode.github.io/husky/#/?id=troubleshoot) guide. [Learn More](https://blitzjs.com/docs/husky-config). +- **Prettier**: It formats your code to look the same everywhere. You can configure it via the `.prettierrc` file. The `.prettierignore` contains the files that should be ignored by Prettier; useful when you have large files or when you want to keep a custom formatting. [Learn More](https://blitzjs.com/docs/prettier-config). + +## Learn more + +Read the [Blitz.js Documentation](https://blitzjs.com/docs/getting-started) to learn more. + +The Blitz community is warm, safe, diverse, inclusive, and fun! Feel free to reach out to us in any of our communication channels. + +- [Website](https://blitzjs.com) +- [Discord](https://blitzjs.com/discord) +- [Report an issue](https://github.com/blitz-js/blitz/issues/new/choose) +- [Forum discussions](https://github.com/blitz-js/blitz/discussions) +- [How to Contribute](https://blitzjs.com/docs/contributing) +- [Sponsor or donate](https://github.com/blitz-js/blitz#sponsors-and-donations) diff --git a/examples/playwright/db/index.ts b/examples/playwright/db/index.ts new file mode 100644 index 0000000000..4d7a5a6350 --- /dev/null +++ b/examples/playwright/db/index.ts @@ -0,0 +1,8 @@ +import { enhancePrisma } from "blitz" +import { PrismaClient } from "@prisma/client" + +const EnhancedPrisma = enhancePrisma(PrismaClient) + +export * from "@prisma/client" +const db = new EnhancedPrisma() +export default db diff --git a/examples/playwright/db/migrations/.keep b/examples/playwright/db/migrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/playwright/db/migrations/20230826115710_initial_migration/migration.sql b/examples/playwright/db/migrations/20230826115710_initial_migration/migration.sql new file mode 100644 index 0000000000..b6eb866e1b --- /dev/null +++ b/examples/playwright/db/migrations/20230826115710_initial_migration/migration.sql @@ -0,0 +1,47 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "name" TEXT, + "email" TEXT NOT NULL, + "hashedPassword" TEXT, + "role" TEXT NOT NULL DEFAULT 'USER' +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "expiresAt" DATETIME, + "handle" TEXT NOT NULL, + "hashedSessionToken" TEXT, + "antiCSRFToken" TEXT, + "publicData" TEXT, + "privateData" TEXT, + "userId" INTEGER, + CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Token" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "hashedToken" TEXT NOT NULL, + "type" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + "sentTo" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_handle_key" ON "Session"("handle"); + +-- CreateIndex +CREATE UNIQUE INDEX "Token_hashedToken_type_key" ON "Token"("hashedToken", "type"); diff --git a/examples/playwright/db/migrations/20230826120243_added_book_feature/migration.sql b/examples/playwright/db/migrations/20230826120243_added_book_feature/migration.sql new file mode 100644 index 0000000000..0398110c29 --- /dev/null +++ b/examples/playwright/db/migrations/20230826120243_added_book_feature/migration.sql @@ -0,0 +1,7 @@ +-- CreateTable +CREATE TABLE "Recipe" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "name" TEXT NOT NULL +); diff --git a/examples/playwright/db/migrations/migration_lock.toml b/examples/playwright/db/migrations/migration_lock.toml new file mode 100644 index 0000000000..e5e5c4705a --- /dev/null +++ b/examples/playwright/db/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/examples/playwright/db/schema.prisma b/examples/playwright/db/schema.prisma new file mode 100644 index 0000000000..d71019877a --- /dev/null +++ b/examples/playwright/db/schema.prisma @@ -0,0 +1,72 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +// -------------------------------------- + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String? + email String @unique + hashedPassword String? + role String @default("USER") + + tokens Token[] + sessions Session[] +} + +model Session { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + expiresAt DateTime? + handle String @unique + hashedSessionToken String? + antiCSRFToken String? + publicData String? + privateData String? + + user User? @relation(fields: [userId], references: [id]) + userId Int? +} + +model Token { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + hashedToken String + type String + // See note below about TokenType enum + // type TokenType + expiresAt DateTime + sentTo String + + user User @relation(fields: [userId], references: [id]) + userId Int + + @@unique([hashedToken, type]) +} + +// NOTE: It's highly recommended to use an enum for the token type +// but enums only work in Postgres. +// See: https://blitzjs.com/docs/database-overview#switch-to-postgre-sql +// enum TokenType { +// RESET_PASSWORD +// } + +model Recipe { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String +} diff --git a/examples/playwright/db/seeds.ts b/examples/playwright/db/seeds.ts new file mode 100644 index 0000000000..32377747c7 --- /dev/null +++ b/examples/playwright/db/seeds.ts @@ -0,0 +1,10 @@ +import db from "./index" +import { SecurePassword } from "@blitzjs/auth/secure-password" + +const seed = async () => { + const password = "e2euserpassword" + const hashedPassword = await SecurePassword.hash(password.trim()) + await db.user.create({ data: { email: "e2e@user.de", hashedPassword } }) +} + +export default seed diff --git a/examples/playwright/integrations/.keep b/examples/playwright/integrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/playwright/mailers/.keep b/examples/playwright/mailers/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/playwright/mailers/forgotPasswordMailer.ts b/examples/playwright/mailers/forgotPasswordMailer.ts new file mode 100644 index 0000000000..baace45b9a --- /dev/null +++ b/examples/playwright/mailers/forgotPasswordMailer.ts @@ -0,0 +1,45 @@ +/* TODO - You need to add a mailer integration in `integrations/` and import here. + * + * The integration file can be very simple. Instantiate the email client + * and then export it. That way you can import here and anywhere else + * and use it straight away. + */ + +type ResetPasswordMailer = { + to: string + token: string +} + +export function forgotPasswordMailer({ to, token }: ResetPasswordMailer) { + // In production, set APP_ORIGIN to your production server origin + const origin = process.env.APP_ORIGIN || process.env.BLITZ_DEV_SERVER_ORIGIN + const resetUrl = `${origin}/auth/reset-password?token=${token}` + + const msg = { + from: "TODO@example.com", + to, + subject: "Your Password Reset Instructions", + html: ` +

Reset Your Password

+

NOTE: You must set up a production email integration in mailers/forgotPasswordMailer.ts

+ + + Click here to set a new password + + `, + } + + return { + async send() { + if (process.env.NODE_ENV === "production") { + // TODO - send the production email, like this: + // await postmark.sendEmail(msg) + throw new Error("No production email implementation in mailers/forgotPasswordMailer") + } else { + // Preview email in the browser + const previewEmail = (await import("preview-email")).default + await previewEmail(msg) + } + }, + } +} diff --git a/examples/playwright/next-env.d.ts b/examples/playwright/next-env.d.ts new file mode 100644 index 0000000000..4f11a03dc6 --- /dev/null +++ b/examples/playwright/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/playwright/next.config.js b/examples/playwright/next.config.js new file mode 100644 index 0000000000..27abafa703 --- /dev/null +++ b/examples/playwright/next.config.js @@ -0,0 +1,9 @@ +// @ts-check +const {withBlitz} = require("@blitzjs/next") + +/** + * @type {import('@blitzjs/next').BlitzConfig} + **/ +const config = {} + +module.exports = withBlitz(config) diff --git a/examples/playwright/package-lock.json b/examples/playwright/package-lock.json new file mode 100644 index 0000000000..e1230a1dfb --- /dev/null +++ b/examples/playwright/package-lock.json @@ -0,0 +1,16858 @@ +{ + "name": "playwright", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "playwright", + "version": "1.0.0", + "dependencies": { + "@blitzjs/auth": "2.0.0-beta.32", + "@blitzjs/next": "2.0.0-beta.32", + "@blitzjs/rpc": "2.0.0-beta.32", + "@playwright/test": "1.31.2", + "@prisma/client": "4.6.1", + "blitz": "2.0.0-beta.32", + "eslint-config-react-app": "7.0.1", + "final-form": "4.20.10", + "next": "13.4.5", + "prisma": "4.6.1", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-final-form": "6.5.9", + "secure-password": "4.0.0", + "zod": "3.20.2" + }, + "devDependencies": { + "@next/bundle-analyzer": "12.0.8", + "@testing-library/jest-dom": "5.16.5", + "@testing-library/react": "13.4.0", + "@testing-library/react-hooks": "8.0.1", + "@types/node": "18.11.9", + "@types/preview-email": "2.0.1", + "@types/react": "18.0.25", + "@typescript-eslint/eslint-plugin": "5.42.1", + "@vitejs/plugin-react": "2.2.0", + "eslint": "8.27.0", + "eslint-config-next": "12.3.1", + "eslint-config-prettier": "8.5.0", + "husky": "8.0.2", + "jsdom": "20.0.3", + "lint-staged": "13.0.3", + "prettier": "^2.7.1", + "prettier-plugin-prisma": "4.4.0", + "pretty-quick": "3.1.3", + "preview-email": "3.0.7", + "typescript": "^4.8.4", + "vite-tsconfig-paths": "3.6.0", + "vitest": "0.25.3" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", + "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", + "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "dependencies": { + "@babel/highlight": "^7.22.10", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/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==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/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==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/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==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/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==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.11.tgz", + "integrity": "sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.10", + "@babel/generator": "^7.22.10", + "@babel/helper-compilation-targets": "^7.22.10", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.11", + "@babel/parser": "^7.22.11", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.11", + "@babel/types": "^7.22.11", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.22.11.tgz", + "integrity": "sha512-YjOYZ3j7TjV8OhLW6NCtyg8G04uStATEUe5eiLuCZaXz2VSDQ3dsAtm2D+TuQyAqNMUK2WacGo0/uma9Pein1w==", + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", + "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "dependencies": { + "@babel/types": "^7.22.10", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz", + "integrity": "sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==", + "dependencies": { + "@babel/types": "^7.22.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", + "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz", + "integrity": "sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", + "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", + "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", + "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz", + "integrity": "sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==", + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.11.tgz", + "integrity": "sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==", + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.11", + "@babel/types": "^7.22.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", + "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/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==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/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==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/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==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/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==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.11.tgz", + "integrity": "sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.22.10.tgz", + "integrity": "sha512-KxN6TqZzcFi4uD3UifqXElBTBNLAEH1l3vzMQj6JwJZbL2sZlThxSViOKCYY+4Ah4V4JhQ95IVB7s/Y6SJSlMQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.10", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/plugin-syntax-decorators": "^7.22.10" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.22.10.tgz", + "integrity": "sha512-z1KTVemBjnz+kSEilAsI4lbkPOl5TvJH7YDSY1CTIzvLWJ+KHXp+mRe8VPmfnyvqOPqar1V2gid2PleKzRUstQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", + "integrity": "sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.11.tgz", + "integrity": "sha512-0pAlmeRJn6wU84zzZsEOx1JV1Jf8fqO9ok7wofIJwUnplYo247dcd24P+cMJht7ts9xkzdtB0EPHmOb7F+KzXw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz", + "integrity": "sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", + "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz", + "integrity": "sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.22.5.tgz", + "integrity": "sha512-tujNbZdxdG0/54g/oua8ISToaXTFBf8EnSb5PgQSciIXWOWKX3S4+JR7ZE9ol8FZwf9kxitzkGQ+QWeov/mCiA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-flow": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.11.tgz", + "integrity": "sha512-o2+bg7GDS60cJMgz9jWqRUsWkMzLCxp+jFDeDUT5sjRlAxcJWZ2ylNdI7QQ2+CH5hWu7OnN+Cv3htt7AkSf96g==", + "dependencies": { + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", + "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.11.tgz", + "integrity": "sha512-nX8cPFa6+UmbepISvlf5jhQyaC7ASs/7UxHmMkuJ/k5xSHvDPPaibMo+v3TXwU/Pjqhep/nFNpd3zn4YR59pnw==", + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.10", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.12.tgz", + "integrity": "sha512-7XXCVqZtyFWqjDsYDY4T45w4mlx1rf7aOgkc/Ww76xkgBiOlmjPkx36PBLHa1k1rwWvVgYMPsbuVnIamx2ZQJw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz", + "integrity": "sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz", + "integrity": "sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", + "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", + "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz", + "integrity": "sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.10.tgz", + "integrity": "sha512-RchI7HePu1eu0CYNKHHHQdfenZcM4nz8rew5B1VWqeRKdcwW5aQ5HeG9eTUbWiAS1UrmHVLmoxTWHt3iLD/NhA==", + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz", + "integrity": "sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-typescript": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.10.tgz", + "integrity": "sha512-Gz9hnBT/tGeTE2DBNDkD7BiWRELZt+8lSysHuDwmYXUIvtwZl0zI+D6mZgXZX0u8YBlLS4tmai9ONNY9tjRgRA==", + "dependencies": { + "@babel/compat-data": "^7.12.7", + "@babel/helper-compilation-targets": "^7.12.5", + "@babel/helper-module-imports": "^7.12.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-option": "^7.12.1", + "@babel/plugin-proposal-async-generator-functions": "^7.12.1", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-dynamic-import": "^7.12.1", + "@babel/plugin-proposal-export-namespace-from": "^7.12.1", + "@babel/plugin-proposal-json-strings": "^7.12.1", + "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.7", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", + "@babel/plugin-proposal-private-methods": "^7.12.1", + "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-class-properties": "^7.12.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.12.1", + "@babel/plugin-transform-arrow-functions": "^7.12.1", + "@babel/plugin-transform-async-to-generator": "^7.12.1", + "@babel/plugin-transform-block-scoped-functions": "^7.12.1", + "@babel/plugin-transform-block-scoping": "^7.12.1", + "@babel/plugin-transform-classes": "^7.12.1", + "@babel/plugin-transform-computed-properties": "^7.12.1", + "@babel/plugin-transform-destructuring": "^7.12.1", + "@babel/plugin-transform-dotall-regex": "^7.12.1", + "@babel/plugin-transform-duplicate-keys": "^7.12.1", + "@babel/plugin-transform-exponentiation-operator": "^7.12.1", + "@babel/plugin-transform-for-of": "^7.12.1", + "@babel/plugin-transform-function-name": "^7.12.1", + "@babel/plugin-transform-literals": "^7.12.1", + "@babel/plugin-transform-member-expression-literals": "^7.12.1", + "@babel/plugin-transform-modules-amd": "^7.12.1", + "@babel/plugin-transform-modules-commonjs": "^7.12.1", + "@babel/plugin-transform-modules-systemjs": "^7.12.1", + "@babel/plugin-transform-modules-umd": "^7.12.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.1", + "@babel/plugin-transform-new-target": "^7.12.1", + "@babel/plugin-transform-object-super": "^7.12.1", + "@babel/plugin-transform-parameters": "^7.12.1", + "@babel/plugin-transform-property-literals": "^7.12.1", + "@babel/plugin-transform-regenerator": "^7.12.1", + "@babel/plugin-transform-reserved-words": "^7.12.1", + "@babel/plugin-transform-shorthand-properties": "^7.12.1", + "@babel/plugin-transform-spread": "^7.12.1", + "@babel/plugin-transform-sticky-regex": "^7.12.7", + "@babel/plugin-transform-template-literals": "^7.12.1", + "@babel/plugin-transform-typeof-symbol": "^7.12.10", + "@babel/plugin-transform-unicode-escapes": "^7.12.1", + "@babel/plugin-transform-unicode-regex": "^7.12.1", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.12.10", + "core-js-compat": "^3.8.0", + "semver": "^5.5.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.22.5.tgz", + "integrity": "sha512-ta2qZ+LSiGCrP5pgcGt8xMnnkXQrq8Sa4Ulhy06BOlF5QbLw9q5hIx7bn5MrsvyTGAfh6kTOo07Q+Pfld/8Y5Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-transform-flow-strip-types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.5.tgz", + "integrity": "sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-transform-react-display-name": "^7.22.5", + "@babel/plugin-transform-react-jsx": "^7.22.5", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.11.tgz", + "integrity": "sha512-tWY5wyCZYBGY7IlalfKI1rLiGlIfnwsRHZqlky0HVv8qviwQ1Uo/05M6+s+TcTCVa6Bmoo2uJW5TMFX6Wa4qVg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.11", + "@babel/plugin-transform-typescript": "^7.22.11" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript/node_modules/@babel/plugin-transform-typescript": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.11.tgz", + "integrity": "sha512-0E4/L+7gfvHub7wsbTv03oRtD69X31LByy44fGmFzbZScpupFByMcgCJ0VbBTkzyjSJKuRoGN8tcijOWKTmqOA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-typescript": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.5.tgz", + "integrity": "sha512-vV6pm/4CijSQ8Y47RH5SopXzursN35RQINfGJkmOlcpAtGuf94miFvIPhCKGQN7WGIcsgG1BHEX2KVdTYwTwUQ==", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.5", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + }, + "node_modules/@babel/runtime": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.11.tgz", + "integrity": "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz", + "integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==", + "dependencies": { + "@babel/code-frame": "^7.22.10", + "@babel/generator": "^7.22.10", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.11", + "@babel/types": "^7.22.11", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", + "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@blitzjs/auth": { + "version": "2.0.0-beta.32", + "resolved": "https://registry.npmjs.org/@blitzjs/auth/-/auth-2.0.0-beta.32.tgz", + "integrity": "sha512-1GdcqqFrmWyh2LaFuXxmrM4FDWkwvLxICCm0Bbf0qXb8v0tke48m1/1/NVJdR7t8wSQmSSQxTYZAqncZA8WEQg==", + "dependencies": { + "@types/b64-lite": "1.3.0", + "@types/cookie-session": "2.0.44", + "@types/oauth": "0.9.1", + "@types/passport": "1.0.7", + "@types/secure-password": "3.1.1", + "b64-lite": "1.4.0", + "bad-behavior": "1.0.1", + "cookie": "0.4.1", + "cookie-session": "2.0.0", + "debug": "4.3.3", + "find-up": "4.1.0", + "http": "0.0.1-security", + "jsonwebtoken": "9.0.0", + "nanoid": "3.2.0", + "oauth": "0.10.0", + "openid-client": "5.2.1", + "passport": "0.6.0", + "path": "0.12.7", + "resolve-from": "5.0.0", + "supports-color": "8.1.1", + "url": "0.11.0" + }, + "peerDependencies": { + "blitz": "2.0.0-beta.32", + "next": "*", + "next-auth": "*", + "secure-password": "4.0.0" + }, + "peerDependenciesMeta": { + "next": { + "optional": true + }, + "next-auth": { + "optional": true + }, + "secure-password": { + "optional": true + } + } + }, + "node_modules/@blitzjs/generator": { + "version": "2.0.0-beta.32", + "resolved": "https://registry.npmjs.org/@blitzjs/generator/-/generator-2.0.0-beta.32.tgz", + "integrity": "sha512-RoftNTkcntOMcFb983MiCbxO4lfx1smI+2O12UL++Onnneot5RkxZD8/t03gGMLr6svRX9iqJSkhU6loKWAZsw==", + "dependencies": { + "@babel/core": "7.12.10", + "@babel/plugin-transform-typescript": "7.12.1", + "@babel/preset-env": "7.12.10", + "@babel/types": "7.12.10", + "@mrleebo/prisma-ast": "0.4.1", + "chalk": "^4.1.0", + "console-table-printer": "2.10.0", + "cross-spawn": "7.0.3", + "diff": "5.0.0", + "enquirer": "2.3.6", + "fast-glob": "3.2.12", + "fs-extra": "10.0.1", + "globby": "13.1.2", + "got": "^11.8.1", + "jscodeshift": "0.13.0", + "mem-fs": "1.2.0", + "mem-fs-editor": "8.0.0", + "npm-which": "3.0.1", + "ora": "5.3.0", + "pluralize": "8.0.0", + "prettier": "^2.7.1", + "recast": "0.20.5", + "supports-color": "8.1.1", + "tslog": "4.9.0", + "username": "5.1.0", + "vinyl": "2.2.1", + "zod": "3.20.2" + } + }, + "node_modules/@blitzjs/generator/node_modules/@babel/core": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz", + "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@blitzjs/generator/node_modules/@babel/types": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.10.tgz", + "integrity": "sha512-sf6wboJV5mGyip2hIpDSKsr80RszPinEFjsHTalMxZAZkoQ2/2yQzxlcFN52SJqsyPfLtPmenL4g2KB3KJXPDw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@blitzjs/generator/node_modules/@mrleebo/prisma-ast": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.4.1.tgz", + "integrity": "sha512-vk1JBunTocQ8xaVqjgVnDVZ63d8Vm/HpOrFXATsKZI5ufC6ZG7FmRtWwyljLXHzW9C/pLp3kJAKc24oSPOYXgw==", + "dependencies": { + "chevrotain": "^9.0.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@blitzjs/generator/node_modules/globby": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz", + "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@blitzjs/generator/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@blitzjs/generator/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@blitzjs/generator/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@blitzjs/next": { + "version": "2.0.0-beta.32", + "resolved": "https://registry.npmjs.org/@blitzjs/next/-/next-2.0.0-beta.32.tgz", + "integrity": "sha512-rSIX4J+PHGhTMea8Bu2Xd6yGkqtjYoO15DQAEHkidgBL5fMA7F0ZlHDw5KkRMGPMuAoh7F4wYy4xvzVOOzXxBQ==", + "hasInstallScript": true, + "dependencies": { + "@blitzjs/rpc": "2.0.0-beta.32", + "@types/hoist-non-react-statics": "3.3.1", + "debug": "4.3.3", + "fs-extra": "10.0.1", + "hoist-non-react-statics": "3.3.2", + "superjson": "1.11.0", + "supports-color": "8.1.1" + }, + "peerDependencies": { + "blitz": "2.0.0-beta.32", + "next": "*", + "react": "*", + "tslog": "*" + } + }, + "node_modules/@blitzjs/rpc": { + "version": "2.0.0-beta.32", + "resolved": "https://registry.npmjs.org/@blitzjs/rpc/-/rpc-2.0.0-beta.32.tgz", + "integrity": "sha512-NPO80BNOWR+B5UUT6xitvpJ7z11gVvL2bEAGRme7deOGeBLHmXWwvGZ+LwPcJkUNXOKUxg4DgKGlWARxF0+h7g==", + "dependencies": { + "@swc/core": "1.3.7", + "@tanstack/react-query": "4.24.4", + "b64-lite": "1.4.0", + "bad-behavior": "1.0.1", + "chalk": "^4.1.0", + "debug": "4.3.3", + "superjson": "1.11.0", + "supports-color": "8.1.1" + }, + "peerDependencies": { + "@tanstack/query-core": "4.24.4", + "blitz": "2.0.0-beta.32", + "next": "*", + "react": "*" + } + }, + "node_modules/@chevrotain/types": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-9.1.0.tgz", + "integrity": "sha512-3hbCD1CThkv9gnaSIPq0GUXwKni68e0ph6jIHwCvcWiQ4JB2xi8bFxBain0RF04qHUWuDjgnZLj4rLgimuGO+g==" + }, + "node_modules/@chevrotain/utils": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-9.1.0.tgz", + "integrity": "sha512-llLJZ8OAlZrjGlBvamm6Zdo/HmGAcCLq5gx7cSwUX8No+n/8ip+oaC4x33IdZIif8+Rh5dQUIZXmfbSghiOmNQ==" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@cush/relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@cush/relative/-/relative-1.0.0.tgz", + "integrity": "sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==", + "dev": true + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", + "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jest/expect-utils": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.4.tgz", + "integrity": "sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.2.6.tgz", + "integrity": "sha512-Df0gAGmws3sxNMmLviTarBL9znf84QVlVhlx4EPgArrnaVBy8tNQZAI9aSTJHzH0JGj9BVGa0Qz1g3hPt12Kxw==", + "dependencies": { + "chevrotain": "^9.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@next/bundle-analyzer": { + "version": "12.0.8", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-12.0.8.tgz", + "integrity": "sha512-tRwFyAkJA0h+rwt4exq31T59qo4qwp7vPoR3yC8gIpK/E5NAwafyk40aNpk4OWhiQ2IvJMFutukMzY3xl79NXA==", + "dev": true, + "dependencies": { + "webpack-bundle-analyzer": "4.3.0" + } + }, + "node_modules/@next/env": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.5.tgz", + "integrity": "sha512-SG/gKH6eij4vwQy87b/3mbpQ1X3x2vUdnpwq6/qL2IQWjtq58EY/UuNAp9CoEZoC9sI4L9AD1r+73Z9r4d3uug==" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.1.tgz", + "integrity": "sha512-sw+lTf6r6P0j+g/n9y4qdWWI2syPqZx+uc0+B/fRENqfR3KpSid6MIKqc9gNwGhJASazEQ5b3w8h4cAET213jw==", + "dev": true, + "dependencies": { + "glob": "7.1.7" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.5.tgz", + "integrity": "sha512-XvTzi2ASUN5bECFIAAcBiSoDb0xsq+KLj4F0bof4d4rdc+FgOqLvseGQaOXwVi1TIh5bHa7o4b6droSJMO5+2g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.5.tgz", + "integrity": "sha512-NQdqal/VKAqlJTuzhjZmNtdo8QSqwmfO7b2xJSAengTEVxQvsH76oGEzQeIv8Ci4NP6DysAFtFrJq++TmIxcUA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.5.tgz", + "integrity": "sha512-nB8TjtpJCXtzIFjYOMbnQu68ajkA8QK58TreHjTGojSQjsF0StDqo5zFHglVVVHrd8d3N/+EjC18yFNSWnd/ZA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.5.tgz", + "integrity": "sha512-W126XUW599OV3giSH9Co40VpT8VAOT47xONVHXZaYEpeca0qEevjj6WUr5IJu/8u+XGWm5xI1S0DYWjR6W+olw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.5.tgz", + "integrity": "sha512-ZbPLO/oztQdtjGmWvGhRmtkZ6j9kQqg65kiO7F7Ijj7ojTtu3hh/vY+XRsHa/4Cse6HgyJ8XGZJMGoLb8ecQfQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.5.tgz", + "integrity": "sha512-f+/h8KMNixVUoRB+2vza8I+jsthJ4KcvopGUsDIUHe7Q4t+m8nKwGFBeyNu9qNIenYK5g5QYEsSwYFEqZylrTQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.5.tgz", + "integrity": "sha512-dvtPQZ5+J+zUE1uq7gP853Oj63e+n0T1ydZ/yRdVh7d8zW9ZFuC9fFrg3MqP1cv1NPPur8rrTqDKN2mRBkSSBw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.5.tgz", + "integrity": "sha512-gK9zwGe25x31S4AjPy3Bf2niQvHIAbmwgkzmqWG3OmD4K2Z/Dh2ju4vuyzPzIt0pwQe4B520meP9NizTBmVWSg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.5.tgz", + "integrity": "sha512-iyNQVc7eGehrik9RJt9xGcnO6b/pi8C7GCfg8RGenx1IlalEKbYRgBJloF7DQzwlrV47E9bQl8swT+JawaNcKA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.31.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.31.2.tgz", + "integrity": "sha512-BYVutxDI4JeZKV1+ups6dt5WiqKhjBtIYowyZIJ3kBDmJgsuPKsqqKNIMFbUePLSCmp2cZu+BDL427RcNKTRYw==", + "dependencies": { + "@types/node": "*", + "playwright-core": "1.31.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "node_modules/@prisma/client": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.6.1.tgz", + "integrity": "sha512-M1+NNrMzqaOIxT7PBGcTs3IZo7d1EW/+gVQd4C4gUgWBDGgD9AcIeZnUSidgWClmpMSgVUdnVORjsWWGUameYA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines-version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/engines": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.6.1.tgz", + "integrity": "sha512-3u2/XxvxB+Q7cMXHnKU0CpBiUK1QWqpgiBv28YDo1zOIJE3FCF8DI2vrp6vuwjGt5h0JGXDSvmSf4D4maVjJdw==", + "hasInstallScript": true + }, + "node_modules/@prisma/engines-version": { + "version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32.tgz", + "integrity": "sha512-HUCmkXAU2jqp2O1RvNtbE+seLGLyJGEABZS/R38rZjSAafAy0WzBuHq+tbZMnD+b5OSCsTVtIPVcuvx1ySxcWQ==" + }, + "node_modules/@prisma/prisma-fmt-wasm": { + "version": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6", + "resolved": "https://registry.npmjs.org/@prisma/prisma-fmt-wasm/-/prisma-fmt-wasm-4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6.tgz", + "integrity": "sha512-Hc2i5nfAt3nLDUkQNWJcKFJaA9Avd5zz6t85w9SW7P0vGtFXScQ+xIu6znbULr9bc0pgTWejY1We2u/7EMxHWw==", + "dev": true + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz", + "integrity": "sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==" + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@swc/core": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.7.tgz", + "integrity": "sha512-g4ptYRZRE+g/6wLB3WBuWhAWJsZDUeiSOvKVM1Wdn29Vi/EgLuVaY5ssz0HLQJxuDSJGwtAOZA8exh4+AKNHLw==", + "hasInstallScript": true, + "bin": { + "swcx": "run_swcx.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-android-arm-eabi": "1.3.7", + "@swc/core-android-arm64": "1.3.7", + "@swc/core-darwin-arm64": "1.3.7", + "@swc/core-darwin-x64": "1.3.7", + "@swc/core-freebsd-x64": "1.3.7", + "@swc/core-linux-arm-gnueabihf": "1.3.7", + "@swc/core-linux-arm64-gnu": "1.3.7", + "@swc/core-linux-arm64-musl": "1.3.7", + "@swc/core-linux-x64-gnu": "1.3.7", + "@swc/core-linux-x64-musl": "1.3.7", + "@swc/core-win32-arm64-msvc": "1.3.7", + "@swc/core-win32-ia32-msvc": "1.3.7", + "@swc/core-win32-x64-msvc": "1.3.7" + } + }, + "node_modules/@swc/core-android-arm-eabi": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.3.7.tgz", + "integrity": "sha512-zvUpTBOUnXDkfp2JXv1T3NfyimxsAnqEfT65gWC/3ZpB/gmc59vqYVko4Pifyvuxo5aVvEdT2gfHlWM/aXwtpg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "dependencies": { + "@swc/wasm": "1.2.122" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-android-arm64": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.3.7.tgz", + "integrity": "sha512-qnh1aYTrIjuFOkgxUYG8SGzpPD92o/w5hrHUy71LfUbHf5HRs7FpMgQXtTGnk33S/uMCvSv7V/ewv+t+N6tlVA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-android-arm64/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "optional": true + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.7.tgz", + "integrity": "sha512-q8NgUK/CleCmGYIuskL1sCad8opkfJD/8GWd+MkGSi+MGkExrLMmJftgG5FCj0l/xCHxGGNYj1TCrM/qV6CheA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.7.tgz", + "integrity": "sha512-dKrJkZYbF7Qi1wQgyVnR1a5Vk8UN7fJ/WlK6pZVJwMvWLoZgYE+U0Nn7RsVB4LmOxHtaJF7eesbGUm2y2NVEwA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-freebsd-x64": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-freebsd-x64/-/core-freebsd-x64-1.3.7.tgz", + "integrity": "sha512-ENHthc4iFPlBj0xaf2DbJLDzYSBA4QMQEA2HhZoSWWMsqhg8mGZxwgRd6+loROGZ2a5HKMZXIxCev8BbYnE0OA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-freebsd-x64/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "optional": true + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.7.tgz", + "integrity": "sha512-anE65tcRLr/fYayXkpwZ7p7Ft5HCH4rvi3wSFdK8ycRWn9fVZhyWUJkJ3p1S0R19xr7hcb14hyxqPbd4m0I4yA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "optional": true + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.7.tgz", + "integrity": "sha512-Qv6f76Tt8t51qb29R2isWvuQM26Xi7ZJavAv0hMdCxfkF+h1Yd14j82H7afGzdONH1LyLaPrhWSQirU/ZtBtdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.7.tgz", + "integrity": "sha512-paYbmvm7+7QxjyMzRd4X4tyhHw5VgkGCMBYC3PbfpuI7SsCdmEFG9v1t5uMbTf60VU1wB4/n+AxY9KCZLfK7DQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.7.tgz", + "integrity": "sha512-tkIHt64mmqEVM0CTGvUsB37Pv7AD/BinOEe6oPfMcS/2a00kYvXn9kEVKPqNTpiFpjYGoFQJaVV8UsD+iv8IvQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.7.tgz", + "integrity": "sha512-V0xeTS8kvnTlghO1YyO1QgfPqsY896MknYCzBeK9CGKkGbc3JaxSoyb11nbGEDEaUwzDd9gj9L4D2uP+IWpoyw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.7.tgz", + "integrity": "sha512-LeauQIok8tw4Mjmj7wlc7C62HCUx3xa5k6tNQnKWbDs7odZVWisgDxn7RSl9/xxlC8wPLTVUyBh3O1rHigVfWg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "optional": true + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.7.tgz", + "integrity": "sha512-E1C8bpUrml0vIv4FTSP7f4CwkZVGsCY9fBsBHCC4j9N1mtQk8/nzpGOUsPo4QP+FTYJiNKedZ4Cy7baihnV4Lw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "optional": true + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.7.tgz", + "integrity": "sha512-Ti9H/1hqBrxhYtNLVaLsahO/iiJn1Zd4qSc0LZpl6wBJxP4LltLV4MLeib6i8lg11pj4ijIhzZfC6bT614ee3w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", + "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@swc/wasm": { + "version": "1.2.122", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.122.tgz", + "integrity": "sha512-sM1VCWQxmNhFtdxME+8UXNyPNhxNu7zdb6ikWpz0YKAQQFRGT5ThZgJrubEpah335SUToNg8pkdDF7ibVCjxbQ==", + "optional": true + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.4.tgz", + "integrity": "sha512-9dqjv9eeB6VHN7lD3cLo16ZAjfjCsdXetSAD5+VyKqLUvcKTL0CklGQRJu+bWzdrS69R6Ea4UZo8obHYZnG6aA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.24.4.tgz", + "integrity": "sha512-RpaS/3T/a3pHuZJbIAzAYRu+1nkp+/enr9hfRXDS/mojwx567UiMksoqW4wUFWlwIvWTXyhot2nbIipTKEg55Q==", + "dependencies": { + "@tanstack/query-core": "4.24.4", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@testing-library/dom": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", + "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/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/@testing-library/react": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", + "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.5.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@testing-library/react-hooks": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", + "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "react-error-boundary": "^3.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0", + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0", + "react-test-renderer": "^16.9.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-test-renderer": { + "optional": true + } + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/aria-query": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", + "dev": true + }, + "node_modules/@types/b64-lite": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/b64-lite/-/b64-lite-1.3.0.tgz", + "integrity": "sha512-xrTKDOdOCLtdWAn+XnFWVoVNdump98dtZEpzQcw+BCtMdrGNVdbg6i6D1b9IU7HWzQm3ypi7hoLuXhrLiJn3bw==" + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-session": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/@types/cookie-session/-/cookie-session-2.0.44.tgz", + "integrity": "sha512-3DheOZ41pql6raSIkqEPphJdhA2dX2bkS+s2Qacv8YMKkoCbAIEXbsDil7351ARzMqvfyDUGNeHGiRZveIzhqQ==", + "dependencies": { + "@types/express": "*", + "@types/keygrip": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.36", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.36.tgz", + "integrity": "sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/global-agent": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/global-agent/-/global-agent-2.1.1.tgz", + "integrity": "sha512-sVox8Phk1UKgP6LQPAdeRxfww6vHKt7Bf59dXzYLsQBUEMEn8S10a+ESp/yO0i4fJ3WS4+CIuz42hgJcuA+3mA==" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, + "node_modules/@types/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", + "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + }, + "node_modules/@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==" + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + }, + "node_modules/@types/nodemailer": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz", + "integrity": "sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/oauth": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz", + "integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "node_modules/@types/passport": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz", + "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/preview-email": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/preview-email/-/preview-email-2.0.1.tgz", + "integrity": "sha512-wHtm/Xxlxk2WKRok0ya7iUr+UvQFlq9nTIQXZi2d3n2tdrt+n4Fkybvi45BjQILl2wfGFq4y+jARCo2+ZoaTrA==", + "dev": true, + "dependencies": { + "@types/nodemailer": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/react": { + "version": "18.0.25", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz", + "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", + "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, + "node_modules/@types/secure-password": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/secure-password/-/secure-password-3.1.1.tgz", + "integrity": "sha512-8S6JYBt22MXJrc/ehq8U0jknbnCzssVFKwOqn2pLnzbZLtiotjxaCd+SRQtOcVqrn1NNiKyw6fZJagW1eBe1bQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==" + }, + "node_modules/@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/testing-library__jest-dom": { + "version": "5.14.9", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", + "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", + "dev": true, + "dependencies": { + "@types/jest": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@types/yoga-layout": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", + "integrity": "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.42.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.1.tgz", + "integrity": "sha512-LyR6x784JCiJ1j6sH5Y0K6cdExqCCm8DJUTcwG5ThNXJj/G8o5E56u5EdG4SLy+bZAwZBswC+GYn3eGdttBVCg==", + "dependencies": { + "@typescript-eslint/scope-manager": "5.42.1", + "@typescript-eslint/type-utils": "5.42.1", + "@typescript-eslint/utils": "5.42.1", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", + "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", + "dependencies": { + "@typescript-eslint/utils": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.42.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.1.tgz", + "integrity": "sha512-QAZY/CBP1Emx4rzxurgqj3rUinfsh/6mvuKbLNMfJMMKYLRBfweus8brgXF8f64ABkIZ3zdj2/rYYtF8eiuksQ==", + "dependencies": { + "@typescript-eslint/types": "5.42.1", + "@typescript-eslint/visitor-keys": "5.42.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.42.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.1.tgz", + "integrity": "sha512-WWiMChneex5w4xPIX56SSnQQo0tEOy5ZV2dqmj8Z371LJ0E+aymWD25JQ/l4FOuuX+Q49A7pzh/CGIQflxMVXg==", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.42.1", + "@typescript-eslint/utils": "5.42.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.42.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.1.tgz", + "integrity": "sha512-Qrco9dsFF5lhalz+lLFtxs3ui1/YfC6NdXu+RAGBa8uSfn01cjO7ssCsjIsUs484vny9Xm699FSKwpkCcqwWwA==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.42.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.1.tgz", + "integrity": "sha512-qElc0bDOuO0B8wDhhW4mYVgi/LZL+igPwXtV87n69/kYC/7NG3MES0jHxJNCr4EP7kY1XVsRy8C/u3DYeTKQmw==", + "dependencies": { + "@typescript-eslint/types": "5.42.1", + "@typescript-eslint/visitor-keys": "5.42.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.42.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.1.tgz", + "integrity": "sha512-Gxvf12xSp3iYZd/fLqiQRD4uKZjDNR01bQ+j8zvhPjpsZ4HmvEFL/tC4amGNyxN9Rq+iqvpHLhlqx6KTxz9ZyQ==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.42.1", + "@typescript-eslint/types": "5.42.1", + "@typescript-eslint/typescript-estree": "5.42.1", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.42.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.1.tgz", + "integrity": "sha512-LOQtSF4z+hejmpUvitPlc4hA7ERGoj2BVkesOcG91HCn8edLGUXbTrErmutmPbl8Bo9HjAvOO/zBKQHExXNA2A==", + "dependencies": { + "@typescript-eslint/types": "5.42.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-2.2.0.tgz", + "integrity": "sha512-FFpefhvExd1toVRlokZgxgy2JtnBOdp4ZDsq7ldCWaqGSGn9UhWMAVm/1lxPL14JfNS5yGz+s9yFrQY6shoStA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.19.6", + "@babel/plugin-transform-react-jsx": "^7.19.0", + "@babel/plugin-transform-react-jsx-development": "^7.18.6", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.26.7", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^3.0.0" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", + "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", + "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", + "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/assert-never": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz", + "integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ast-types": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", + "integrity": "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==" + }, + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dependencies": { + "has-symbols": "^1.0.3" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/auto-bind": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", + "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", + "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/b64-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/b64-lite/-/b64-lite-1.4.0.tgz", + "integrity": "sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w==", + "dependencies": { + "base-64": "^0.1.0" + } + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" + }, + "node_modules/babel-preset-react-app": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", + "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-decorators": "^7.16.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-transform-flow-strip-types": "^7.16.0", + "@babel/plugin-transform-react-display-name": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.4", + "@babel/preset-env": "^7.16.4", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", + "@babel/runtime": "^7.16.3", + "babel-plugin-macros": "^3.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/preset-env": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.10.tgz", + "integrity": "sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A==", + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.10", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.10", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.10", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.6", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.10", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.10", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.22.10", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-react-app/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bad-behavior": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bad-behavior/-/bad-behavior-1.0.1.tgz", + "integrity": "sha512-jWOTPr1R2zhobzDJUJNzAiJ33Uk3DzZ8UqbetUkVHuDrd4T8pUNKyU5ygreeV7XHTN61yFV3qv6GIZnBzjfWcg==", + "engines": { + "node": ">=6.0.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==" + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blitz": { + "version": "2.0.0-beta.32", + "resolved": "https://registry.npmjs.org/blitz/-/blitz-2.0.0-beta.32.tgz", + "integrity": "sha512-p83Y2igIYrwSyxxkLB5zk6W/MNjN9uYMv3KyQ2K/k5YSiNsnpno0l0uDtWDVxxw0jwHQ58GTfFsm9rLnSc13xA==", + "dependencies": { + "@blitzjs/generator": "2.0.0-beta.32", + "@mrleebo/prisma-ast": "0.2.6", + "@types/global-agent": "2.1.1", + "arg": "5.0.1", + "ast-types": "0.14.2", + "boxen": "7.0.0", + "chalk": "^4.1.0", + "chokidar": "3.5.3", + "console-table-printer": "2.10.0", + "cross-spawn": "7.0.3", + "debug": "4.3.3", + "detect-port": "1.3.0", + "diff": "5.0.0", + "dotenv": "16.0.0", + "dotenv-expand": "8.0.3", + "envinfo": "7.8.1", + "esbuild": "0.14.34", + "esbuild-register": "3.3.3", + "find-up": "4.1.0", + "findup-sync": "5.0.0", + "fs-extra": "10.0.1", + "global-agent": "3.0.0", + "globby": "13.1.2", + "got": "^11.8.1", + "hasbin": "1.2.3", + "ink": "3.2.0", + "ink-spinner": "4.0.3", + "jscodeshift": "0.13.0", + "node-fetch": "3.2.3", + "npm-which": "3.0.1", + "ora": "5.3.0", + "os-name": "5.0.1", + "p-event": "4.2.0", + "pkg-dir": "5.0.0", + "progress": "2.0.3", + "prompts": "2.4.2", + "recast": "0.20.5", + "resolve-cwd": "3.0.0", + "resolve-from": "5.0.0", + "rimraf": "3.0.2", + "semver": "7.3.8", + "superjson": "1.11.0", + "supports-color": "8.1.1", + "tar": "6.1.11", + "ts-node": "10.9.1", + "tsconfig-paths": "4.0.0", + "tslog": "4.9.0", + "watchpack": "2.1.1" + }, + "bin": { + "blitz": "bin/blitz" + } + }, + "node_modules/blitz/node_modules/globby": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz", + "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blitz/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/blitz/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/blitz/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blitz/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==" + }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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==", + "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==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001523", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001523.tgz", + "integrity": "sha512-I5q5cisATTPZ1mc588Z//pj/Ox80ERYDfR71YnvY7raS/NOk8xXlZcB0sF7JdqaV//kOaa6aus7lRfpdnt1eBA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", + "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/chalk/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==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "dev": true, + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chevrotain": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-9.1.0.tgz", + "integrity": "sha512-A86/55so63HCfu0dgGg3j9u8uuuBOrSqly1OhBZxRu2x6sAKILLzfVjbGMw45kgier6lz45EzcjjWtTRgoT84Q==", + "dependencies": { + "@chevrotain/types": "^9.1.0", + "@chevrotain/utils": "^9.1.0", + "regexp-to-ast": "0.5.0" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==" + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "node_modules/cloneable-readable/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/cloneable-readable/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/cloneable-readable/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/cloneable-readable/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/code-excerpt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-3.0.0.tgz", + "integrity": "sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw==", + "dependencies": { + "convert-to-spaces": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "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==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" + }, + "node_modules/console-table-printer": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.10.0.tgz", + "integrity": "sha512-7pTsysaJs1+R+OO4cCtJbl+Lr4piHYIhi7/V1qHbOg/uiYgq2yUINFgvXZtVHqm9qpW0+Uk190qkGcKvzdunvg==", + "dependencies": { + "simple-wcswidth": "^1.0.1" + } + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/convert-to-spaces": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz", + "integrity": "sha512-cj09EBuObp9gZNQCzc7hByQyrs6jVGE+o9kSJmeUoj+GiPiJvi5LYqEH/Hmme4+MTLHM+Ejtq+FChpjjEnsPdQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-session": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.0.0.tgz", + "integrity": "sha512-hKvgoThbw00zQOleSlUr2qpvuNweoqBtxrmx0UFosx6AGi9lYtLoA+RbsvknrEX8Pr6MDbdWAb2j6SnMn+lPsg==", + "dependencies": { + "cookies": "0.8.0", + "debug": "3.2.7", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cookie-session/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.1.tgz", + "integrity": "sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==", + "dependencies": { + "browserslist": "^4.21.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.1.tgz", + "integrity": "sha512-5j88ECEn6h17UePrLi6pn1JcLtAiANa3KExyr9y9Z5vo2mv56Gh3I4Aja/B9P9uyMwyxNHAHWv+nE72f30T5Dg==", + "dev": true, + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-equal": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", + "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.1", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, + "node_modules/detect-port": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz", + "integrity": "sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ==", + "dependencies": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "bin": { + "detect": "bin/detect-port", + "detect-port": "bin/detect-port" + }, + "engines": { + "node": ">= 4.2.1" + } + }, + "node_modules/detect-port/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/detect-port/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/display-notification": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/display-notification/-/display-notification-2.0.0.tgz", + "integrity": "sha512-TdmtlAcdqy1NU+j7zlkDdMnCL878zriLaBmoD9quOoq1ySSSGv03l0hXK5CvIFZlIfFI/hizqdQuW+Num7xuhw==", + "dev": true, + "dependencies": { + "escape-string-applescript": "^1.0.0", + "run-applescript": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "dev": true + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", + "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/dotenv-expand": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", + "integrity": "sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.503", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.503.tgz", + "integrity": "sha512-LF2IQit4B0VrUHFeQkWhZm97KuJSGF2WJqq1InpY+ECpFRkXd8yTIaTtJxsO0OKDmiBYwWqcrNaXOurn2T2wiA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/encoding-japanese": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.0.0.tgz", + "integrity": "sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==", + "dev": true, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.1", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.13.tgz", + "integrity": "sha512-LK3VGwzvaPWobO8xzXXGRUOGw8Dcjyfk62CsY/wfHN75CwsJPbuypOYJxK6g5RyEL8YDjIWcl6jgd8foO6mmrA==", + "dependencies": { + "asynciterator.prototype": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.21.3", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.0", + "safe-array-concat": "^1.0.0" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, + "node_modules/esbuild": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.34.tgz", + "integrity": "sha512-QIWdPT/gFF6hCaf4m7kP0cJ+JIuFkdHibI7vVFvu3eJS1HpVmYHWDulyN5WXwbRA0SX/7ZDaJ/1DH8SdY9xOJg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-64": "0.14.34", + "esbuild-android-arm64": "0.14.34", + "esbuild-darwin-64": "0.14.34", + "esbuild-darwin-arm64": "0.14.34", + "esbuild-freebsd-64": "0.14.34", + "esbuild-freebsd-arm64": "0.14.34", + "esbuild-linux-32": "0.14.34", + "esbuild-linux-64": "0.14.34", + "esbuild-linux-arm": "0.14.34", + "esbuild-linux-arm64": "0.14.34", + "esbuild-linux-mips64le": "0.14.34", + "esbuild-linux-ppc64le": "0.14.34", + "esbuild-linux-riscv64": "0.14.34", + "esbuild-linux-s390x": "0.14.34", + "esbuild-netbsd-64": "0.14.34", + "esbuild-openbsd-64": "0.14.34", + "esbuild-sunos-64": "0.14.34", + "esbuild-windows-32": "0.14.34", + "esbuild-windows-64": "0.14.34", + "esbuild-windows-arm64": "0.14.34" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.34.tgz", + "integrity": "sha512-XfxcfJqmMYsT/LXqrptzFxmaR3GWzXHDLdFNIhm6S00zPaQF1TBBWm+9t0RZ6LRR7iwH57DPjaOeW20vMqI4Yw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.34.tgz", + "integrity": "sha512-T02+NXTmSRL1Mc6puz+R9CB54rSPICkXKq6+tw8B6vxZFnCPzbJxgwIX4kcluz9p8nYBjF3+lSilTGWb7+Xgew==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.34.tgz", + "integrity": "sha512-pLRip2Bh4Ng7Bf6AMgCrSp3pPe/qZyf11h5Qo2mOfJqLWzSVjxrXW+CFRJfrOVP7TCnh/gmZSM2AFdCPB72vtw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.34.tgz", + "integrity": "sha512-vpidSJEBxx6lf1NWgXC+DCmGqesJuZ5Y8aQVVsaoO4i8tRXbXb0whChRvop/zd3nfNM4dIl5EXAky0knRX5I6w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.34.tgz", + "integrity": "sha512-m0HBjePhe0hAQJgtMRMNV9kMgIyV4/qSnzPx42kRMQBcPhgjAq1JRu4Il26czC+9FgpMbFkUktb07f/Lwnc6CA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.34.tgz", + "integrity": "sha512-cpRc2B94L1KvMPPYB4D6G39jLqpKlD3noAMY4/e86iXXXkhUYJJEtTuyNFTa9JRpWM0xCAp4mxjHjoIiLuoCLA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.34.tgz", + "integrity": "sha512-8nQaEaoW7MH/K/RlozJa+lE1ejHIr8fuPIHhc513UebRav7HtXgQvxHQ6VZRUkWtep23M6dd7UqhwO1tMOfzQQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.34.tgz", + "integrity": "sha512-Y3of4qQoLLlAgf042MlrY1P+7PnN9zWj8nVtw9XQG5hcLOZLz7IKpU35oeu7n4wvyaZHwvQqDJ93gRLqdJekcQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.34.tgz", + "integrity": "sha512-9lpq1NcJqssAF7alCO6zL3gvBVVt/lKw4oetUM7OgNnRX0OWpB+ZIO9FwCrSj/dMdmgDhPLf+119zB8QxSMmAg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.34.tgz", + "integrity": "sha512-IlWaGtj9ir7+Nrume1DGcyzBDlK8GcnJq0ANKwcI9pVw8tqr+6GD0eqyF9SF1mR8UmAp+odrx1H5NdR2cHdFHA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.34.tgz", + "integrity": "sha512-k3or+01Rska1AjUyNjA4buEwB51eyN/xPQAoOx1CjzAQC3l8rpjUDw55kXyL63O/1MUi4ISvtNtl8gLwdyEcxw==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.34.tgz", + "integrity": "sha512-+qxb8M9FfM2CJaVU7GgYpJOHM1ngQOx+/VrtBjb4C8oVqaPcESCeg2anjl+HRZy8VpYc71q/iBYausPPbJ+Keg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.34.tgz", + "integrity": "sha512-Y717ltBdQ5j5sZIHdy1DV9kieo0wMip0dCmVSTceowCPYSn1Cg33Kd6981+F/3b9FDMzNWldZFOBRILViENZSA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.34.tgz", + "integrity": "sha512-bDDgYO4LhL4+zPs+WcBkXph+AQoPcQRTv18FzZS0WhjfH8TZx2QqlVPGhmhZ6WidrY+jKthUqO6UhGyIb4MpmA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.34.tgz", + "integrity": "sha512-cfaFGXdRt0+vHsjNPyF0POM4BVSHPSbhLPe8mppDc7GDDxjIl08mV1Zou14oDWMp/XZMjYN1kWYRSfftiD0vvQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.34.tgz", + "integrity": "sha512-vmy9DxXVnRiI14s8GKuYBtess+EVcDALkbpTqd5jw4XITutIzyB7n4x0Tj5utAkKsgZJB22lLWGekr0ABnSLow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-register": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.3.3.tgz", + "integrity": "sha512-eFHOkutgIMJY5gc8LUp/7c+LLlDqzNi9T6AwCZ2WKKl3HmT+5ef3ZRyPPxDOynInML0fgaC50yszPKfPnjC0NQ==", + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.34.tgz", + "integrity": "sha512-eNPVatNET1F7tRMhii7goL/eptfxc0ALRjrj9SPFNqp0zmxrehBFD6BaP3R4LjMn6DbMO0jOAnTLFKr8NqcJAA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.34.tgz", + "integrity": "sha512-EFhpXyHEcnqWYe2rAHFd8dRw8wkrd9U+9oqcyoEL84GbanAYjiiIjBZsnR8kl0sCQ5w6bLpk7vCEIA2VS32Vcg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.34.tgz", + "integrity": "sha512-a8fbl8Ky7PxNEjf1aJmtxdDZj32/hC7S1OcA2ckEpCJRTjiKslI9vAdPpSjrKIWhws4Galpaawy0nB7fjHYf5Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.34.tgz", + "integrity": "sha512-EYvmKbSa2B3sPnpC28UEu9jBK5atGV4BaVRE7CYGUci2Hlz4AvtV/LML+TcDMT6gBgibnN2gcltWclab3UutMg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-applescript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/escape-string-applescript/-/escape-string-applescript-1.0.0.tgz", + "integrity": "sha512-4/hFwoYaC6TkpDn9A3pTC52zQPArFeXuIfhUtCGYdauTzXVP9H3BDr3oO/QzQehMpLDC7srvYgfwvImPFGfvBA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "8.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.27.0.tgz", + "integrity": "sha512-0y1bfG2ho7mty+SiILVf9PfuRA49ek4Nc60Wmmu62QlobNR+CeXa4xXIJgcuwSQgZiWaPH+5BDsctpIW0PR/wQ==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.11.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.15.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-12.3.1.tgz", + "integrity": "sha512-EN/xwKPU6jz1G0Qi6Bd/BqMnHLyRAL0VsaQaWA7F3KkjAgZHi4f1uL1JKGWNxdQpHTW/sdGONBd0bzxUka/DJg==", + "dev": true, + "dependencies": { + "@next/eslint-plugin-next": "12.3.1", + "@rushstack/eslint-patch": "^1.1.3", + "@typescript-eslint/parser": "^5.21.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^2.7.1", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.31.7", + "eslint-plugin-react-hooks": "^4.5.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", + "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "glob": "^7.2.0", + "is-glob": "^4.0.3", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-flowtype": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", + "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@babel/plugin-syntax-flow": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.8.0", + "has": "^1.0.3", + "is-core-module": "^2.13.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", + "object.values": "^1.1.6", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", + "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", + "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "aria-query": "^5.1.3", + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.6.2", + "axobject-query": "^3.1.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.3", + "language-tags": "=1.0.5", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-testing-library": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", + "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", + "dependencies": { + "@typescript-eslint/utils": "^5.58.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.4.tgz", + "integrity": "sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/final-form": { + "version": "4.20.10", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", + "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", + "dependencies": { + "@babel/runtime": "^7.10.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + } + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/first-chunk-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", + "integrity": "sha512-X8Z+b/0L4lToKYq+lwnKqi9X/Zek0NibLpsJgVsSxpoYq7JtiCtRb5HqKVEjEw/qAb/4AKKRLOwwKHlWNpm2Eg==", + "dependencies": { + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/first-chunk-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/first-chunk-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/first-chunk-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/first-chunk-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "dependencies": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/flow-parser": { + "version": "0.215.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.215.1.tgz", + "integrity": "sha512-qq3rdRToqwesrddyXf+Ml8Tuf7TdoJS+EMbJgC6fHAVoBCXjb4mHelNd3J+jD8ts0bSHX81FG3LN7Qn/dcl6pA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-extra": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "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-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-regex": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/glob-regex/-/glob-regex-0.3.2.tgz", + "integrity": "sha512-m5blUd3/OqDTWwzBBtWBPrGlAzatRywHameHeekAZyZrskYouOGdNB8T/q6JucucvJXtOuyHIn0/Yia7iDasDw==", + "dev": true + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hasbin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/hasbin/-/hasbin-1.2.3.tgz", + "integrity": "sha512-CCd8e/w2w28G8DyZvKgiHnQJ/5XXDz6qiUHnthvtag/6T5acUeN5lqq+HMoBqcmgWueWDhiCplrw0Kb1zDACRg==", + "dependencies": { + "async": "~1.5" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "dev": true, + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", + "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/husky": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz", + "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "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==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ink": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-3.2.0.tgz", + "integrity": "sha512-firNp1q3xxTzoItj/eOOSZQnYSlyrWks5llCTVX37nJ59K3eXbQ8PtzCguqo8YI19EELo5QxaKnJd4VxzhU8tg==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "auto-bind": "4.0.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.0", + "cli-cursor": "^3.1.0", + "cli-truncate": "^2.1.0", + "code-excerpt": "^3.0.0", + "indent-string": "^4.0.0", + "is-ci": "^2.0.0", + "lodash": "^4.17.20", + "patch-console": "^1.0.0", + "react-devtools-core": "^4.19.1", + "react-reconciler": "^0.26.2", + "scheduler": "^0.20.2", + "signal-exit": "^3.0.2", + "slice-ansi": "^3.0.0", + "stack-utils": "^2.0.2", + "string-width": "^4.2.2", + "type-fest": "^0.12.0", + "widest-line": "^3.1.0", + "wrap-ansi": "^6.2.0", + "ws": "^7.5.5", + "yoga-layout-prebuilt": "^1.9.6" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": ">=16.8.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/ink-spinner": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/ink-spinner/-/ink-spinner-4.0.3.tgz", + "integrity": "sha512-uJ4nbH00MM9fjTJ5xdw0zzvtXMkeGb0WV6dzSWvFv2/+ks6FIhpkt+Ge/eLdh0Ah6Vjw5pLMyNfoHQpRDRVFbQ==", + "dependencies": { + "cli-spinners": "^2.3.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ink": ">=3.0.5", + "react": ">=16.8.2" + } + }, + "node_modules/ink/node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/ink/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ink/node_modules/type-fest": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz", + "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ink/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-expression/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/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "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": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz", + "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.0.tgz", + "integrity": "sha512-rjuhAk1AJ1fssphHD0IFV6TWL40CwRZ53FrztKx43yk2v6rguBYsY4Bj1VU4HmoMmKwZUlx7mfnhDf9cOp4YTw==", + "dependencies": { + "define-properties": "^1.1.4", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "has-tostringtag": "^1.0.0", + "reflect.getprototypeof": "^1.0.3" + } + }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "node_modules/jest-diff": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.4.tgz", + "integrity": "sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz", + "integrity": "sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.6.4", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-message-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.3.tgz", + "integrity": "sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.6.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", + "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jose": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", + "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-sdsl": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz", + "integrity": "sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jscodeshift": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.13.0.tgz", + "integrity": "sha512-FNHLuwh7TeI0F4EzNVIRwUSxSqsGWM5nTv596FK4NfBnEEKFpIcyFeG559DMFGHSTIYA5AY4Fqh2cBrJx0EAwg==", + "dependencies": { + "@babel/core": "^7.13.16", + "@babel/parser": "^7.13.16", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-transform-modules-commonjs": "^7.13.8", + "@babel/preset-flow": "^7.13.13", + "@babel/preset-typescript": "^7.13.0", + "@babel/register": "^7.13.16", + "babel-core": "^7.0.0-bridge.0", + "colors": "^1.1.2", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^3.1.10", + "neo-async": "^2.5.0", + "node-dir": "^0.1.17", + "recast": "^0.20.4", + "temp": "^0.8.4", + "write-file-atomic": "^2.3.0" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/jscodeshift/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jscodeshift/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jscodeshift/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jscodeshift/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jscodeshift/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jscodeshift/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jscodeshift/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jscodeshift/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jscodeshift/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "dev": true, + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + }, + "node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "dependencies": { + "language-subtag-registry": "~0.3.2" + } + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "dev": true, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libbase64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.2.1.tgz", + "integrity": "sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==", + "dev": true + }, + "node_modules/libmime": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.2.1.tgz", + "integrity": "sha512-A0z9O4+5q+ZTj7QwNe/Juy1KARNb4WaviO4mYeFC4b8dBT2EEqK2pkM+GC8MVnkOjqhl5nYQxRgnPYRRTNmuSQ==", + "dev": true, + "dependencies": { + "encoding-japanese": "2.0.0", + "iconv-lite": "0.6.3", + "libbase64": "1.2.1", + "libqp": "2.0.1" + } + }, + "node_modules/libqp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.0.1.tgz", + "integrity": "sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==", + "dev": true + }, + "node_modules/lilconfig": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lint-staged": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz", + "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.17", + "commander": "^9.3.0", + "debug": "^4.3.4", + "execa": "^6.1.0", + "lilconfig": "2.0.5", + "listr2": "^4.0.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-inspect": "^1.12.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.1", + "yaml": "^2.1.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lint-staged/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/listr2": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", + "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.5", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "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/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/macos-release": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.2.0.tgz", + "integrity": "sha512-fSErXALFNsnowREYZ49XCdOHF8wOPWuFOGQrAhP7x5J/BqQv+B02cNsTykGpDgRVx43EKg++6ANmTaGTtW+hUA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", + "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mailparser": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.6.5.tgz", + "integrity": "sha512-nteTpF0Khm5JLOnt4sigmzNdUH/6mO7PZ4KEnvxf4mckyXYFFhrtAWZzbq/V5aQMH+049gA7ZjfLdh+QiX2Uqg==", + "dev": true, + "dependencies": { + "encoding-japanese": "2.0.0", + "he": "1.2.0", + "html-to-text": "9.0.5", + "iconv-lite": "0.6.3", + "libmime": "5.2.1", + "linkify-it": "4.0.1", + "mailsplit": "5.4.0", + "nodemailer": "6.9.3", + "tlds": "1.240.0" + } + }, + "node_modules/mailparser/node_modules/nodemailer": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", + "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/mailsplit": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.0.tgz", + "integrity": "sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==", + "dev": true, + "dependencies": { + "libbase64": "1.2.1", + "libmime": "5.2.0", + "libqp": "2.0.1" + } + }, + "node_modules/mailsplit/node_modules/libmime": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.2.0.tgz", + "integrity": "sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==", + "dev": true, + "dependencies": { + "encoding-japanese": "2.0.0", + "iconv-lite": "0.6.3", + "libbase64": "1.2.1", + "libqp": "2.0.1" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dependencies": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mem-fs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mem-fs/-/mem-fs-1.2.0.tgz", + "integrity": "sha512-b8g0jWKdl8pM0LqAPdK9i8ERL7nYrzmJfRhxMiWH2uYdfYnb7uXnmwVb0ZGe7xyEl4lj+nLIU3yf4zPUT+XsVQ==", + "dependencies": { + "through2": "^3.0.0", + "vinyl": "^2.0.1", + "vinyl-file": "^3.0.0" + } + }, + "node_modules/mem-fs-editor": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-8.0.0.tgz", + "integrity": "sha512-0+6Zp44EmPpF01MZOlY0kt7JTndjdvALo4jA7Kk9GPCuqGzGnBmWtcE44Cwzj1aru57IN5/LKIWd1lIvaT6sKw==", + "dependencies": { + "commondir": "^1.0.1", + "deep-extend": "^0.6.0", + "ejs": "^3.1.5", + "globby": "^11.0.1", + "isbinaryfile": "^4.0.0", + "multimatch": "^5.0.0", + "normalize-path": "^3.0.0", + "through2": "^4.0.2", + "vinyl": "^2.2.1" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/mem-fs-editor/node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/mem/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoassert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", + "integrity": "sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==" + }, + "node_modules/nanoid": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/next": { + "version": "13.4.5", + "resolved": "https://registry.npmjs.org/next/-/next-13.4.5.tgz", + "integrity": "sha512-pfNsRLVM9e5Y1/z02VakJRfD6hMQkr24FaN2xc9GbcZDBxoOgiNAViSg5cXwlWCoMhtm4U315D7XYhgOr96Q3Q==", + "dependencies": { + "@next/env": "13.4.5", + "@swc/helpers": "0.5.1", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.14", + "styled-jsx": "5.1.1", + "watchpack": "2.4.0", + "zod": "3.21.4" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=16.8.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "13.4.5", + "@next/swc-darwin-x64": "13.4.5", + "@next/swc-linux-arm64-gnu": "13.4.5", + "@next/swc-linux-arm64-musl": "13.4.5", + "@next/swc-linux-x64-gnu": "13.4.5", + "@next/swc-linux-x64-musl": "13.4.5", + "@next/swc-win32-arm64-msvc": "13.4.5", + "@next/swc-win32-ia32-msvc": "13.4.5", + "@next/swc-win32-x64-msvc": "13.4.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "fibers": ">= 3.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "fibers": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/next/node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node_modules/node-dir": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", + "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", + "dependencies": { + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.10.5" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.3.tgz", + "integrity": "sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-gyp-build": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + }, + "node_modules/nodemailer": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz", + "integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-path": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz", + "integrity": "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==", + "dependencies": { + "which": "^1.2.10" + }, + "bin": { + "npm-path": "bin/npm-path" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm-path/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz", + "integrity": "sha512-CM8vMpeFQ7MAPin0U3wzDhSGV0hMHNwHU0wjo402IVizPDrs45jSfSuoC+wThevY88LQti8VvaAnqYAeVy3I1A==", + "dependencies": { + "commander": "^2.9.0", + "npm-path": "^2.0.2", + "which": "^1.2.10" + }, + "bin": { + "npm-which": "bin/npm-which.js" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/npm-which/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/npm-which/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, + "node_modules/oauth": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", + "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz", + "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.hasown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", + "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "dependencies": { + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/openid-client": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.2.1.tgz", + "integrity": "sha512-KPxqWnxobG/70Cxqyvd43RWfCfHedFnCdHSBpw5f7WnTnuBAeBnvot/BIo+brrcTr0wyAYUlL/qejQSGwWtdIg==", + "dependencies": { + "jose": "^4.10.0", + "lru-cache": "^6.0.0", + "object-hash": "^2.0.1", + "oidc-token-hash": "^5.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", + "integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==", + "dependencies": { + "bl": "^4.0.3", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "log-symbols": "^4.0.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-5.0.1.tgz", + "integrity": "sha512-0EQpaHUHq7olp2/YFUr+0vZi9tMpDTblHGz+Ch5RntKxiRXOAY0JOz1UlxhSjMSksHvkm13eD6elJj3M8Ht/kw==", + "dependencies": { + "macos-release": "^3.0.1", + "windows-release": "^5.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-wait-for": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-3.2.0.tgz", + "integrity": "sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==", + "dev": true, + "dependencies": { + "p-timeout": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "dev": true, + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/patch-console": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-1.0.0.tgz", + "integrity": "sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "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": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "dev": true, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/playwright-core": { + "version": "1.31.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.31.2.tgz", + "integrity": "sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==", + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-prisma": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-prisma/-/prettier-plugin-prisma-4.4.0.tgz", + "integrity": "sha512-631zwtvHUjcuS+i83wkxE3LqizazojWie4Pkha9GL6r1UsochNUedvxfvWPCdw6Dpyawvc5UKkmxcb9aT9+xfg==", + "dev": true, + "dependencies": { + "@prisma/prisma-fmt-wasm": "4.4.0-66.f352a33b70356f46311da8b00d83386dd9f145d6" + }, + "engines": { + "node": ">=12", + "npm": ">=7" + }, + "peerDependencies": { + "prettier": ">=2" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/pretty-quick": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.1.3.tgz", + "integrity": "sha512-kOCi2FJabvuh1as9enxYmrnBC6tVMoVOenMaBqRfsvBHB0cbpYHjdQEpSglpASDFEXVwplpcGR4CLEaisYAFcA==", + "dev": true, + "dependencies": { + "chalk": "^3.0.0", + "execa": "^4.0.0", + "find-up": "^4.1.0", + "ignore": "^5.1.4", + "mri": "^1.1.5", + "multimatch": "^4.0.0" + }, + "bin": { + "pretty-quick": "bin/pretty-quick.js" + }, + "engines": { + "node": ">=10.13" + }, + "peerDependencies": { + "prettier": ">=2.0.0" + } + }, + "node_modules/pretty-quick/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-quick/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/pretty-quick/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/pretty-quick/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-quick/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pretty-quick/node_modules/multimatch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", + "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-quick/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-quick/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-quick/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pretty-quick/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/preview-email": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/preview-email/-/preview-email-3.0.7.tgz", + "integrity": "sha512-WGko2NiS3d8qoGcC981sXotm7noW/dcv4Cp4wo+X95ek2WwJ4A+aDpw/MzMjMW/johihvmfrfUdUWBbh+HnxCw==", + "deprecated": "We just released outbound SMTP support! Try it out at @ https://forwardemail.net/docs/how-to-javascript-contact-forms-node-js 🚀 ✉️ 👽", + "dev": true, + "dependencies": { + "ci-info": "^3.3.2", + "crypto-random-string": "3.3.1", + "display-notification": "2.0.0", + "get-port": "5.1.1", + "mailparser": "^3.5.0", + "nodemailer": "^6.7.7", + "open": "7", + "p-event": "4.2.0", + "p-wait-for": "3.2.0", + "pug": "^3.0.2", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/preview-email/node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/prisma": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.6.1.tgz", + "integrity": "sha512-BR4itMCuzrDV4tn3e2TF+nh1zIX/RVU0isKtKoN28ADeoJ9nYaMhiuRRkFd2TZN8+l/XfYzoRKyHzUFXLQhmBQ==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "4.6.1" + }, + "bin": { + "prisma": "build/index.js", + "prisma2": "build/index.js" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pug": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz", + "integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==", + "dev": true, + "dependencies": { + "pug-code-gen": "^3.0.2", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dev": true, + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz", + "integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==", + "dev": true, + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.0.0", + "pug-runtime": "^3.0.0", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz", + "integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==", + "dev": true + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "dev": true, + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "dev": true, + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dev": true, + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dev": true, + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "dev": true + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dev": true, + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "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==", + "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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.0.tgz", + "integrity": "sha512-E3C3X1skWBdBzwpOUbmXG8SgH6BtsluSMe+s6rRcujNKG1DGi8uIfhdhszkgDpAsMoE55hwqRUzeXCmETDBpTg==", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-final-form": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", + "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", + "dependencies": { + "@babel/runtime": "^7.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + }, + "peerDependencies": { + "final-form": "^4.20.4", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-reconciler": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.26.2.tgz", + "integrity": "sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^17.0.2" + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.20.5", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.20.5.tgz", + "integrity": "sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==", + "dependencies": { + "ast-types": "0.14.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recrawl-sync": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recrawl-sync/-/recrawl-sync-2.2.3.tgz", + "integrity": "sha512-vSaTR9t+cpxlskkdUFrsEpnf67kSmPk66yAGT1fZPrDudxQjoMzPgQhSMImQ0pAw5k0NPirefQfhopSjhdUtpQ==", + "dev": true, + "dependencies": { + "@cush/relative": "^1.0.0", + "glob-regex": "^0.3.0", + "slash": "^3.0.0", + "sucrase": "^3.20.3", + "tslib": "^1.9.3" + } + }, + "node_modules/recrawl-sync/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.3.tgz", + "integrity": "sha512-TTAOZpkJ2YLxl7mVHWrNo3iDMEkYlva/kgFcXndqMgbo/AZUmmavEkdXV+hXtE4P8xdyEKRzalaFqZVuwIk/Nw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==" + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-3.2.0.tgz", + "integrity": "sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg==", + "dev": true, + "dependencies": { + "execa": "^0.10.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/run-applescript/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/run-applescript/node_modules/execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/run-applescript/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/run-applescript/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/run-applescript/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/run-applescript/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/run-applescript/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/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==", + "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/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/secure-password": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/secure-password/-/secure-password-4.0.0.tgz", + "integrity": "sha512-B268T/tx+hq7q85KH6gonEqK/lhrLhNtzYzqojuMtBPVFBtwiIwxqF+4yr9POsJu5cIxbJyM66eYfXZiPZUXRA==", + "dependencies": { + "nanoassert": "^1.0.0", + "sodium-native": "^3.1.1" + } + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "dev": true, + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-wcswidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz", + "integrity": "sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==" + }, + "node_modules/sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sodium-native": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.4.1.tgz", + "integrity": "sha512-PaNN/roiFWzVVTL6OqjzYct38NSXewdl2wz8SRB51Br/MLIJPrbM3XexhVWkq7D3UWMysfrhKVf1v1phZq6MeQ==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated" + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", + "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom-buf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz", + "integrity": "sha512-1sUIL1jck0T1mhOLP2c696BIznzT525Lkub+n4jjMHjhjhoAQA6Ye659DxdlZBr0aLDMQoTxKIpnlqxgtwjsuQ==", + "dependencies": { + "is-utf8": "^0.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", + "integrity": "sha512-yH0+mD8oahBZWnY43vxs4pSinn8SMKAdml/EOGBewoe1Y0Eitd0h2Mg3ZRiXruUW6L4P+lvZiEgbh0NgUGia1w==", + "dependencies": { + "first-chunk-stream": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom-stream/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-0.4.2.tgz", + "integrity": "sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "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/superjson": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.11.0.tgz", + "integrity": "sha512-6PfAg1FKhqkwWvPb2uXhH4MkMttdc17eJ91+Aoz4s1XUEDZFmLfFx/xVA3wgkPxAGy5dpozgGdK6V/n20Wj9yg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/temp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", + "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", + "dependencies": { + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/tinybench": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.1.tgz", + "integrity": "sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.1.1.tgz", + "integrity": "sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tlds": { + "version": "1.240.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.240.0.tgz", + "integrity": "sha512-1OYJQenswGZSOdRw7Bql5Qu7uf75b+F3HFBXbqnG/ifHa0fev1XcG+3pJf3pA/KC6RtHQzfKgIf1vkMlMG7mtQ==", + "dev": true, + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "dev": true + }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.0.0.tgz", + "integrity": "sha512-SLBg2GBKlR6bVtMgJJlud/o3waplKtL7skmLkExomIiaAtLGtVsoXIqP3SYdjbcH9lq/KVv7pMZeCBpLYOit6Q==", + "dependencies": { + "json5": "^2.2.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tslog": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.0.tgz", + "integrity": "sha512-YEb55YxukbKO0bSAAsd9eSnw6RA0e3jt3cniZ00wj9offySAbp20lBrjOh1OTn11L51r58V4kFy8h7dMPnbpmg==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/fullstack-build/tslog?sponsor=1" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated" + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/username": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", + "integrity": "sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg==", + "dependencies": { + "execa": "^1.0.0", + "mem": "^4.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/username/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/username/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/username/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/username/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/username/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/username/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/username/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/username/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/username/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/username/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-file": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-3.0.0.tgz", + "integrity": "sha512-BoJDj+ca3D9xOuPEM6RWVtWQtvEPQiQYn82LvdxhLWplfQsBzBqtgK0yhCP0s1BNTi6dH9BO+dzybvyQIacifg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.3.0", + "strip-bom-buf": "^1.0.0", + "strip-bom-stream": "^2.0.0", + "vinyl": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/vinyl-file/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz", + "integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==", + "dev": true, + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-3.6.0.tgz", + "integrity": "sha512-UfsPYonxLqPD633X8cWcPFVuYzx/CMNHAjZTasYwX69sXpa4gNmQkR0XCjj82h7zhLGdTWagMjC1qfb9S+zv0A==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "recrawl-sync": "^2.0.3", + "tsconfig-paths": "^4.0.0" + }, + "peerDependencies": { + "vite": ">2.0.0-0" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/vite/node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", + "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.25.3.tgz", + "integrity": "sha512-/UzHfXIKsELZhL7OaM2xFlRF8HRZgAHtPctacvNK8H4vOcbJJAMEgbWNGSAK7Y9b1NBe5SeM7VTuz2RsTHFJJA==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.3", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "acorn": "^8.8.0", + "acorn-walk": "^8.2.0", + "chai": "^4.3.6", + "debug": "^4.3.4", + "local-pkg": "^0.4.2", + "source-map": "^0.6.1", + "strip-literal": "^0.4.2", + "tinybench": "^2.3.1", + "tinypool": "^0.3.0", + "tinyspy": "^1.0.2", + "vite": "^3.0.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.16.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/watchpack": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.1.tgz", + "integrity": "sha512-Oo7LXCmc1eE1AjyuSBmtC3+Wy4HcV8PxWh2kP6fOl8yTlNS7r0K9l1ao2lrrUza7V39Y3D/BbJgY8VeSlc5JKw==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.3.0.tgz", + "integrity": "sha512-J3TPm54bPARx6QG8z4cKBszahnUglcv70+N+8gUqv2I5KOFHJbzBiLx+pAp606so0X004fxM7hqRu10MLjJifA==", + "dev": true, + "dependencies": { + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^6.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/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/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-5.1.1.tgz", + "integrity": "sha512-NMD00arvqcq2nwqc5Q6KtrSRHK+fVD31erE5FEMahAw5PmVCgD7MUXodq3pdZSUkqA9Cda2iWx6s1XYwiJWRmw==", + "dependencies": { + "execa": "^5.1.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/windows-release/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/windows-release/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/windows-release/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/windows-release/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/windows-release/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-layout-prebuilt": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.10.0.tgz", + "integrity": "sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g==", + "dependencies": { + "@types/yoga-layout": "1.9.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/examples/playwright/package.json b/examples/playwright/package.json new file mode 100644 index 0000000000..89aa891aa3 --- /dev/null +++ b/examples/playwright/package.json @@ -0,0 +1,70 @@ +{ + "name": "playwright", + "version": "1.0.0", + "scripts": { + "dev": "blitz dev", + "build": "blitz build", + "start": "blitz start", + "studio": "blitz prisma studio", + "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", + "test:unit": "vitest run --passWithNoTests", + "test:unit:watch": "vitest", + "test:e2e:server": "NODE_ENV=test blitz prisma migrate reset -f && NODE_ENV=test blitz db seed && NODE_ENV=test blitz build && NODE_ENV=test blitz start -p 3099 -d", + "test:e2e": "npx playwright test", + "test:e2e:ui": "npx playwright test --ui", + "prepare": "husky install" + }, + "prisma": { + "schema": "db/schema.prisma" + }, + "prettier": { + "semi": false, + "printWidth": 100 + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "eslint --fix" + ] + }, + "dependencies": { + "@blitzjs/auth": "2.0.0-beta.32", + "@blitzjs/next": "2.0.0-beta.32", + "@blitzjs/rpc": "2.0.0-beta.32", + "@playwright/test": "1.31.2", + "@prisma/client": "4.6.1", + "blitz": "2.0.0-beta.32", + "next": "13.4.5", + "prisma": "4.6.1", + "react": "18.2.0", + "react-dom": "18.2.0", + "secure-password": "4.0.0", + "zod": "3.20.2", + "final-form": "4.20.10", + "react-final-form": "6.5.9" + }, + "devDependencies": { + "@next/bundle-analyzer": "12.0.8", + "@testing-library/jest-dom": "5.16.5", + "@testing-library/react": "13.4.0", + "@testing-library/react-hooks": "8.0.1", + "@types/node": "18.11.9", + "@types/preview-email": "2.0.1", + "@types/react": "18.0.25", + "@typescript-eslint/eslint-plugin": "5.42.1", + "@vitejs/plugin-react": "2.2.0", + "eslint": "8.27.0", + "eslint-config-next": "12.3.1", + "eslint-config-prettier": "8.5.0", + "husky": "8.0.2", + "jsdom": "20.0.3", + "lint-staged": "13.0.3", + "prettier": "^2.7.1", + "prettier-plugin-prisma": "4.4.0", + "pretty-quick": "3.1.3", + "preview-email": "3.0.7", + "typescript": "^4.8.4", + "vite-tsconfig-paths": "3.6.0", + "vitest": "0.25.3" + }, + "private": true +} diff --git a/examples/playwright/playwright.config.ts b/examples/playwright/playwright.config.ts new file mode 100644 index 0000000000..37c3467896 --- /dev/null +++ b/examples/playwright/playwright.config.ts @@ -0,0 +1,52 @@ +import { defineConfig, devices } from "@playwright/test" + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./test/e2e", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Timeout for each test */ + timeout: 10000, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + webServer: { + command: "npm run test:e2e:server", + url: "http://localhost:3099", + }, + globalSetup: require.resolve("./test/e2e/global-setup"), + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3099", + storageState: "state.json", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "retain-on-failure", + screenshot: "only-on-failure", + video: "retain-on-failure", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/examples/playwright/public/favicon.ico b/examples/playwright/public/favicon.ico new file mode 100755 index 0000000000..c7bd1c095d Binary files /dev/null and b/examples/playwright/public/favicon.ico differ diff --git a/examples/playwright/public/logo.png b/examples/playwright/public/logo.png new file mode 100644 index 0000000000..6a982616ee Binary files /dev/null and b/examples/playwright/public/logo.png differ diff --git a/examples/playwright/src/auth/components/LoginForm.tsx b/examples/playwright/src/auth/components/LoginForm.tsx new file mode 100644 index 0000000000..ff488863c6 --- /dev/null +++ b/examples/playwright/src/auth/components/LoginForm.tsx @@ -0,0 +1,59 @@ +import { AuthenticationError, PromiseReturnType } from "blitz" +import Link from "next/link" +import { LabeledTextField } from "src/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "src/core/components/Form" +import login from "src/auth/mutations/login" +import { Login } from "src/auth/schemas" +import { useMutation } from "@blitzjs/rpc" +import { Routes } from "@blitzjs/next" + +type LoginFormProps = { + onSuccess?: (user: PromiseReturnType) => void +} + +export const LoginForm = (props: LoginFormProps) => { + const [loginMutation] = useMutation(login) + return ( +
+

Login

+ +
{ + try { + const user = await loginMutation(values) + props.onSuccess?.(user) + } catch (error: any) { + if (error instanceof AuthenticationError) { + return { [FORM_ERROR]: "Sorry, those credentials are invalid" } + } else { + return { + [FORM_ERROR]: + "Sorry, we had an unexpected error. Please try again. - " + error.toString(), + } + } + } + }} + > + + +
+ + Forgot your password? + +
+ + +
+ Or{" "} + + Sign Up + +
+
+ ) +} + +export default LoginForm diff --git a/examples/playwright/src/auth/components/SignupForm.tsx b/examples/playwright/src/auth/components/SignupForm.tsx new file mode 100644 index 0000000000..bfe260350a --- /dev/null +++ b/examples/playwright/src/auth/components/SignupForm.tsx @@ -0,0 +1,42 @@ +import { LabeledTextField } from "src/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "src/core/components/Form" +import signup from "src/auth/mutations/signup" +import { Signup } from "src/auth/schemas" +import { useMutation } from "@blitzjs/rpc" + +type SignupFormProps = { + onSuccess?: () => void +} + +export const SignupForm = (props: SignupFormProps) => { + const [signupMutation] = useMutation(signup) + return ( +
+

Create an Account

+ +
{ + try { + await signupMutation(values) + props.onSuccess?.() + } catch (error: any) { + if (error.code === "P2002" && error.meta?.target?.includes("email")) { + // This error comes from Prisma + return { email: "This email is already being used" } + } else { + return { [FORM_ERROR]: error.toString() } + } + } + }} + > + + + +
+ ) +} + +export default SignupForm diff --git a/examples/playwright/src/auth/mutations/changePassword.ts b/examples/playwright/src/auth/mutations/changePassword.ts new file mode 100644 index 0000000000..052a087b96 --- /dev/null +++ b/examples/playwright/src/auth/mutations/changePassword.ts @@ -0,0 +1,32 @@ +import { NotFoundError, AuthenticationError } from 'blitz' +import { resolver } from '@blitzjs/rpc' +import { SecurePassword } from "@blitzjs/auth/secure-password" +import db from 'db' +import { authenticateUser } from './login' +import { ChangePassword } from '../schemas' + +export default resolver.pipe( + resolver.zod(ChangePassword), + resolver.authorize(), + async ({ currentPassword, newPassword }, ctx) => { + const user = await db.user.findFirst({ where: { id: ctx.session.userId } }) + if (!user) throw new NotFoundError() + + try { + await authenticateUser(user.email, currentPassword) + } catch (error) { + if (error instanceof AuthenticationError) { + throw new Error('Invalid Password') + } + throw error + } + + const hashedPassword = await SecurePassword.hash(newPassword.trim()) + await db.user.update({ + where: { id: user.id }, + data: { hashedPassword }, + }) + + return true + }, +) diff --git a/examples/playwright/src/auth/mutations/forgotPassword.test.ts b/examples/playwright/src/auth/mutations/forgotPassword.test.ts new file mode 100644 index 0000000000..62ed15cf3e --- /dev/null +++ b/examples/playwright/src/auth/mutations/forgotPassword.test.ts @@ -0,0 +1,64 @@ +import { vi, describe, it, beforeEach } from "vitest" +import db from "db" +import { hash256 } from "@blitzjs/auth" +import forgotPassword from "./forgotPassword" +import previewEmail from "preview-email" +import { Ctx } from "@blitzjs/next" + +beforeEach(async () => { + await db.$reset() +}) + +const generatedToken = "plain-token" +vi.mock("@blitzjs/auth", async () => { + const auth = await vi.importActual>("@blitzjs/auth")! + return { + ...auth, + generateToken: () => generatedToken + } +}) + +vi.mock("preview-email", () => ({ default: vi.fn() })) + +describe("forgotPassword mutation", () => { + it("does not throw error if user doesn't exist", async () => { + await expect(forgotPassword({ email: "no-user@email.com" }, {} as Ctx)).resolves.not.toThrow() + }) + + it("works correctly", async () => { + // Create test user + const user = await db.user.create({ + data: { + email: "user@example.com", + tokens: { + // Create old token to ensure it's deleted + create: { + type: "RESET_PASSWORD", + hashedToken: "token", + expiresAt: new Date(), + sentTo: "user@example.com", + }, + }, + }, + include: { tokens: true }, + }) + + // Invoke the mutation + await forgotPassword({ email: user.email }, {} as Ctx) + + const tokens = await db.token.findMany({ where: { userId: user.id } }) + const token = tokens[0] + if (!user.tokens[0]) throw new Error("Missing user token") + if (!token) throw new Error("Missing token") + + // delete's existing tokens + expect(tokens.length).toBe(1) + + expect(token.id).not.toBe(user.tokens[0].id) + expect(token.type).toBe("RESET_PASSWORD") + expect(token.sentTo).toBe(user.email) + expect(token.hashedToken).toBe(hash256(generatedToken)) + expect(token.expiresAt > new Date()).toBe(true) + expect(previewEmail).toBeCalled() + }) +}) diff --git a/examples/playwright/src/auth/mutations/forgotPassword.ts b/examples/playwright/src/auth/mutations/forgotPassword.ts new file mode 100644 index 0000000000..7e40fc967c --- /dev/null +++ b/examples/playwright/src/auth/mutations/forgotPassword.ts @@ -0,0 +1,42 @@ +import { generateToken, hash256 } from "@blitzjs/auth" +import { resolver } from "@blitzjs/rpc" +import db from "db" +import { forgotPasswordMailer } from "mailers/forgotPasswordMailer" +import { ForgotPassword } from "../schemas" + +const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4 + +export default resolver.pipe(resolver.zod(ForgotPassword), async ({ email }) => { + // 1. Get the user + const user = await db.user.findFirst({ where: { email: email.toLowerCase() } }) + + // 2. Generate the token and expiration date. + const token = generateToken() + const hashedToken = hash256(token) + const expiresAt = new Date() + expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS) + + // 3. If user with this email was found + if (user) { + // 4. Delete any existing password reset tokens + await db.token.deleteMany({ where: { type: "RESET_PASSWORD", userId: user.id } }) + // 5. Save this new token in the database. + await db.token.create({ + data: { + user: { connect: { id: user.id } }, + type: "RESET_PASSWORD", + expiresAt, + hashedToken, + sentTo: user.email, + }, + }) + // 6. Send the email + await forgotPasswordMailer({ to: user.email, token }).send() + } else { + // 7. If no user found wait the same time so attackers can't tell the difference + await new Promise((resolve) => setTimeout(resolve, 750)) + } + + // 8. Return the same result whether a password reset email was sent or not + return +}) diff --git a/examples/playwright/src/auth/mutations/login.ts b/examples/playwright/src/auth/mutations/login.ts new file mode 100644 index 0000000000..e055b7822b --- /dev/null +++ b/examples/playwright/src/auth/mutations/login.ts @@ -0,0 +1,32 @@ +import { SecurePassword } from "@blitzjs/auth/secure-password" +import { resolver } from "@blitzjs/rpc" +import { AuthenticationError } from "blitz" +import db from "db" +import { Role } from "types" +import { Login } from "../schemas" + +export const authenticateUser = async (rawEmail: string, rawPassword: string) => { + const { email, password } = Login.parse({ email: rawEmail, password: rawPassword }) + const user = await db.user.findFirst({ where: { email } }) + if (!user) throw new AuthenticationError() + + const result = await SecurePassword.verify(user.hashedPassword, password) + + if (result === SecurePassword.VALID_NEEDS_REHASH) { + // Upgrade hashed password with a more secure hash + const improvedHash = await SecurePassword.hash(password) + await db.user.update({ where: { id: user.id }, data: { hashedPassword: improvedHash } }) + } + + const { hashedPassword, ...rest } = user + return rest +} + +export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ctx) => { + // This throws an error if credentials are invalid + const user = await authenticateUser(email, password) + + await ctx.session.$create({ userId: user.id, role: user.role as Role}) + + return user +}) diff --git a/examples/playwright/src/auth/mutations/logout.ts b/examples/playwright/src/auth/mutations/logout.ts new file mode 100644 index 0000000000..c2f2fefd8d --- /dev/null +++ b/examples/playwright/src/auth/mutations/logout.ts @@ -0,0 +1,5 @@ +import { Ctx } from "blitz" + +export default async function logout(_: any, ctx: Ctx) { + return await ctx.session.$revoke() +} diff --git a/examples/playwright/src/auth/mutations/resetPassword.test.ts b/examples/playwright/src/auth/mutations/resetPassword.test.ts new file mode 100644 index 0000000000..0a7d3f5dab --- /dev/null +++ b/examples/playwright/src/auth/mutations/resetPassword.test.ts @@ -0,0 +1,84 @@ +import { vi, describe, it, beforeEach, expect } from "vitest" +import resetPassword from "./resetPassword" +import db from "db" +import { hash256 } from "@blitzjs/auth" +import { SecurePassword } from "@blitzjs/auth/secure-password" + +beforeEach(async () => { + await db.$reset() +}) + +const mockCtx: any = { + session: { + $create: vi.fn(), + }, +} + +describe("resetPassword mutation", () => { + it("works correctly", async () => { + expect(true).toBe(true) + + // Create test user + const goodToken = "randomPasswordResetToken" + const expiredToken = "expiredRandomPasswordResetToken" + const future = new Date() + future.setHours(future.getHours() + 4) + const past = new Date() + past.setHours(past.getHours() - 4) + + const user = await db.user.create({ + data: { + email: "user@example.com", + tokens: { + // Create old token to ensure it's deleted + create: [ + { + type: "RESET_PASSWORD", + hashedToken: hash256(expiredToken), + expiresAt: past, + sentTo: "user@example.com", + }, + { + type: "RESET_PASSWORD", + hashedToken: hash256(goodToken), + expiresAt: future, + sentTo: "user@example.com", + }, + ], + }, + }, + include: { tokens: true }, + }) + + const newPassword = "newPassword" + + // Non-existent token + await expect( + resetPassword({ token: "no-token", password: "", passwordConfirmation: "" }, mockCtx) + ).rejects.toThrowError() + + // Expired token + await expect( + resetPassword( + { token: expiredToken, password: newPassword, passwordConfirmation: newPassword }, + mockCtx + ) + ).rejects.toThrowError() + + // Good token + await resetPassword( + { token: goodToken, password: newPassword, passwordConfirmation: newPassword }, + mockCtx + ) + + // Delete's the token + const numberOfTokens = await db.token.count({ where: { userId: user.id } }) + expect(numberOfTokens).toBe(0) + + // Updates user's password + const updatedUser = await db.user.findFirst({ where: { id: user.id } }) + expect(await SecurePassword.verify(updatedUser!.hashedPassword, newPassword)).toBe( + SecurePassword.VALID + ) + }) +}) diff --git a/examples/playwright/src/auth/mutations/resetPassword.ts b/examples/playwright/src/auth/mutations/resetPassword.ts new file mode 100644 index 0000000000..23f602fb75 --- /dev/null +++ b/examples/playwright/src/auth/mutations/resetPassword.ts @@ -0,0 +1,49 @@ +import { hash256 } from "@blitzjs/auth" +import { SecurePassword } from "@blitzjs/auth/secure-password" +import { resolver } from "@blitzjs/rpc" +import db from "db" +import { ResetPassword } from "../schemas" +import login from "./login" + +export class ResetPasswordError extends Error { + name = "ResetPasswordError" + message = "Reset password link is invalid or it has expired." +} + +export default resolver.pipe(resolver.zod(ResetPassword), async ({ password, token }, ctx) => { + // 1. Try to find this token in the database + const hashedToken = hash256(token) + const possibleToken = await db.token.findFirst({ + where: { hashedToken, type: "RESET_PASSWORD" }, + include: { user: true }, + }) + + // 2. If token not found, error + if (!possibleToken) { + throw new ResetPasswordError() + } + const savedToken = possibleToken + + // 3. Delete token so it can't be used again + await db.token.delete({ where: { id: savedToken.id } }) + + // 4. If token has expired, error + if (savedToken.expiresAt < new Date()) { + throw new ResetPasswordError() + } + + // 5. Since token is valid, now we can update the user's password + const hashedPassword = await SecurePassword.hash(password.trim()) + const user = await db.user.update({ + where: { id: savedToken.userId }, + data: { hashedPassword }, + }) + + // 6. Revoke all existing login sessions for this user + await db.session.deleteMany({ where: { userId: user.id } }) + + // 7. Now log the user in with the new credentials + await login({ email: user.email, password }, ctx) + + return true +}) diff --git a/examples/playwright/src/auth/mutations/signup.ts b/examples/playwright/src/auth/mutations/signup.ts new file mode 100644 index 0000000000..27c669f071 --- /dev/null +++ b/examples/playwright/src/auth/mutations/signup.ts @@ -0,0 +1,16 @@ +import { SecurePassword } from "@blitzjs/auth/secure-password" +import { resolver } from "@blitzjs/rpc" +import db from "db" +import { Role } from "types" +import { Signup } from "../schemas" + +export default resolver.pipe(resolver.zod(Signup), async ({ email, password }, ctx) => { + const hashedPassword = await SecurePassword.hash(password.trim()) + const user = await db.user.create({ + data: { email: email.toLowerCase().trim(), hashedPassword, role: "USER" }, + select: { id: true, name: true, email: true, role: true }, + }) + + await ctx.session.$create({ userId: user.id, role: user.role as Role }) + return user +}) diff --git a/examples/playwright/src/auth/schemas.ts b/examples/playwright/src/auth/schemas.ts new file mode 100644 index 0000000000..38249ec452 --- /dev/null +++ b/examples/playwright/src/auth/schemas.ts @@ -0,0 +1,42 @@ +import { z } from "zod" + +export const email = z + .string() + .email() + .transform((str) => str.toLowerCase().trim()) + +export const password = z + .string() + .min(10) + .max(100) + .transform((str) => str.trim()) + +export const Signup = z.object({ + email, + password, +}) + +export const Login = z.object({ + email, + password: z.string(), +}) + +export const ForgotPassword = z.object({ + email, +}) + +export const ResetPassword = z + .object({ + password: password, + passwordConfirmation: password, + token: z.string(), + }) + .refine((data) => data.password === data.passwordConfirmation, { + message: "Passwords don't match", + path: ["passwordConfirmation"], // set the path of the error + }) + +export const ChangePassword = z.object({ + currentPassword: z.string(), + newPassword: password, +}) diff --git a/examples/playwright/src/blitz-client.ts b/examples/playwright/src/blitz-client.ts new file mode 100644 index 0000000000..b4bdf60d73 --- /dev/null +++ b/examples/playwright/src/blitz-client.ts @@ -0,0 +1,14 @@ +import { AuthClientPlugin } from "@blitzjs/auth" +import { setupBlitzClient } from "@blitzjs/next" +import { BlitzRpcPlugin } from "@blitzjs/rpc" + +export const authConfig = { + cookiePrefix: "playwright" +} + +export const { withBlitz } = setupBlitzClient({ + plugins: [ + AuthClientPlugin(authConfig), + BlitzRpcPlugin({}), + ], +}) diff --git a/examples/playwright/src/blitz-server.ts b/examples/playwright/src/blitz-server.ts new file mode 100644 index 0000000000..d89437bd70 --- /dev/null +++ b/examples/playwright/src/blitz-server.ts @@ -0,0 +1,17 @@ +import { setupBlitzServer } from '@blitzjs/next'; +import { AuthServerPlugin, PrismaStorage } from '@blitzjs/auth'; +import { simpleRolesIsAuthorized } from '@blitzjs/auth'; +import { BlitzLogger } from 'blitz'; +import db from 'db'; +import { authConfig } from './blitz-client'; + +export const { gSSP, gSP, api } = setupBlitzServer({ + plugins: [ + AuthServerPlugin({ + ...authConfig, + storage: PrismaStorage(db), + isAuthorized: simpleRolesIsAuthorized + }) + ], + logger: BlitzLogger({}) +}); diff --git a/examples/playwright/src/core/components/Form.tsx b/examples/playwright/src/core/components/Form.tsx new file mode 100644 index 0000000000..eb45dc3b9f --- /dev/null +++ b/examples/playwright/src/core/components/Form.tsx @@ -0,0 +1,59 @@ +import { ReactNode, PropsWithoutRef } from "react" +import { Form as FinalForm, FormProps as FinalFormProps } from "react-final-form" +import { z } from "zod" +import { validateZodSchema } from "blitz" +export { FORM_ERROR } from "final-form" + +export interface FormProps> + extends Omit, "onSubmit"> { + /** All your form fields */ + children?: ReactNode + /** Text to display in the submit button */ + submitText?: string + schema?: S + onSubmit: FinalFormProps>["onSubmit"] + initialValues?: FinalFormProps>["initialValues"] +} + +export function Form>({ + children, + submitText, + schema, + initialValues, + onSubmit, + ...props +}: FormProps) { + return ( + ( +
+ {/* Form fields supplied as children are rendered here */} + {children} + + {submitError && ( +
+ {submitError} +
+ )} + + {submitText && ( + + )} + + +
+ )} + /> + ) +} + +export default Form diff --git a/examples/playwright/src/core/components/LabeledTextField.tsx b/examples/playwright/src/core/components/LabeledTextField.tsx new file mode 100644 index 0000000000..7dc98b89c9 --- /dev/null +++ b/examples/playwright/src/core/components/LabeledTextField.tsx @@ -0,0 +1,66 @@ +import { forwardRef, ComponentPropsWithoutRef, PropsWithoutRef } from "react" +import { useField, UseFieldConfig } from "react-final-form" + +export interface LabeledTextFieldProps extends PropsWithoutRef { + /** Field name. */ + name: string + /** Field label. */ + label: string + /** Field type. Doesn't include radio buttons and checkboxes */ + type?: "text" | "password" | "email" | "number" + outerProps?: PropsWithoutRef + labelProps?: ComponentPropsWithoutRef<"label"> + fieldProps?: UseFieldConfig +} + +export const LabeledTextField = forwardRef( + ({ name, label, outerProps, fieldProps, labelProps, ...props }, ref) => { + const { + input, + meta: { touched, error, submitError, submitting }, + } = useField(name, { + parse: + props.type === "number" + ? (Number as any) + : // Converting `""` to `null` ensures empty values will be set to null in the DB + (v) => (v === "" ? null : v), + ...fieldProps, + }) + + const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError + + return ( +
+ + + {touched && normalizedError && ( +
+ {normalizedError} +
+ )} + + +
+ ) + } +) + +export default LabeledTextField diff --git a/examples/playwright/src/core/layouts/Layout.tsx b/examples/playwright/src/core/layouts/Layout.tsx new file mode 100644 index 0000000000..363a619d5d --- /dev/null +++ b/examples/playwright/src/core/layouts/Layout.tsx @@ -0,0 +1,21 @@ +import Head from "next/head" +import React from "react" +import { BlitzLayout } from "@blitzjs/next" + +const Layout: BlitzLayout<{ title?: string; children?: React.ReactNode }> = ({ + title, + children, +}) => { + return ( + <> + + {title || "playwright"} + + + + {children} + + ) +} + +export default Layout diff --git a/examples/playwright/src/pages/404.tsx b/examples/playwright/src/pages/404.tsx new file mode 100644 index 0000000000..08304d80a9 --- /dev/null +++ b/examples/playwright/src/pages/404.tsx @@ -0,0 +1,20 @@ +import Head from 'next/head' +import {ErrorComponent} from "@blitzjs/next" + +// ------------------------------------------------------ +// This page is rendered if a route match is not found +// ------------------------------------------------------ +export default function Page404() { + const statusCode = 404 + const title = "This page could not be found" + return ( + <> + + + {statusCode}: {title} + + + + + ) +} \ No newline at end of file diff --git a/examples/playwright/src/pages/_app.tsx b/examples/playwright/src/pages/_app.tsx new file mode 100644 index 0000000000..b1bfc7b879 --- /dev/null +++ b/examples/playwright/src/pages/_app.tsx @@ -0,0 +1,36 @@ +import { ErrorFallbackProps, ErrorComponent, ErrorBoundary, AppProps } from "@blitzjs/next" +import { AuthenticationError, AuthorizationError } from "blitz" +import React from "react" +import { withBlitz } from "src/blitz-client" +import 'src/styles/globals.css' + +function RootErrorFallback({ error }: ErrorFallbackProps) { + if (error instanceof AuthenticationError) { + return
Error: You are not authenticated
+ } else if (error instanceof AuthorizationError) { + return ( + + ) + } else { + return ( + + ) + } +} + +function MyApp({ Component, pageProps }: AppProps) { + const getLayout = Component.getLayout || ((page) => page) + return ( + + {getLayout()} + + ) +} + +export default withBlitz(MyApp) diff --git a/examples/playwright/src/pages/_document.tsx b/examples/playwright/src/pages/_document.tsx new file mode 100644 index 0000000000..0f85b9c6b8 --- /dev/null +++ b/examples/playwright/src/pages/_document.tsx @@ -0,0 +1,22 @@ +import Document, { Html, Main, NextScript, Head } from 'next/document'; + +class MyDocument extends Document { + // Only uncomment if you need to customize this behaviour + // static async getInitialProps(ctx: DocumentContext) { + // const initialProps = await Document.getInitialProps(ctx) + // return {...initialProps} + // } + render() { + return ( + + + +
+ + + + ); + } +} + +export default MyDocument; diff --git a/examples/playwright/src/pages/api/rpc/[[...blitz]].ts b/examples/playwright/src/pages/api/rpc/[[...blitz]].ts new file mode 100644 index 0000000000..c6096b0115 --- /dev/null +++ b/examples/playwright/src/pages/api/rpc/[[...blitz]].ts @@ -0,0 +1,4 @@ +import { rpcHandler } from "@blitzjs/rpc" +import { api } from "src/blitz-server" + +export default api(rpcHandler({ onError: console.log })) diff --git a/examples/playwright/src/pages/auth/forgot-password.tsx b/examples/playwright/src/pages/auth/forgot-password.tsx new file mode 100644 index 0000000000..def2d682c4 --- /dev/null +++ b/examples/playwright/src/pages/auth/forgot-password.tsx @@ -0,0 +1,46 @@ +import Layout from "src/core/layouts/Layout" +import { LabeledTextField } from "src/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "src/core/components/Form" +import { ForgotPassword } from "src/auth/schemas" +import forgotPassword from "src/auth/mutations/forgotPassword" +import { useMutation } from "@blitzjs/rpc" +import { BlitzPage } from "@blitzjs/next" + +const ForgotPasswordPage: BlitzPage = () => { + const [forgotPasswordMutation, { isSuccess }] = useMutation(forgotPassword) + + return ( + +

Forgot your password?

+ + {isSuccess ? ( +
+

Request Submitted

+

+ If your email is in our system, you will receive instructions to reset your password + shortly. +

+
+ ) : ( +
{ + try { + await forgotPasswordMutation(values) + } catch (error: any) { + return { + [FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.", + } + } + }} + > + + + )} +
+ ) +} + +export default ForgotPasswordPage diff --git a/examples/playwright/src/pages/auth/login.tsx b/examples/playwright/src/pages/auth/login.tsx new file mode 100644 index 0000000000..5f7316f302 --- /dev/null +++ b/examples/playwright/src/pages/auth/login.tsx @@ -0,0 +1,21 @@ +import { BlitzPage } from "@blitzjs/next" +import Layout from "src/core/layouts/Layout" +import { LoginForm } from "src/auth/components/LoginForm" +import { useRouter } from "next/router" + +const LoginPage: BlitzPage = () => { + const router = useRouter() + + return ( + + { + const next = router.query.next ? decodeURIComponent(router.query.next as string) : "/" + return router.push(next) + }} + /> + + ) +} + +export default LoginPage diff --git a/examples/playwright/src/pages/auth/reset-password.tsx b/examples/playwright/src/pages/auth/reset-password.tsx new file mode 100644 index 0000000000..bd1ba9f99f --- /dev/null +++ b/examples/playwright/src/pages/auth/reset-password.tsx @@ -0,0 +1,69 @@ +import Layout from "src/core/layouts/Layout" +import { LabeledTextField } from "src/core/components/LabeledTextField" +import { Form, FORM_ERROR } from "src/core/components/Form" +import { ResetPassword } from "src/auth/schemas" +import resetPassword from "src/auth/mutations/resetPassword" +import { BlitzPage, Routes } from "@blitzjs/next" +import { useRouter } from "next/router" +import { useMutation } from "@blitzjs/rpc" +import Link from "next/link" +import { assert } from "blitz" + +const ResetPasswordPage: BlitzPage = () => { + const router = useRouter() + const token = router.query.token?.toString() + const [resetPasswordMutation, { isSuccess }] = useMutation(resetPassword) + + return ( +
+

Set a New Password

+ + {isSuccess ? ( +
+

Password Reset Successfully

+

+ Go to the homepage +

+
+ ) : ( +
{ + try { + assert(token, "token is required.") + await resetPasswordMutation({ ...values, token }) + } catch (error: any) { + if (error.name === "ResetPasswordError") { + return { + [FORM_ERROR]: error.message, + } + } else { + return { + [FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.", + } + } + } + }} + > + + + + )} +
+ ) +} + +ResetPasswordPage.redirectAuthenticatedTo = "/" +ResetPasswordPage.getLayout = (page) => {page} + +export default ResetPasswordPage diff --git a/examples/playwright/src/pages/auth/signup.tsx b/examples/playwright/src/pages/auth/signup.tsx new file mode 100644 index 0000000000..fd6b94df6c --- /dev/null +++ b/examples/playwright/src/pages/auth/signup.tsx @@ -0,0 +1,16 @@ +import { useRouter } from "next/router" +import Layout from "src/core/layouts/Layout" +import { SignupForm } from "src/auth/components/SignupForm" +import { BlitzPage, Routes } from "@blitzjs/next" + +const SignupPage: BlitzPage = () => { + const router = useRouter() + + return ( + + router.push(Routes.Home())} /> + + ) +} + +export default SignupPage diff --git a/examples/playwright/src/pages/index.tsx b/examples/playwright/src/pages/index.tsx new file mode 100644 index 0000000000..aedf8a4b47 --- /dev/null +++ b/examples/playwright/src/pages/index.tsx @@ -0,0 +1,197 @@ +import { Suspense } from "react" +import Link from "next/link" +import Layout from "src/core/layouts/Layout" +import { useCurrentUser } from "src/users/hooks/useCurrentUser" +import logout from "src/auth/mutations/logout" +import { useMutation } from "@blitzjs/rpc" +import { Routes, BlitzPage } from "@blitzjs/next" +import styles from 'src/styles/Home.module.css' + +/* + * This file is just for a pleasant getting started page for your new app. + * You can delete everything in here and start from scratch if you like. + */ + +const UserInfo = () => { + const currentUser = useCurrentUser() + const [logoutMutation] = useMutation(logout) + + if (currentUser) { + return ( + <> + +
+ User id: {currentUser.id} +
+ User role: {currentUser.role} +
+ + ) + } else { + return ( + <> + + Sign Up + + + Login + + + ) + } +} + +const Home: BlitzPage = () => { + return ( + +
+ +
+
+

+ Congrats! Your app is ready, including user sign-up and log-in. +

+
+ +
+ +
+
+ +
+ +
+ +

Your database & authentication is ready. Try it by signing up.

+ + {/* Auth */} + +
+ + + +
+ + +
+ +
+ {/* Instructions */} +
+

+ + Add a new model by running the following in your terminal: + +

+ +
+
+ 1 +
+                      blitz generate all project name:string
+                    
+
+ +
+ 2 +
+                      Ctrl + c
+                    
+
+ +
+ 3 +
+                      blitz dev
+                    
+
+ +
+ 4 +
+                      Go to{" "}
+                    
+                      /projects
+                    
+                    
+
+ +
+
+ {/* Links */} + + +
+
+
+ + +
+ + ) +} + +export default Home diff --git a/examples/playwright/src/pages/recipes/[recipeId].tsx b/examples/playwright/src/pages/recipes/[recipeId].tsx new file mode 100644 index 0000000000..ff53a504e4 --- /dev/null +++ b/examples/playwright/src/pages/recipes/[recipeId].tsx @@ -0,0 +1,65 @@ +import { Suspense } from "react" +import { Routes } from "@blitzjs/next" +import Head from "next/head" +import Link from "next/link" +import { useRouter } from "next/router" +import { useQuery, useMutation } from "@blitzjs/rpc" +import { useParam } from "@blitzjs/next" + +import Layout from "src/core/layouts/Layout" +import getRecipe from "src/recipes/queries/getRecipe" +import deleteRecipe from "src/recipes/mutations/deleteRecipe" + +export const Recipe = () => { + const router = useRouter() + const recipeId = useParam("recipeId", "number") + const [deleteRecipeMutation] = useMutation(deleteRecipe) + const [recipe] = useQuery(getRecipe, { id: recipeId }) + + return ( + <> + + Recipe {recipe.id} + + +
+

Recipe {recipe.id}

+
{JSON.stringify(recipe, null, 2)}
+ + Edit + + +
+ + ) +} + +const ShowRecipePage = () => { + return ( +
+

+ Recipes +

+ + Loading...
}> + + +
+ ) +} + +ShowRecipePage.authenticate = true +ShowRecipePage.getLayout = (page) => {page} + +export default ShowRecipePage diff --git a/examples/playwright/src/pages/recipes/[recipeId]/edit.tsx b/examples/playwright/src/pages/recipes/[recipeId]/edit.tsx new file mode 100644 index 0000000000..23db217be9 --- /dev/null +++ b/examples/playwright/src/pages/recipes/[recipeId]/edit.tsx @@ -0,0 +1,80 @@ +import { Suspense } from "react" +import { Routes } from "@blitzjs/next" +import Head from "next/head" +import Link from "next/link" +import { useRouter } from "next/router" +import { useQuery, useMutation } from "@blitzjs/rpc" +import { useParam } from "@blitzjs/next" + +import Layout from "src/core/layouts/Layout" +import { UpdateRecipeSchema } from "src/recipes/schemas" +import getRecipe from "src/recipes/queries/getRecipe" +import updateRecipe from "src/recipes/mutations/updateRecipe" +import { RecipeForm, FORM_ERROR } from "src/recipes/components/RecipeForm" + +export const EditRecipe = () => { + const router = useRouter() + const recipeId = useParam("recipeId", "number") + const [recipe, { setQueryData }] = useQuery( + getRecipe, + { id: recipeId }, + { + // This ensures the query never refreshes and overwrites the form data while the user is editing. + staleTime: Infinity, + } + ) + const [updateRecipeMutation] = useMutation(updateRecipe) + + return ( + <> + + Edit Recipe {recipe.id} + + +
+

Edit Recipe {recipe.id}

+
{JSON.stringify(recipe, null, 2)}
+ Loading...
}> + { + try { + const updated = await updateRecipeMutation({ + ...values, + }) + await setQueryData(updated) + await router.push(Routes.ShowRecipePage({ recipeId: updated.id })) + } catch (error: any) { + console.error(error) + return { + [FORM_ERROR]: error.toString(), + } + } + }} + /> + + + + ) +} + +const EditRecipePage = () => { + return ( +
+ Loading...
}> + + + +

+ Recipes +

+ + ) +} + +EditRecipePage.authenticate = true +EditRecipePage.getLayout = (page) => {page} + +export default EditRecipePage diff --git a/examples/playwright/src/pages/recipes/index.tsx b/examples/playwright/src/pages/recipes/index.tsx new file mode 100644 index 0000000000..431ae5e7f9 --- /dev/null +++ b/examples/playwright/src/pages/recipes/index.tsx @@ -0,0 +1,64 @@ +import { Suspense } from "react" +import { Routes } from "@blitzjs/next" +import Head from "next/head" +import Link from "next/link" +import { usePaginatedQuery } from "@blitzjs/rpc" +import { useRouter } from "next/router" +import Layout from "src/core/layouts/Layout" +import getRecipes from "src/recipes/queries/getRecipes" + +const ITEMS_PER_PAGE = 100 + +export const RecipesList = () => { + const router = useRouter() + const page = Number(router.query.page) || 0 + const [{ recipes, hasMore }] = usePaginatedQuery(getRecipes, { + orderBy: { id: "asc" }, + skip: ITEMS_PER_PAGE * page, + take: ITEMS_PER_PAGE, + }) + + const goToPreviousPage = () => router.push({ query: { page: page - 1 } }) + const goToNextPage = () => router.push({ query: { page: page + 1 } }) + + return ( +
+
    + {recipes.map((recipe) => ( +
  • + {recipe.name} +
  • + ))} +
+ + + +
+ ) +} + +const RecipesPage = () => { + return ( + + + Recipes + + +
+

+ Create Recipe +

+ + Loading...
}> + + + +
+ ) +} + +export default RecipesPage diff --git a/examples/playwright/src/pages/recipes/new.tsx b/examples/playwright/src/pages/recipes/new.tsx new file mode 100644 index 0000000000..9653464e00 --- /dev/null +++ b/examples/playwright/src/pages/recipes/new.tsx @@ -0,0 +1,45 @@ +import { Routes } from "@blitzjs/next" +import Link from "next/link" +import { useRouter } from "next/router" +import { useMutation } from "@blitzjs/rpc" +import Layout from "src/core/layouts/Layout" +import { CreateRecipeSchema } from "src/recipes/schemas" +import createRecipe from "src/recipes/mutations/createRecipe" +import { RecipeForm, FORM_ERROR } from "src/recipes/components/RecipeForm" +import { Suspense } from "react" + +const NewRecipePage = () => { + const router = useRouter() + const [createRecipeMutation] = useMutation(createRecipe) + + return ( + +

Create New Recipe

+ Loading...}> + { + try { + const recipe = await createRecipeMutation(values) + await router.push(Routes.ShowRecipePage({ recipeId: recipe.id })) + } catch (error: any) { + console.error(error) + return { + [FORM_ERROR]: error.toString(), + } + } + }} + /> + +

+ Recipes +

+
+ ) +} + +NewRecipePage.authenticate = true + +export default NewRecipePage diff --git a/examples/playwright/src/recipes/components/RecipeForm.tsx b/examples/playwright/src/recipes/components/RecipeForm.tsx new file mode 100644 index 0000000000..026820ec11 --- /dev/null +++ b/examples/playwright/src/recipes/components/RecipeForm.tsx @@ -0,0 +1,15 @@ +import React from "react" +import { Form, FormProps } from "src/core/components/Form" +import { LabeledTextField } from "src/core/components/LabeledTextField" + +import { z } from "zod" +export { FORM_ERROR } from "src/core/components/Form" + +export function RecipeForm>(props: FormProps) { + return ( + {...props}> + + {/* template: <__component__ name="__fieldName__" label="__Field_Name__" placeholder="__Field_Name__" type="__inputType__" /> */} + + ) +} diff --git a/examples/playwright/src/recipes/mutations/createRecipe.ts b/examples/playwright/src/recipes/mutations/createRecipe.ts new file mode 100644 index 0000000000..d4e3d94d91 --- /dev/null +++ b/examples/playwright/src/recipes/mutations/createRecipe.ts @@ -0,0 +1,14 @@ +import { resolver } from "@blitzjs/rpc" +import db from "db" +import { CreateRecipeSchema } from "../schemas" + +export default resolver.pipe( + resolver.zod(CreateRecipeSchema), + resolver.authorize(), + async (input) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const recipe = await db.recipe.create({ data: input }) + + return recipe + } +) diff --git a/examples/playwright/src/recipes/mutations/deleteRecipe.ts b/examples/playwright/src/recipes/mutations/deleteRecipe.ts new file mode 100644 index 0000000000..36d03f681a --- /dev/null +++ b/examples/playwright/src/recipes/mutations/deleteRecipe.ts @@ -0,0 +1,14 @@ +import { resolver } from "@blitzjs/rpc" +import db from "db" +import { DeleteRecipeSchema } from "../schemas" + +export default resolver.pipe( + resolver.zod(DeleteRecipeSchema), + resolver.authorize(), + async ({ id }) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const recipe = await db.recipe.deleteMany({ where: { id } }) + + return recipe + } +) diff --git a/examples/playwright/src/recipes/mutations/updateRecipe.ts b/examples/playwright/src/recipes/mutations/updateRecipe.ts new file mode 100644 index 0000000000..b2a2b57c0b --- /dev/null +++ b/examples/playwright/src/recipes/mutations/updateRecipe.ts @@ -0,0 +1,14 @@ +import { resolver } from "@blitzjs/rpc" +import db from "db" +import { UpdateRecipeSchema } from "../schemas" + +export default resolver.pipe( + resolver.zod(UpdateRecipeSchema), + resolver.authorize(), + async ({ id, ...data }) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const recipe = await db.recipe.update({ where: { id }, data }) + + return recipe + } +) diff --git a/examples/playwright/src/recipes/queries/getRecipe.ts b/examples/playwright/src/recipes/queries/getRecipe.ts new file mode 100644 index 0000000000..dfcd905707 --- /dev/null +++ b/examples/playwright/src/recipes/queries/getRecipe.ts @@ -0,0 +1,18 @@ +import { NotFoundError } from "blitz" +import { resolver } from "@blitzjs/rpc" +import db from "db" +import { z } from "zod" + +const GetRecipe = z.object({ + // This accepts type of undefined, but is required at runtime + id: z.number().optional().refine(Boolean, "Required"), +}) + +export default resolver.pipe(resolver.zod(GetRecipe), resolver.authorize(), async ({ id }) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const recipe = await db.recipe.findFirst({ where: { id } }) + + if (!recipe) throw new NotFoundError() + + return recipe +}) diff --git a/examples/playwright/src/recipes/queries/getRecipes.ts b/examples/playwright/src/recipes/queries/getRecipes.ts new file mode 100644 index 0000000000..3735845a67 --- /dev/null +++ b/examples/playwright/src/recipes/queries/getRecipes.ts @@ -0,0 +1,31 @@ +import { paginate } from "blitz" +import { resolver } from "@blitzjs/rpc" +import db, { Prisma } from "db" + +interface GetRecipesInput + extends Pick {} + +export default resolver.pipe( + resolver.authorize(), + async ({ where, orderBy, skip = 0, take = 100 }: GetRecipesInput) => { + // TODO: in multi-tenant app, you must add validation to ensure correct tenant + const { + items: recipes, + hasMore, + nextPage, + count, + } = await paginate({ + skip, + take, + count: () => db.recipe.count({ where }), + query: (paginateArgs) => db.recipe.findMany({ ...paginateArgs, where, orderBy }), + }) + + return { + recipes, + nextPage, + hasMore, + count, + } + } +) diff --git a/examples/playwright/src/recipes/schemas.ts b/examples/playwright/src/recipes/schemas.ts new file mode 100644 index 0000000000..a55e3fed4f --- /dev/null +++ b/examples/playwright/src/recipes/schemas.ts @@ -0,0 +1,14 @@ +import { z } from "zod" + +export const CreateRecipeSchema = z.object({ + name: z.string(), + // template: __fieldName__: z.__zodType__(), +}) +export const UpdateRecipeSchema = z.object({ + id: z.number(), + // template: __fieldName__: z.__zodType__(), +}) + +export const DeleteRecipeSchema = z.object({ + id: z.number(), +}) diff --git a/examples/playwright/src/styles/Home.module.css b/examples/playwright/src/styles/Home.module.css new file mode 100644 index 0000000000..4aebc92f9c --- /dev/null +++ b/examples/playwright/src/styles/Home.module.css @@ -0,0 +1,317 @@ +.container { + display: flex; + flex-direction: column; + height: 100vh; +} + +.main { + flex: 1; + padding: 0rem 1rem; + display: flex; + flex-direction: column; + justify-content: space-evenly; +} + +.wrapper { + display: flex; + flex-direction: column; + gap: 2rem; +} + + +.frost { + background: rgba(255, 255, 255, 0.2); + border-radius: 16px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.31); +} + + +.header { + display: flex; + flex-direction: column; + padding: 4rem 0rem 2rem 0rem; + text-align: center; + gap: 2rem; +} + +.header h1 { + max-width: 620px; + align-self: center; + +} + +.body { + composes: frost; + border-radius: var(--border-radius); + display: flex; + padding: 1rem; + width: 100%; + flex-direction: row; + align-self: center; + max-width: calc(min(var(--screen-width),800px)); +} + + +.instructions { + display: inline-flex; + flex-direction: column; + padding: 1rem; + width: 100%; +} + +.globe { + position: fixed; + width: 350vmin; + height: 75vmin; + left: 20%; + top: 50%; + transform: translate(-50%, calc(-50% + 40px)); + z-index: -1; + border-radius: 100%; + background-image: radial-gradient(95.63% 95.63% at 95.92% 0%, rgba(255, 255, 255, 0.62) 0%, #8155ff38 60.42%, #002fff5c 169%); + filter: blur(8vmin); +} + +.footer { + display: flex; + padding: 2rem 0; + justify-content: center; + align-items: center; +} + +.footer span { + margin-right: 0.2rem; +} + +.code { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.code span { + background: rgba(124, 58, 237, 50%); + border-radius: 50rem; + font-size: 14px; + font-weight: 500; + padding: 17px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + color: rgb(57, 33, 97); +} + +.code pre { + background: rgba(124, 58, 237, 12%); + border-radius: 4px; + padding: 0.7em 1.4em; + text-align: center; +} + +.code code { + font-size: 0.86em; + font-weight: bold; + color: rgb(124, 58, 237); + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, + Bitstream Vera Sans Mono, Courier New, monospace; +} + +.toastContainer { + border: 1px solid #edff; + padding: 0 1rem; + background: #eeff; + color: #62af; + text-align: center; +} + +.toastContainer strong { + color: rgb(124, 58, 237); +} + +.textLink { + color: rgb(124, 58, 237); + background: + linear-gradient( + to right, + rgba(231, 216, 246, 1), + rgba(231, 216, 246, 1) + ), + linear-gradient( + to right, + rgba(99, 1, 235, 1), + rgba(124, 58, 237, 1), + rgba(231, 216, 246, 1) + ); + background-size: 100% 1px, 0 1px; + background-position: 100% 100%, 0 100%; + background-repeat: no-repeat; + transition: background-size 400ms; +} + +.textLink:hover, +.textLink:focus, +.textLink:active { + background-size: 0 1px, 100% 1px; +} + +.arrowIcon { + box-sizing: border-box; + display: block; + width: 8px; + height: 8px; + border-top: 2px solid; + transform: scale(var(--ggs,1)); + border-right: 2px solid; + position: absolute; + right: 6px; + top: 6px; + color:#b1a5c4; +} + +.arrowIcon::after { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 8px; + height: 2px; + background: currentColor; + transform: rotate(-45deg); + top: 2px; + right: -1px +} + +.buttonContainer { + display: flex; + flex-direction: row; + gap: 1rem; + justify-content: center; + align-items: center; + flex: 1; +} + +.button { + background: linear-gradient(to top, rgb(124, 58, 237), rgb(117, 81, 236)); + border: 1px solid rgb(231, 216, 246); + color: white; + text-shadow: rgba(0, 0, 0, 0.25) 0px 3px 8px; + padding: 0 24px; + height: 48px; + width: 200px; + max-width: 300px; + position: relative; + display: inline-flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + user-select: none; + white-space: nowrap; + border-radius: 0.75rem; + border-bottom-left-radius: 0px; + font-size: 15px; + transition: all 0.3s ease 0s; + cursor: pointer; +} + +.button:hover { + color: white; + text-shadow: rgb(0 0 0 / 56%) 0px 3px 12px; + box-shadow: rgb(80 63 205 / 50%) 0px 1px 40px; +} + +.loginButton { + composes: button; + background: rgb(248 250 252); + border: 1px solid rgb(231, 216, 246); + color: rgb(30 41 59); + text-shadow: none; +} + +.loginButton:hover { + color: rgb(30 41 59); + text-shadow: none; +} + +.card:hover .arrowIcon { + color: #7450ec; +} + +.linkGrid { + display: flex; + flex-direction: column; + flex-wrap: wrap; + gap: 1rem; +} + + +.card { + composes: frost; + padding: 1rem 0rem; + text-align: center; + color: inherit; + text-decoration: none; + border-radius: 10px; + border-bottom-left-radius: 0px; + transition: color 0.15s ease, border-color 0.15s ease; + max-width: 200px; + min-width: 200px; + display: flex; + flex-direction: row; + justify-content: center; +} + +.card:hover, +.card:focus, +.card:active { + color: #7450ec; + border-color: #7450ec; +} + +.card h2 { + margin: 0 0 1rem 0; + font-size: 1.5rem; +} + +.card p { + margin: 0; + font-size: 1.25rem; + line-height: 1.5; +} + +.logo { + flex: 1; + padding: 1rem 2rem; +} + +.logo svg { + height: 100%; + width: 200px; + fill: #7450ec; +} + + +/* MOBILE */ +@media(max-width:800px) { + .linkGrid { + width: 100%; + } + + .card { + max-width: 100%; + } + + .body { + flex-wrap: wrap; + } + + .buttonContainer { + flex-wrap: wrap; + } + +} + diff --git a/examples/playwright/src/styles/globals.css b/examples/playwright/src/styles/globals.css new file mode 100644 index 0000000000..98e2e157d1 --- /dev/null +++ b/examples/playwright/src/styles/globals.css @@ -0,0 +1,26 @@ +:root { + --border-radius: 0.75rem; + --screen-width: 90vmin; +} + + +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +i { + font-size: 0.8rem; +} + +* { + box-sizing: border-box; +} \ No newline at end of file diff --git a/examples/playwright/src/users/hooks/useCurrentUser.ts b/examples/playwright/src/users/hooks/useCurrentUser.ts new file mode 100644 index 0000000000..5439c60ae8 --- /dev/null +++ b/examples/playwright/src/users/hooks/useCurrentUser.ts @@ -0,0 +1,7 @@ +import { useQuery } from "@blitzjs/rpc" +import getCurrentUser from "src/users/queries/getCurrentUser" + +export const useCurrentUser = () => { + const [user] = useQuery(getCurrentUser, null) + return user +} diff --git a/examples/playwright/src/users/queries/getCurrentUser.ts b/examples/playwright/src/users/queries/getCurrentUser.ts new file mode 100644 index 0000000000..560097d0d0 --- /dev/null +++ b/examples/playwright/src/users/queries/getCurrentUser.ts @@ -0,0 +1,13 @@ +import { Ctx } from 'blitz' +import db from 'db' + +export default async function getCurrentUser(_ = null, { session }: Ctx) { + if (!session.userId) return null + + const user = await db.user.findFirst({ + where: { id: session.userId }, + select: { id: true, name: true, email: true, role: true }, + }) + + return user +} diff --git a/examples/playwright/test/e2e/auth.spec.ts b/examples/playwright/test/e2e/auth.spec.ts new file mode 100644 index 0000000000..586def7b13 --- /dev/null +++ b/examples/playwright/test/e2e/auth.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from "@playwright/test" + +// overwrites global-setup auth action +test.use({ storageState: { cookies: [], origins: [] } }) + +test.beforeEach(async ({ page }) => { + await page.goto("/") + await expect(page).toHaveTitle(/Home/) +}) +test("login", async ({ page }) => { + await page.getByText("Login").click() + await expect(page).toHaveTitle(/Log In/) + + await page.locator("input[name='email']").fill("e2e@user.de") + await page.locator("input[name='password']").fill("e2euserpassword") + + await page.getByRole("button").click() + + await expect(page.getByText("Logout")).toBeDefined() +}) + +test("user account can be created", async ({ page, browserName }) => { + await page.getByText("Sign Up").click() + await expect(page).toHaveTitle("Sign Up") + + await page.locator("input[name='email']").fill(`${browserName}@user.de`) + await page.locator("input[name='password']").fill("e2e-password") + + await page.getByRole("button").click() + + await expect(page.getByText("Logout")).toBeDefined() +}) + +test("user can logout after login", async ({ page }) => { + await page.getByText("Login").click() + await page.locator("input[name='email']").fill("e2e@user.de") + await page.locator("input[name='password']").fill("e2euserpassword") + + await page.getByRole("button").click() + + await expect(page.getByText("Logout")).toBeDefined() + + await page.getByText("Logout").click() + + await page.goto("/recipes/new") + + await expect(page.getByText("Error: You are not authenticated")).toBeDefined() +}) diff --git a/examples/playwright/test/e2e/global-setup.ts b/examples/playwright/test/e2e/global-setup.ts new file mode 100644 index 0000000000..eafad5d49a --- /dev/null +++ b/examples/playwright/test/e2e/global-setup.ts @@ -0,0 +1,20 @@ +// global-setup.ts +import { chromium, expect, FullConfig } from "@playwright/test" + +async function globalSetup(config: FullConfig) { + if (!config.projects[0]) throw new Error("You need to define projects") + const { baseURL, storageState } = config.projects[0].use + + const browser = await chromium.launch() + const page = await browser.newPage() + await page.goto(`${baseURL}/auth/login`) + await page.locator("input[name='email']").fill("e2e@user.de") + await page.locator("input[name='password']").fill("e2euserpassword") + await page.getByRole("button").click() + await expect(page.locator("button", { hasText: "Logout" })).toBeDefined() + + await page.context().storageState({ path: storageState as string }) + await browser.close() +} + +export default globalSetup diff --git a/examples/playwright/test/e2e/recipes.spec.ts b/examples/playwright/test/e2e/recipes.spec.ts new file mode 100644 index 0000000000..1cf0618bd3 --- /dev/null +++ b/examples/playwright/test/e2e/recipes.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from "@playwright/test" +import { Routes } from "@blitzjs/next" + +test.describe("test recipes", () => { + test.beforeEach(async ({ page }) => { + await page.goto(Routes.RecipesPage().href) + await expect(page.getByText("Loading...")).toBeHidden() + }) + + test("Should be able to create a new recipe", async ({ page }) => { + await page.getByText("Create Recipe").click() + await page.locator('input[name="name"]').type("Added Recipe") + await page.getByRole("button").click() + + await expect(page.getByText("Added Recipe")).toBeDefined() + }) +}) diff --git a/examples/playwright/test/unit/index.test.tsx b/examples/playwright/test/unit/index.test.tsx new file mode 100644 index 0000000000..f899f6e66f --- /dev/null +++ b/examples/playwright/test/unit/index.test.tsx @@ -0,0 +1,32 @@ +/** + * @vitest-environment jsdom + */ + +import { expect, vi, test } from "vitest" +import { render } from "test/unit/utils" + +import Home from "../../src/pages" + +vi.mock("public/logo.png", () => ({ + default: { src: "/logo.png" }, +})) + +test.skip("renders blitz documentation link", () => { + // This is an example of how to ensure a specific item is in the document + // But it's disabled by default (by test.skip) so the test doesn't fail + // when you remove the default content from the page + + // This is an example on how to mock api hooks when testing + vi.mock("src/users/hooks/useCurrentUser", () => ({ + useCurrentUser: () => ({ + id: 1, + name: "User", + email: "user@email.com", + role: "user", + }), + })) + + const { getByText } = render() + const linkElement = getByText(/Blitz Docs/i) + expect(linkElement).toBeInTheDocument() +}) diff --git a/examples/playwright/test/unit/setup.ts b/examples/playwright/test/unit/setup.ts new file mode 100644 index 0000000000..2335fec51e --- /dev/null +++ b/examples/playwright/test/unit/setup.ts @@ -0,0 +1,3 @@ +import "@testing-library/jest-dom" + +export {} diff --git a/examples/playwright/test/unit/utils.tsx b/examples/playwright/test/unit/utils.tsx new file mode 100644 index 0000000000..73f64efeaa --- /dev/null +++ b/examples/playwright/test/unit/utils.tsx @@ -0,0 +1,106 @@ +import { vi } from "vitest" +import { render as defaultRender } from "@testing-library/react" +import { renderHook as defaultRenderHook } from "@testing-library/react-hooks" +import { NextRouter } from "next/router" +import { BlitzProvider, RouterContext } from "@blitzjs/next" +import { QueryClient } from "@blitzjs/rpc" + +export * from "@testing-library/react" + +// -------------------------------------------------------------------------------- +// This file customizes the render() and renderHook() test functions provided +// by React testing library. It adds a router context wrapper with a mocked router. +// +// You should always import `render` and `renderHook` from this file +// +// This is the place to add any other context providers you need while testing. +// -------------------------------------------------------------------------------- + +// -------------------------------------------------- +// render() +// -------------------------------------------------- +// Override the default test render with our own +// +// You can override the router mock like this: +// +// const { baseElement } = render(, { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- + +const queryClient = new QueryClient() +export function render( + ui: RenderUI, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ) + } + return defaultRender(ui, { wrapper, ...options }) +} + +// -------------------------------------------------- +// renderHook() +// -------------------------------------------------- +// Override the default test renderHook with our own +// +// You can override the router mock like this: +// +// const result = renderHook(() => myHook(), { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function renderHook( + hook: RenderHook, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ) + } + return defaultRenderHook(hook, { wrapper, ...options }) +} + +export const mockRouter: NextRouter = { + basePath: "", + pathname: "/", + route: "/", + asPath: "/", + query: {}, + isReady: true, + isLocaleDomain: false, + isPreview: false, + forward: vi.fn(), + push: vi.fn(), + replace: vi.fn(), + reload: vi.fn(), + back: vi.fn(), + prefetch: vi.fn(), + beforePopState: vi.fn(), + events: { + on: vi.fn(), + off: vi.fn(), + emit: vi.fn(), + }, + isFallback: false, +} + +type DefaultParams = Parameters +type RenderUI = DefaultParams[0] +type RenderOptions = DefaultParams[1] & { router?: Partial; dehydratedState?: unknown } + +type DefaultHookParams = Parameters +type RenderHook = DefaultHookParams[0] diff --git a/examples/playwright/tsconfig.json b/examples/playwright/tsconfig.json new file mode 100644 index 0000000000..471cee2633 --- /dev/null +++ b/examples/playwright/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": ".", + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "exclude": ["node_modules", "**/*.e2e.ts", "cypress"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types"] +} diff --git a/examples/playwright/types.ts b/examples/playwright/types.ts new file mode 100644 index 0000000000..bead91f252 --- /dev/null +++ b/examples/playwright/types.ts @@ -0,0 +1,14 @@ +import { SimpleRolesIsAuthorized } from "@blitzjs/auth" +import { User } from "db" + +export type Role = "ADMIN" | "USER" + +declare module "@blitzjs/auth" { + export interface Session { + isAuthorized: SimpleRolesIsAuthorized + PublicData: { + userId: User["id"] + role: Role + } + } +} diff --git a/examples/playwright/vitest.config.ts b/examples/playwright/vitest.config.ts new file mode 100644 index 0000000000..706430be5f --- /dev/null +++ b/examples/playwright/vitest.config.ts @@ -0,0 +1,22 @@ +import { loadEnvConfig } from "@next/env"; +import { defineConfig } from "vitest/config"; + +import react from '@vitejs/plugin-react' +import tsconfigPaths from 'vite-tsconfig-paths' + + +const projectDir = process.cwd(); +loadEnvConfig(projectDir); + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), tsconfigPaths()], + test: { + dir: "./", + globals: true, + setupFiles: './test/setup.ts', + coverage: { + reporter: ['text', 'json', 'html'], + }, + } +}) \ No newline at end of file diff --git a/examples/static/.env b/examples/static/.env new file mode 100644 index 0000000000..5cd1aca530 --- /dev/null +++ b/examples/static/.env @@ -0,0 +1,3 @@ +# This env file should be checked into source control +# This is the place for default values for all environments +# Values in `.env.local` and `.env.production` will override these values diff --git a/examples/static/.eslintrc.js b/examples/static/.eslintrc.js new file mode 100644 index 0000000000..f845b10d52 --- /dev/null +++ b/examples/static/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["blitz"], +} diff --git a/examples/static/.gitignore b/examples/static/.gitignore new file mode 100644 index 0000000000..3665bf5ce0 --- /dev/null +++ b/examples/static/.gitignore @@ -0,0 +1,52 @@ +# dependencies +node_modules +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* +.npm +web_modules/ + +# blitz +/.blitz/ +/.next/ +*.sqlite +.now +.blitz-console-history +blitz-log.log + +# misc +.DS_Store + +# local env files +.env.local +.env.*.local +.envrc + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Testing +.coverage +*.lcov +.nyc_output +lib-cov + +# Caches +*.tsbuildinfo +.eslintcache +.node_repl_history +.yarn-integrity + +# Serverless directories +.serverless/ + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test diff --git a/examples/static/.npmrc b/examples/static/.npmrc new file mode 100644 index 0000000000..1b78f1c6f2 --- /dev/null +++ b/examples/static/.npmrc @@ -0,0 +1,2 @@ +save-exact=true +legacy-peer-deps=true diff --git a/examples/static/.prettierignore b/examples/static/.prettierignore new file mode 100644 index 0000000000..ad8c486b66 --- /dev/null +++ b/examples/static/.prettierignore @@ -0,0 +1,7 @@ +.gitkeep +.env* +*.ico +*.lock +db/migrations +.next +.blitz diff --git a/examples/static/README.md b/examples/static/README.md new file mode 100644 index 0000000000..e83bed7e82 --- /dev/null +++ b/examples/static/README.md @@ -0,0 +1,3 @@ +# Static + +This is an example to demonstrate usage of `blitz export`. diff --git a/examples/static/app/core/layouts/Layout.tsx b/examples/static/app/core/layouts/Layout.tsx new file mode 100644 index 0000000000..d3df320238 --- /dev/null +++ b/examples/static/app/core/layouts/Layout.tsx @@ -0,0 +1,16 @@ +import { Head, BlitzLayout } from "blitz" + +const Layout: BlitzLayout<{ title?: string }> = ({ title, children }) => { + return ( + <> + + {title || "static"} + + + + {children} + + ) +} + +export default Layout diff --git a/examples/static/app/pages/404.tsx b/examples/static/app/pages/404.tsx new file mode 100644 index 0000000000..ab81189e6c --- /dev/null +++ b/examples/static/app/pages/404.tsx @@ -0,0 +1,19 @@ +import { Head, ErrorComponent } from "blitz" + +// ------------------------------------------------------ +// This page is rendered if a route match is not found +// ------------------------------------------------------ +export default function Page404() { + const statusCode = 404 + const title = "This page could not be found" + return ( + <> + + + {statusCode}: {title} + + + + + ) +} diff --git a/examples/static/app/pages/_app.tsx b/examples/static/app/pages/_app.tsx new file mode 100644 index 0000000000..eb0bf78739 --- /dev/null +++ b/examples/static/app/pages/_app.tsx @@ -0,0 +1,22 @@ +import { + AppProps, + ErrorBoundary, + ErrorComponent, + ErrorFallbackProps, + useQueryErrorResetBoundary, +} from "blitz" + +export default function App({ Component, pageProps }: AppProps) { + const getLayout = Component.getLayout || ((page) => page) + const { reset } = useQueryErrorResetBoundary() + + return ( + + {getLayout()} + + ) +} + +function RootErrorFallback({ error }: ErrorFallbackProps) { + return +} diff --git a/examples/static/app/pages/_document.tsx b/examples/static/app/pages/_document.tsx new file mode 100644 index 0000000000..78b9fc4d99 --- /dev/null +++ b/examples/static/app/pages/_document.tsx @@ -0,0 +1,23 @@ +import { Document, Html, DocumentHead, Main, BlitzScript /*DocumentContext*/ } from "blitz" + +class MyDocument extends Document { + // Only uncomment if you need to customize this behaviour + // static async getInitialProps(ctx: DocumentContext) { + // const initialProps = await Document.getInitialProps(ctx) + // return {...initialProps} + // } + + render() { + return ( + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/static/app/pages/index.test.tsx b/examples/static/app/pages/index.test.tsx new file mode 100644 index 0000000000..1d05dda0ea --- /dev/null +++ b/examples/static/app/pages/index.test.tsx @@ -0,0 +1,14 @@ +import React from "react" +import { render } from "test/utils" + +import Home from "./index" + +test.skip("renders blitz documentation link", () => { + // This is an example of how to ensure a specific item is in the document + // But it's disabled by default (by test.skip) so the test doesn't fail + // when you remove the the default content from the page + + const { getByText } = render() + const linkElement = getByText(/Documentation/i) + expect(linkElement).toBeInTheDocument() +}) diff --git a/examples/static/app/pages/index.tsx b/examples/static/app/pages/index.tsx new file mode 100644 index 0000000000..75bd6e1583 --- /dev/null +++ b/examples/static/app/pages/index.tsx @@ -0,0 +1,191 @@ +import { BlitzPage, Image } from "blitz" +import Layout from "app/core/layouts/Layout" +import logo from "public/logo.png" + +const Home: BlitzPage = () => { + return ( +
+
+
+ blitz.js +
+

+ Congrats! Your static app is ready. +

+ +
+ + + + +
+ ) +} + +Home.getLayout = (page) => {page} + +export default Home diff --git a/examples/static/babel.config.js b/examples/static/babel.config.js new file mode 100644 index 0000000000..dfdf62cea1 --- /dev/null +++ b/examples/static/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["blitz/babel"], + plugins: [], +} diff --git a/examples/static/blitz.config.js b/examples/static/blitz.config.js new file mode 100644 index 0000000000..1554aa5af5 --- /dev/null +++ b/examples/static/blitz.config.js @@ -0,0 +1,11 @@ +module.exports = { + middleware: [], + /* Uncomment this to customize the webpack config + webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + // Important: return the modified config + return config + }, + */ +} diff --git a/examples/static/jest.config.js b/examples/static/jest.config.js new file mode 100644 index 0000000000..297905551f --- /dev/null +++ b/examples/static/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: "blitz", +} diff --git a/examples/static/package.json b/examples/static/package.json new file mode 100644 index 0000000000..16fa6ddb06 --- /dev/null +++ b/examples/static/package.json @@ -0,0 +1,55 @@ +{ + "name": "@examples/static", + "version": "0.34.0-canary.0", + "scripts": { + "dev": "blitz dev", + "build": "blitz build", + "start": "blitz start", + "studio": "blitz prisma studio", + "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", + "test": "jest", + "test:watch": "jest --watch" + }, + "browserslist": [ + "defaults" + ], + "prisma": { + "schema": "db/schema.prisma" + }, + "prettier": { + "semi": false, + "printWidth": 100 + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged && pretty-quick --staged", + "pre-push": "tsc && npm run lint && npm run test" + } + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "eslint --fix" + ] + }, + "dependencies": { + "@prisma/client": "2.24.1", + "blitz": "0.45.4", + "final-form": "4.20.1", + "prisma": "2.24.1", + "react": "0.0.0-experimental-6a589ad71", + "react-dom": "0.0.0-experimental-6a589ad71", + "react-final-form": "6.5.2" + }, + "devDependencies": { + "@types/preview-email": "2.0.0", + "@types/react": "17.0.2", + "eslint": "7.21.0", + "husky": "5.1.2", + "lint-staged": "10.5.4", + "prettier": "2.2.1", + "prettier-plugin-prisma": "0.4.0", + "pretty-quick": "3.1.0", + "preview-email": "3.0.3" + }, + "private": true +} diff --git a/examples/static/public/favicon.ico b/examples/static/public/favicon.ico new file mode 100755 index 0000000000..a94b0f7a7f Binary files /dev/null and b/examples/static/public/favicon.ico differ diff --git a/examples/static/public/logo.png b/examples/static/public/logo.png new file mode 100644 index 0000000000..6a982616ee Binary files /dev/null and b/examples/static/public/logo.png differ diff --git a/examples/static/test/setup.ts b/examples/static/test/setup.ts new file mode 100644 index 0000000000..c278ddb9e6 --- /dev/null +++ b/examples/static/test/setup.ts @@ -0,0 +1,4 @@ +// This is the jest 'setupFilesAfterEnv' setup file +// It's a good place to set globals, add global before/after hooks, etc + +export {} // so TS doesn't complain diff --git a/examples/static/test/utils.tsx b/examples/static/test/utils.tsx new file mode 100644 index 0000000000..5444510768 --- /dev/null +++ b/examples/static/test/utils.tsx @@ -0,0 +1,107 @@ +import React from "react" +import { RouterContext, BlitzRouter, BlitzProvider } from "blitz" +import { render as defaultRender } from "@testing-library/react" +import { renderHook as defaultRenderHook } from "@testing-library/react-hooks" + +export * from "@testing-library/react" + +// -------------------------------------------------------------------------------- +// This file customizes the render() and renderHook() test functions provided +// by React testing library. It adds a router context wrapper with a mocked router. +// +// You should always import `render` and `renderHook` from this file +// +// This is the place to add any other context providers you need while testing. +// -------------------------------------------------------------------------------- + +// -------------------------------------------------- +// render() +// -------------------------------------------------- +// Override the default test render with our own +// +// You can override the router mock like this: +// +// const { baseElement } = render(, { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function render( + ui: RenderUI, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }) => ( + + + {children} + + + ) + } + return defaultRender(ui, { wrapper, ...options }) +} + +// -------------------------------------------------- +// renderHook() +// -------------------------------------------------- +// Override the default test renderHook with our own +// +// You can override the router mock like this: +// +// const result = renderHook(() => myHook(), { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function renderHook( + hook: RenderHook, + { wrapper, router, dehydratedState, ...options }: RenderHookOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }) => ( + + + {children} + + + ) + } + return defaultRenderHook(hook, { wrapper, ...options }) +} + +export const mockRouter: BlitzRouter = { + basePath: "", + pathname: "/", + route: "/", + asPath: "/", + params: {}, + query: {}, + isReady: true, + push: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + isFallback: false, +} + +type DefaultParams = Parameters +type RenderUI = DefaultParams[0] +type RenderOptions = DefaultParams[1] & { + router?: Partial + dehydratedState?: unknown +} + +type DefaultHookParams = Parameters +type RenderHook = DefaultHookParams[0] +type RenderHookOptions = DefaultHookParams[1] & { + router?: Partial + dehydratedState?: unknown +} diff --git a/examples/static/tsconfig.json b/examples/static/tsconfig.json new file mode 100644 index 0000000000..a49d31ce0e --- /dev/null +++ b/examples/static/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": "./", + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "exclude": ["node_modules", "**/*.e2e.ts", "cypress"], + "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"] +} diff --git a/examples/static/types.ts b/examples/static/types.ts new file mode 100644 index 0000000000..0afbe50e39 --- /dev/null +++ b/examples/static/types.ts @@ -0,0 +1,16 @@ +import { DefaultCtx, SessionContext } from "blitz" + +// Note: You should switch to Postgres and then use a DB enum for role type +export type Role = "ADMIN" | "USER" + +declare module "blitz" { + export interface Ctx extends DefaultCtx { + session: SessionContext + } + export interface Session { + PublicData: { + userId: string + roles: Role[] + } + } +} diff --git a/examples/store/.env.example b/examples/store/.env.example new file mode 100644 index 0000000000..91cb683691 --- /dev/null +++ b/examples/store/.env.example @@ -0,0 +1 @@ +DATABASE_URL=postgresql://USERNAME@localhost:5432/blitz-example-store diff --git a/examples/store/.gitignore b/examples/store/.gitignore new file mode 100644 index 0000000000..ac35a3f3bc --- /dev/null +++ b/examples/store/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage +cypress/videos +cypress/screenshots + +# next.js +/.next/ +/out/ +/.blitz/ +*.sqlite +.generated-prisma-client +.blitz-console-history + +# production +/build +.now + +# misc +.DS_Store + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.envrc +.env +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/examples/store/.prettierignore b/examples/store/.prettierignore new file mode 100644 index 0000000000..26354d667a --- /dev/null +++ b/examples/store/.prettierignore @@ -0,0 +1,6 @@ +.gitkeep +.env +*.ico +*.lock +db/migrations + diff --git a/examples/store/README.md b/examples/store/README.md new file mode 100644 index 0000000000..e90313d2c8 --- /dev/null +++ b/examples/store/README.md @@ -0,0 +1,15 @@ +## Getting Started + +1. DB migrate + +``` +yarn blitz prisma migrate dev +``` + +2. Start the dev server + +``` +yarn blitz dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/examples/store/app/admin/api/users.ts b/examples/store/app/admin/api/users.ts new file mode 100644 index 0000000000..9357ed8a81 --- /dev/null +++ b/examples/store/app/admin/api/users.ts @@ -0,0 +1,7 @@ +import db from "db" +import {BlitzApiRequest, BlitzApiResponse} from "blitz" + +export default async function users(_req: BlitzApiRequest, res: BlitzApiResponse) { + const products = await db.product.findMany() + res.status(200).send(products) +} diff --git a/examples/store/app/admin/pages/admin/index.module.scss b/examples/store/app/admin/pages/admin/index.module.scss new file mode 100644 index 0000000000..3ab234362d --- /dev/null +++ b/examples/store/app/admin/pages/admin/index.module.scss @@ -0,0 +1,4 @@ +/* integration test for scss modules */ +.red { + border: 2px solid red; +} diff --git a/examples/store/app/admin/pages/admin/index.tsx b/examples/store/app/admin/pages/admin/index.tsx new file mode 100644 index 0000000000..74520852a0 --- /dev/null +++ b/examples/store/app/admin/pages/admin/index.tsx @@ -0,0 +1,20 @@ +import {Link} from "blitz" + +import styles from "./index.module.scss" + +function StoreAdminPage() { + return ( +
+

Store Admin

+
+

+ + Manage Products + +

+
+
+ ) +} + +export default StoreAdminPage diff --git a/examples/store/app/admin/pages/admin/products/[id].tsx b/examples/store/app/admin/pages/admin/products/[id].tsx new file mode 100644 index 0000000000..5a2ae4884a --- /dev/null +++ b/examples/store/app/admin/pages/admin/products/[id].tsx @@ -0,0 +1,42 @@ +import {Suspense} from "react" +import {Link, useRouter, useQuery, useParam} from "blitz" +import getProduct from "app/products/queries/getProduct" +import ProductForm from "app/products/components/ProductForm" + +function Product() { + const router = useRouter() + const id = useParam("id", "number") + const [product] = useQuery(getProduct, {where: {id}}) + + // Here to test for https://github.com/blitz-js/blitz/issues/1443 + if (!product) throw new Error("useQuery did not throw!") + + return ( + { + await router.push("/admin/products") + }} + /> + ) +} + +function EditProductPage() { + return ( +
+

Edit Product

+

+ + Manage Products + +

+
+ Loading...
}> + + +
+ + ) +} + +export default EditProductPage diff --git a/examples/store/app/admin/pages/admin/products/index.tsx b/examples/store/app/admin/pages/admin/products/index.tsx new file mode 100644 index 0000000000..9b0959d8b8 --- /dev/null +++ b/examples/store/app/admin/pages/admin/products/index.tsx @@ -0,0 +1,88 @@ +import {Suspense, useState} from "react" +import {useQuery, Link, useRouterQuery, invalidateQuery, setQueryData} from "blitz" +import getProducts from "app/products/queries/getProducts" +import {mean} from "lodash" +import {Product} from "@prisma/client" + +// this is here mainly as an integration test for +// importing from api/ +export function meanPrice(products: Product[]) { + const prices = products.map((p) => p.price).filter((p) => !!p) as number[] + return mean(prices) +} + +function reversedProductList(productsList) { + return {...productsList, products: [...productsList.products].reverse()} +} + +function ProductsList() { + const {orderby = "id", order = "desc"} = useRouterQuery() + const [refetch, setRefetch] = useState(false) + const params = { + orderBy: { + [Array.isArray(orderby) ? orderby[0] : orderby]: order, + }, + } + + const [{products}] = useQuery(getProducts, params) + + return ( + <> + + + + + +

Mean price: {meanPrice(products)}

+ + ) +} + +function AdminProducts() { + return ( +
+

Products

+ + + +

+ + Create Product + + + Home + +

+ + Loading...
}> + + + + ) +} + +export default AdminProducts diff --git a/examples/store/app/admin/pages/admin/products/new.tsx b/examples/store/app/admin/pages/admin/products/new.tsx new file mode 100644 index 0000000000..bf655b7553 --- /dev/null +++ b/examples/store/app/admin/pages/admin/products/new.tsx @@ -0,0 +1,21 @@ +import {Link, useRouter} from "blitz" +import ProductForm from "app/products/components/ProductForm" + +function AdminNewProductPage() { + const router = useRouter() + return ( +
+

Create a New Product

+

+ + Manage Products + +

+
+ router.push("/admin/products")} /> +
+
+ ) +} + +export default AdminNewProductPage diff --git a/examples/store/app/components/.keep b/examples/store/app/components/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/store/app/layouts/.keep b/examples/store/app/layouts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/store/app/pages/_app.tsx b/examples/store/app/pages/_app.tsx new file mode 100644 index 0000000000..4c62b4bce2 --- /dev/null +++ b/examples/store/app/pages/_app.tsx @@ -0,0 +1,19 @@ +import {AppProps, ErrorBoundary, ErrorComponent, useQueryErrorResetBoundary} from "blitz" + +if (typeof window !== "undefined") { + window["DEBUG_BLITZ"] = 1 +} + +export default function App({Component, pageProps}: AppProps) { + const {reset} = useQueryErrorResetBoundary() + + return ( + + + + ) +} + +function RootErrorFallback({error, resetErrorBoundary}) { + return +} diff --git a/examples/store/app/pages/_document.tsx b/examples/store/app/pages/_document.tsx new file mode 100644 index 0000000000..a510cc6531 --- /dev/null +++ b/examples/store/app/pages/_document.tsx @@ -0,0 +1,22 @@ +import {Document, Html, DocumentHead, Main, BlitzScript, DocumentContext} from "blitz" + +class MyDocument extends Document { + static async getInitialProps(ctx: DocumentContext) { + const initialProps = await Document.getInitialProps(ctx) + return {...initialProps} + } + + render() { + return ( + + + +
+ + + + ) + } +} + +export default MyDocument diff --git a/examples/store/app/pages/index.module.css b/examples/store/app/pages/index.module.css new file mode 100644 index 0000000000..c0b68701bb --- /dev/null +++ b/examples/store/app/pages/index.module.css @@ -0,0 +1,4 @@ +/* integration test for css modules */ +.red { + border: 2px solid red; +} diff --git a/examples/store/app/pages/index.tsx b/examples/store/app/pages/index.tsx new file mode 100644 index 0000000000..ed54f0658f --- /dev/null +++ b/examples/store/app/pages/index.tsx @@ -0,0 +1,210 @@ +import React, {Suspense} from "react" +import {Head, Link, useQuery} from "blitz" +import getReferer from "app/queries/getReferer" +// integration test for css modules +import styles from "./index.module.css" + +const Referer = () => { + // This is here mainly as an integration test for global middleware + const [referer] = useQuery(getReferer, {}) + + return
Referer: {referer}
+} + +const Home = () => ( +
+ + Blitz Example Store + + + +
+

+ Blitz Store Example +

+ +
+ + + + + + +
+) + +export default Home diff --git a/examples/store/app/products/api.ts b/examples/store/app/products/api.ts new file mode 100644 index 0000000000..3804db33e8 --- /dev/null +++ b/examples/store/app/products/api.ts @@ -0,0 +1,5 @@ +// this file acts as a regression test for #1646 + +export function getMeSomeQualityHumor() { + return "https://xkcd.com/" + Math.floor(Math.random() * 2402) +} diff --git a/examples/store/app/products/components/ProductForm.module.scss b/examples/store/app/products/components/ProductForm.module.scss new file mode 100644 index 0000000000..c0b68701bb --- /dev/null +++ b/examples/store/app/products/components/ProductForm.module.scss @@ -0,0 +1,4 @@ +/* integration test for css modules */ +.red { + border: 2px solid red; +} diff --git a/examples/store/app/products/components/ProductForm.tsx b/examples/store/app/products/components/ProductForm.tsx new file mode 100644 index 0000000000..6ef64d5086 --- /dev/null +++ b/examples/store/app/products/components/ProductForm.tsx @@ -0,0 +1,93 @@ +import {Form, Field} from "react-final-form" +import {Product, Prisma} from "db" +import createProduct from "../mutations/createProduct" +import updateProduct from "../mutations/updateProduct" +import {useMutation} from "blitz" +// integration test for css modules +import styles from "./ProductForm.module.scss" + +type ProductInput = Prisma.ProductCreateInput | Product + +function isNew(product: ProductInput): product is Prisma.ProductCreateInput { + return (product as any).id === undefined +} + +type ProductFormProps = { + product?: Prisma.ProductUpdateInput + style?: React.CSSProperties + onSuccess: (product: Product) => any +} + +function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) { + const [createProductMutation] = useMutation(createProduct) + const [updateProductMutation] = useMutation(updateProduct) + return ( +
{ + if (isNew(data)) { + try { + const product = await createProductMutation({data}) + onSuccess(product) + } catch (error) { + alert("Error creating product " + JSON.stringify(error, null, 2)) + } + } else { + try { + // Can't update id + const id = data.id + delete data.id + const product = await updateProductMutation({where: {id}, data}) + onSuccess(product) + } catch (error) { + alert("Error updating product " + JSON.stringify(error, null, 2)) + } + } + }} + render={({handleSubmit}) => ( + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ )} + /> + ) +} + +export default ProductForm diff --git a/examples/store/app/products/mutations/createProduct.ts b/examples/store/app/products/mutations/createProduct.ts new file mode 100644 index 0000000000..298558da36 --- /dev/null +++ b/examples/store/app/products/mutations/createProduct.ts @@ -0,0 +1,10 @@ +import db, {Prisma} from "db" + +type CreateProductInput = { + data: Prisma.ProductCreateArgs["data"] +} +export default async function createProduct({data}: CreateProductInput) { + const product = await db.product.create({data}) + + return product +} diff --git a/examples/store/app/products/mutations/deleteProduct.ts b/examples/store/app/products/mutations/deleteProduct.ts new file mode 100644 index 0000000000..01f7be6e89 --- /dev/null +++ b/examples/store/app/products/mutations/deleteProduct.ts @@ -0,0 +1,11 @@ +import db, {Prisma} from "db" + +type DeleteProductInput = { + where: Prisma.ProductDeleteArgs["where"] +} + +export default async function deleteProduct({where}: DeleteProductInput) { + const product = await db.product.delete({where}) + + return product +} diff --git a/examples/store/app/products/mutations/updateProduct.ts b/examples/store/app/products/mutations/updateProduct.ts new file mode 100644 index 0000000000..8b24facfc9 --- /dev/null +++ b/examples/store/app/products/mutations/updateProduct.ts @@ -0,0 +1,13 @@ +import db, {Prisma} from "db" +import {Ctx} from "blitz" + +type UpdateProductInput = { + where: Prisma.ProductUpdateArgs["where"] + data: Prisma.ProductUpdateArgs["data"] +} + +export default async function updateProduct({where, data}: UpdateProductInput, _ctx: Ctx) { + const product = await db.product.update({where, data}) + + return product +} diff --git a/examples/store/app/products/pages/products/[handle].tsx b/examples/store/app/products/pages/products/[handle].tsx new file mode 100644 index 0000000000..a8ec98eb32 --- /dev/null +++ b/examples/store/app/products/pages/products/[handle].tsx @@ -0,0 +1,39 @@ +import {Link, InferGetStaticPropsType, GetStaticPaths} from "blitz" +import {GetStaticPropsContext} from "next" +import getProduct from "app/products/queries/getProduct" +import getProducts from "app/products/queries/getProducts" + +export const getStaticProps = async (ctx: GetStaticPropsContext) => { + const product = await getProduct({where: {handle: ctx.params!.handle as string}}, {} as any) + return { + props: {product}, + revalidate: 1, + } +} +export const getStaticPaths: GetStaticPaths = async () => { + const paths = (await getProducts({orderBy: {id: "desc"}})).products.map(({handle}) => ({ + params: {handle}, + })) + return { + paths, + fallback: true, + } +} + +const Page = function ({product}: InferGetStaticPropsType) { + if (!product) { + return
Loading...
+ } + return ( +
+

{product.name}

+

{product.description}

+

Price: ${product.price}

+ + + All Products + +
+ ) +} +export default Page diff --git a/examples/store/app/products/pages/products/index.tsx b/examples/store/app/products/pages/products/index.tsx new file mode 100644 index 0000000000..6fcb8af3ff --- /dev/null +++ b/examples/store/app/products/pages/products/index.tsx @@ -0,0 +1,40 @@ +import {Link, BlitzPage, InferGetStaticPropsType} from "blitz" +import getProducts from "../../queries/getProducts" +import {sum} from "lodash" +import {Product} from "@prisma/client" + +// regression test for #1646 +import {getMeSomeQualityHumor} from "../../api" +console.log("Attention! Must read: " + getMeSomeQualityHumor()) + +export function averagePrice(products: Product[]) { + const prices = products.map((p) => p.price ?? 0) + return sum(prices) / prices.length +} + +export const getStaticProps = async () => { + const {products} = await getProducts({orderBy: {id: "desc"}}) + return { + props: {products}, + revalidate: 1, + } +} + +const Products: BlitzPage> = function ({products}) { + return ( +
+

First 100 Products

+
+ {products.map((product) => ( +

+ + {product.name} + +

+ ))} +
+

Average price: {averagePrice(products).toFixed(1)}

+
+ ) +} +export default Products diff --git a/examples/store/app/products/pages/products/infinite.tsx b/examples/store/app/products/pages/products/infinite.tsx new file mode 100644 index 0000000000..12d2782d7c --- /dev/null +++ b/examples/store/app/products/pages/products/infinite.tsx @@ -0,0 +1,53 @@ +import {Suspense, Fragment} from "react" +import {BlitzPage, useInfiniteQuery, Link} from "blitz" +import getProducts from "app/products/queries/getProducts" + +const Products = () => { + const [ + productPages, + {isFetching, isFetchingNextPage, fetchNextPage, hasNextPage}, + ] = useInfiniteQuery(getProducts, (page = {take: 3, skip: 0}) => page, { + getNextPageParam: (lastPage) => lastPage.nextPage, + }) + + return ( + <> + {productPages.map((page, i) => ( + + {page.products.map((product) => ( +

+ {product.name} +

+ ))} +
+ ))} + +
+ +
+ +
{isFetching && !isFetchingNextPage ? "Fetching..." : null}
+ + ) +} + +const ProductsInfinite: BlitzPage = function () { + return ( +
+

Products - Infinite

+ + Go to Paginated Product List + + Loading...
}> + + + + ) +} +export default ProductsInfinite diff --git a/examples/store/app/products/pages/products/paginated.tsx b/examples/store/app/products/pages/products/paginated.tsx new file mode 100644 index 0000000000..9fb4f5aebd --- /dev/null +++ b/examples/store/app/products/pages/products/paginated.tsx @@ -0,0 +1,50 @@ +import {Suspense} from "react" +import {Link, BlitzPage, usePaginatedQuery, useRouter} from "blitz" +import getProducts from "app/products/queries/getProducts" + +const ITEMS_PER_PAGE = 3 + +const Products = () => { + const router = useRouter() + const page = Number(router.query.page) || 0 + const [{products, hasMore}] = usePaginatedQuery(getProducts, { + skip: ITEMS_PER_PAGE * page, + take: ITEMS_PER_PAGE, + }) + + const goToPreviousPage = () => router.push({query: {page: page - 1}}) + const goToNextPage = () => router.push({query: {page: page + 1}}) + + return ( +
+ {products.map((product) => ( +

+ + {product.name} + +

+ ))} + + +
+ ) +} + +const ProductsPaginated: BlitzPage = function () { + return ( +
+

Products - Paginated

+ + Go to Infinite Product List + + Loading...
}> + + + + ) +} +export default ProductsPaginated diff --git a/examples/store/app/products/pages/products/ssr.tsx b/examples/store/app/products/pages/products/ssr.tsx new file mode 100644 index 0000000000..fadee3a18d --- /dev/null +++ b/examples/store/app/products/pages/products/ssr.tsx @@ -0,0 +1,31 @@ +import {invokeWithMiddleware, Link, BlitzPage, InferGetServerSidePropsType} from "blitz" +import getProducts from "app/products/queries/getProducts" + +export const getServerSideProps = async ({req, res}) => { + const result = await invokeWithMiddleware(getProducts, {orderBy: {id: "desc"}}, {req, res}) + return { + props: { + products: result.products, + }, + } +} +type PageProps = InferGetServerSidePropsType + +const ProductsSSR: BlitzPage = function (props) { + return ( +
+

Products

+
+ {props.products.map((product) => ( +

+ + {product.name} + + - Created At: {product.createdAt.toISOString()} +

+ ))} +
+
+ ) +} +export default ProductsSSR diff --git a/examples/store/app/products/queries/getProduct.ts b/examples/store/app/products/queries/getProduct.ts new file mode 100644 index 0000000000..88bbeeee4a --- /dev/null +++ b/examples/store/app/products/queries/getProduct.ts @@ -0,0 +1,16 @@ +import {NotFoundError, Ctx} from "blitz" +import db, {Prisma} from "db" + +type GetProductInput = { + where: Prisma.ProductFindFirstArgs["where"] + // Only available if a model relationship exists + // include?: FindFirstProductArgs['include'] +} + +export default async function getProduct({where}: GetProductInput, _ctx: Ctx) { + const product = await db.product.findFirst({where}) + + if (!product) throw new NotFoundError() + + return product +} diff --git a/examples/store/app/products/queries/getProducts.ts b/examples/store/app/products/queries/getProducts.ts new file mode 100644 index 0000000000..29beb1b9f1 --- /dev/null +++ b/examples/store/app/products/queries/getProducts.ts @@ -0,0 +1,43 @@ +import {Middleware, paginate} from "blitz" +import db, {Prisma} from "db" + +type GetProductsInput = { + where?: Prisma.ProductFindManyArgs["where"] + orderBy?: Prisma.ProductFindManyArgs["orderBy"] + skip?: number + take?: number +} + +export const middleware: Middleware[] = [ + async (req, res, next) => { + await next() + if (req.method !== "HEAD" && Array.isArray(res.blitzResult)) { + console.log("[Middleware] Total product count:", res.blitzResult.length, "\n") + } + }, +] + +export default async function getProducts( + {where, orderBy, skip = 0, take = 100}: GetProductsInput, + ctx: Record = {}, +) { + if (ctx.referer) { + console.log("HTTP referer:", ctx.referer) + } + + console.log("this line should not be included in the frontend bundle") + + const {items: products, hasMore, nextPage, count} = await paginate({ + skip, + take, + count: () => db.product.count({where}), + query: (paginateArgs) => db.product.findMany({...paginateArgs, where, orderBy}), + }) + + return { + products, + hasMore, + nextPage, + count, + } +} diff --git a/examples/store/app/queries/getReferer.ts b/examples/store/app/queries/getReferer.ts new file mode 100644 index 0000000000..93f1bd2fa0 --- /dev/null +++ b/examples/store/app/queries/getReferer.ts @@ -0,0 +1,3 @@ +export default function getReferer(_, ctx: any = {}) { + return ctx.referer +} diff --git a/examples/store/assert-tree-shaking-works.js b/examples/store/assert-tree-shaking-works.js new file mode 100755 index 0000000000..ef17a3eae2 --- /dev/null +++ b/examples/store/assert-tree-shaking-works.js @@ -0,0 +1,25 @@ +if (process.platform !== "win32") { + const glob = require("glob") + const fs = require("fs") + + glob("./.next/static/chunks/**/*", {nodir: true}, (err, matches) => { + if (err) { + throw err + } + + const offenders = [] + + for (const match of matches) { + const content = fs.readFileSync(match).toString() + + if (content.includes("this line should not be included in the frontend bundle")) { + offenders.push(match) + } + } + + if (offenders.length > 0) { + console.error(`tree-shaking failed: ${offenders} includes the forbidden words.`) + process.exit(1) + } + }) +} diff --git a/examples/store/babel.config.js b/examples/store/babel.config.js new file mode 100644 index 0000000000..dfdf62cea1 --- /dev/null +++ b/examples/store/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["blitz/babel"], + plugins: [], +} diff --git a/examples/store/blitz.config.js b/examples/store/blitz.config.js new file mode 100644 index 0000000000..926699068b --- /dev/null +++ b/examples/store/blitz.config.js @@ -0,0 +1,25 @@ +module.exports = { + middleware: [ + (req, res, next) => { + res.blitzCtx.referer = req.headers.referer + return next() + }, + ], + log: { + // level: "trace", + }, + experimental: { + isomorphicResolverImports: true, + }, + // webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + // // Note: we provide webpack above so you should not `require` it + // // Perform customizations to webpack config + // // Important: return the modified config + // return config + // }, + // webpackDevMiddleware: (config) => { + // // Perform customizations to webpack dev middleware config + // // Important: return the modified config + // return config + // }, +} diff --git a/examples/store/cypress.json b/examples/store/cypress.json new file mode 100644 index 0000000000..489a43829c --- /dev/null +++ b/examples/store/cypress.json @@ -0,0 +1,9 @@ +{ + "baseUrl": "http://localhost:3099", + "defaultCommandTimeout": 10000, + "retries": { + "runMode": 1 + }, + "video": false, + "chromeWebSecurity": false +} diff --git a/examples/store/cypress/fixtures/example.json b/examples/store/cypress/fixtures/example.json new file mode 100644 index 0000000000..02e4254378 --- /dev/null +++ b/examples/store/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/examples/store/cypress/index.d.ts b/examples/store/cypress/index.d.ts new file mode 100644 index 0000000000..137a1e0d38 --- /dev/null +++ b/examples/store/cypress/index.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/store/cypress/integration/admin/index.test.ts b/examples/store/cypress/integration/admin/index.test.ts new file mode 100644 index 0000000000..0c45b03efd --- /dev/null +++ b/examples/store/cypress/integration/admin/index.test.ts @@ -0,0 +1,17 @@ +describe("admin#index page", () => { + beforeEach(() => { + cy.visit("/admin") + }) + + it("Has H1", () => { + cy.visit("/admin") + cy.contains("h1", "Store Admin") + }) + + it("goes to admin/products page", () => { + cy.contains("a", "Manage Products").click() + cy.location("pathname").should("equal", "/admin/products") + }) +}) + +export {} diff --git a/examples/store/cypress/integration/admin/products/edit.test.ts b/examples/store/cypress/integration/admin/products/edit.test.ts new file mode 100644 index 0000000000..6a8f8f5ad1 --- /dev/null +++ b/examples/store/cypress/integration/admin/products/edit.test.ts @@ -0,0 +1,50 @@ +import {insert, data, fields} from "./helper" + +describe("admin/products/[handle] page", () => { + beforeEach(() => { + insert() + + cy.visit("/admin/products") + cy.get("ul > li:last-child a").click() + }) + + it("Has h1 and link back", () => { + cy.contains("h1", "Edit Product") + cy.get("p > a").first().contains("Manage Products").click() + cy.location("pathname").should("equal", "/admin/products") + }) + + it("Has all fields, change ProductName", () => { + cy.get("form > div > label").as("inputs") + cy.get("@inputs").should("have.length", 4) + + const random = Math.round(Math.random() * 100000).toString() + + const count = {} + for (let i = 0; i < data.length; i++) { + const {label, type} = fields[i] + const [element, inputType] = type.split("|") + let item = data[i] + + if (count[element] === undefined) count[element] = 0 + if (inputType) { + cy.get("@inputs").get(element).eq(count[element]).should("have.attr", "type", inputType) + } + + item += random + + cy.get("@inputs").eq(i).contains(label) + cy.get("@inputs").get(element).eq(count[element]).clear().type(item) + cy.get("@inputs").get(element).eq(count[element]).should("have.value", item) + + count[element]++ + } + + cy.get("button").click() + + cy.location("pathname").should("equal", "/admin/products") + cy.get("ul > li:last-child").contains(data[0] + random) + }) +}) + +export {} diff --git a/examples/store/cypress/integration/admin/products/helper.ts b/examples/store/cypress/integration/admin/products/helper.ts new file mode 100644 index 0000000000..dd85ff32c0 --- /dev/null +++ b/examples/store/cypress/integration/admin/products/helper.ts @@ -0,0 +1,45 @@ +export const fields = [ + {label: "Product Name", type: "input|", uniq: true}, + {label: "Handle", type: "input|", uniq: true}, + {label: "Description", type: "textarea|"}, + {label: "Price", type: "input|"}, // TODO: Add input type here input|number +] +export const data = ["Apples", "apples", "Fresh apples", "32"] + +export const insert = (): string => { + cy.visit("/admin/products/new") + cy.get("form > div > label").as("inputs") + cy.get("@inputs").should("have.length", 4) + + const random = Math.round(Math.random() * 100000).toString() + const name = data[0] + random + + const count = {} + for (let i = 0; i < data.length; i++) { + const {label, type, uniq} = fields[i] + const [element, inputType] = type.split("|") + let item = data[i] + + if (count[element] === undefined) count[element] = 0 + if (inputType) { + cy.get("@inputs").get(element).eq(count[element]).should("have.attr", "type", inputType) + } + + if (uniq) { + item += random + } + + cy.get("@inputs").eq(i).contains(label) + cy.get("@inputs").eq(i).type(item) + cy.get("@inputs").get(element).eq(count[element]).should("have.value", item) + + count[element]++ + } + + cy.get("button").click() + + cy.location("pathname").should("equal", "/admin/products") + cy.get("ul > li:first-child").contains(name) + + return name +} diff --git a/examples/store/cypress/integration/admin/products/index.test.ts b/examples/store/cypress/integration/admin/products/index.test.ts new file mode 100644 index 0000000000..f03980c1d1 --- /dev/null +++ b/examples/store/cypress/integration/admin/products/index.test.ts @@ -0,0 +1,44 @@ +import {insert} from "./helper" + +describe("admin/products page", () => { + beforeEach(() => { + cy.visit("/admin/products") + }) + + it("Has H1", () => { + cy.contains("h1", "Products") + }) + + it("goes to new product page", () => { + cy.get("p > a").first().contains("Create Product").click() + cy.location("pathname").should("equal", "/admin/products/new") + }) + + it("goes to bas product page", () => { + cy.get("p > a").last().contains("Home").click() + cy.location("pathname").should("equal", "/") + }) + + it("shows ascending order", () => { + cy.get("ul > li") + .first() + .then(($li) => { + const title = $li.find("> a").text() + + cy.visit("/admin/products?order=asc") + cy.get("ul > li").last().contains("a", title) + }) + }) + + it("show the mean price", () => { + cy.contains(/Mean price: \d+/) + }) + + // This is kind of redundant because this logic is handled in insert() + it("shows latest created product", () => { + const name = insert() + cy.get("ul > li").contains(name) + }) +}) + +export {} diff --git a/examples/store/cypress/integration/admin/products/new.test.ts b/examples/store/cypress/integration/admin/products/new.test.ts new file mode 100644 index 0000000000..550da307fb --- /dev/null +++ b/examples/store/cypress/integration/admin/products/new.test.ts @@ -0,0 +1,19 @@ +import {insert} from "./helper" + +describe("admin/products/new page", () => { + beforeEach(() => { + cy.visit("/admin/products/new") + }) + + it("Has h1 and link back", () => { + cy.contains("h1", "Create a New Product") + cy.get("p > a").first().contains("Manage Products").click() + cy.location("pathname").should("equal", "/admin/products") + }) + + it("Fills fields and creates product", () => { + insert() + }) +}) + +export {} diff --git a/examples/store/cypress/integration/index.test.ts b/examples/store/cypress/integration/index.test.ts new file mode 100644 index 0000000000..dfcbb7cbed --- /dev/null +++ b/examples/store/cypress/integration/index.test.ts @@ -0,0 +1,23 @@ +describe("index page", () => { + beforeEach(() => { + cy.visit("/") + }) + + it("Has title and H1", () => { + cy.contains("h1", "Blitz Store Example") + cy.title().should("eq", "Blitz Example Store") + cy.get("#referer").contains("http://localhost:3099") + }) + + it("goes to products page", () => { + cy.contains("a", "Static Product Listings").click() + cy.location("pathname").should("equal", "/products") + }) + + it("goes to admin page", () => { + cy.contains("a", "Admin Section (client-rendered)").click() + cy.location("pathname").should("equal", "/admin/products") + }) +}) + +export {} diff --git a/examples/store/cypress/integration/products.test.ts b/examples/store/cypress/integration/products.test.ts new file mode 100644 index 0000000000..a9e9a74bd5 --- /dev/null +++ b/examples/store/cypress/integration/products.test.ts @@ -0,0 +1,77 @@ +describe("products#index page", () => { + it("Has H1", () => { + cy.visit("/products") + cy.contains("h1", "Products") + }) + + it("Logs the XKCD (Regression #1646)", () => { + cy.visit("/products", { + onBeforeLoad(win) { + cy.stub(win.console, "log").as("consoleLog") + }, + }) + cy.get("@consoleLog").should("be.calledWithMatch", /Attention! Must read: .*/) + }) +}) + +describe("products#show page", () => { + beforeEach(() => { + cy.visit("/products") + }) + + it("goes to product page", () => { + cy.get("#products > p > a").first().click() + cy.location("pathname").should("match", /\/products\/\S+$/) + }) + + it("has price and title", () => { + cy.get("#products > p > a").first().click() + cy.location("pathname").should("match", /\/products\/\S+$/) + + cy.get("h1").should("have.length", 1) + cy.get(".description").should("exist") + cy.get(".price").contains(/Price: \$[0-9]*/) + }) + + it("goes to back to products page", () => { + cy.get("#products > p > a").first().click() + cy.location("pathname").should("match", /\/products\/\S+$/) + + cy.get("a").first().click() + cy.location("pathname").should("equal", "/products") + }) + + it("shows the average price", () => { + cy.contains(/Average price: \d+.\d/) + }) +}) + +describe("products#ssr page", () => { + beforeEach(() => { + cy.visit("/products/ssr") + }) + + it("has title", () => { + cy.get("h1").contains("Products") + }) + + it("goes to back to products page", () => { + cy.get("#products > p > a").first().click() + cy.location("pathname").should("not.match", /\/products\/ssr$/) + + cy.get("a").first().click() + cy.location("pathname").should("equal", "/products") + }) +}) + +describe("products#infinite page", () => { + beforeEach(() => { + cy.visit("/products/infinite") + }) + + it("shows 3 products", () => { + cy.get('[data-test="productName"]').should("have.length", 3) + }) +}) + +export {} diff --git a/examples/store/cypress/plugins/index.js b/examples/store/cypress/plugins/index.js new file mode 100644 index 0000000000..aa9918d215 --- /dev/null +++ b/examples/store/cypress/plugins/index.js @@ -0,0 +1,21 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/examples/store/cypress/support/commands.js b/examples/store/cypress/support/commands.js new file mode 100644 index 0000000000..ca4d256f3e --- /dev/null +++ b/examples/store/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/examples/store/cypress/support/index.js b/examples/store/cypress/support/index.js new file mode 100644 index 0000000000..7ce8adebc4 --- /dev/null +++ b/examples/store/cypress/support/index.js @@ -0,0 +1,24 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import "./commands" + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +Cypress.Screenshot.defaults({ + screenshotOnRunFailure: false, +}) diff --git a/examples/store/db/index.ts b/examples/store/db/index.ts new file mode 100644 index 0000000000..cde0794c22 --- /dev/null +++ b/examples/store/db/index.ts @@ -0,0 +1,7 @@ +import {enhancePrisma} from "blitz" +import {PrismaClient} from "@prisma/client" + +const EnhancedPrisma = enhancePrisma(PrismaClient) + +export * from "@prisma/client" +export default new EnhancedPrisma() diff --git a/examples/store/db/migrations/20201214222620_initial_migration/migration.sql b/examples/store/db/migrations/20201214222620_initial_migration/migration.sql new file mode 100644 index 0000000000..7be4708cc5 --- /dev/null +++ b/examples/store/db/migrations/20201214222620_initial_migration/migration.sql @@ -0,0 +1,27 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + "role" TEXT, + "storeId" INTEGER +); + +-- CreateTable +CREATE TABLE "Product" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "handle" TEXT NOT NULL, + "name" TEXT, + "description" TEXT, + "price" INTEGER +); + +-- CreateIndex +CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Product.handle_unique" ON "Product"("handle"); diff --git a/examples/store/db/migrations/20210122020051_add_variant/migration.sql b/examples/store/db/migrations/20210122020051_add_variant/migration.sql new file mode 100644 index 0000000000..f355cd1ae7 --- /dev/null +++ b/examples/store/db/migrations/20210122020051_add_variant/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "Variant" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "name" TEXT NOT NULL, + "productId" INTEGER NOT NULL, + FOREIGN KEY ("productId") REFERENCES "Product" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); diff --git a/examples/store/db/migrations/migration_lock.toml b/examples/store/db/migrations/migration_lock.toml new file mode 100644 index 0000000000..963afcc9a1 --- /dev/null +++ b/examples/store/db/migrations/migration_lock.toml @@ -0,0 +1,2 @@ +# Please do not edit this file manually +provider = "sqlite" \ No newline at end of file diff --git a/examples/store/db/schema.prisma b/examples/store/db/schema.prisma new file mode 100644 index 0000000000..790c2d8bde --- /dev/null +++ b/examples/store/db/schema.prisma @@ -0,0 +1,51 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +datasource sqlite { + provider = "sqlite" + url = "file:./db.sqlite" +} + +// SQLite is easy to start with, but if you use Postgres in production +// you should also use it in development with the following: +//datasource postgresql { +// provider = "postgresql" +// url = env("DATABASE_URL") +//} + +generator client { + provider = "prisma-client-js" +} + +// -------------------------------------- + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + name String? + role String? + storeId Int? +} + +model Product { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + handle String @unique + name String? + description String? + price Int? + + variants Variant[] +} + +model Variant { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String + product Product @relation(fields: [productId], references: [id]) + productId Int +} diff --git a/examples/store/db/seeds.ts b/examples/store/db/seeds.ts new file mode 100644 index 0000000000..ec82f54f9f --- /dev/null +++ b/examples/store/db/seeds.ts @@ -0,0 +1,30 @@ +import db from "./index" + +const randomString = (len: number, offset = 3) => { + let output = "" + + for (let i = 0; i < len + Math.ceil((Math.random() - 0.5) * offset); i++) { + const ascii = Math.floor(Math.random() * 26) + (i % 2 === 0 ? 97 : 65) + output += String.fromCharCode(ascii) + } + + return output +} + +const randomProduct = () => { + return { + name: randomString(10), + handle: randomString(6, 0), + description: Array.from(new Array(10), () => randomString(10)).join(" "), + price: Math.floor(Math.random() * 10000), + } +} + +const seed = async () => { + for (let i = 0; i < 5; i++) { + await db.product.create({data: randomProduct()}) + } + await db.user.create({data: {email: randomString(5) + "@bar.com", name: "Foobar"}}) +} + +export default seed diff --git a/examples/store/integrations/.keep b/examples/store/integrations/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/store/jobs/.keep b/examples/store/jobs/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/store/now.json b/examples/store/now.json new file mode 100644 index 0000000000..5409218c1c --- /dev/null +++ b/examples/store/now.json @@ -0,0 +1,10 @@ +{ + "env": { + "DATABASE_URL": "@store-example-db" + }, + "build": { + "env": { + "DATABASE_URL": "@store-example-db" + } + } +} diff --git a/examples/store/package.json b/examples/store/package.json new file mode 100644 index 0000000000..8bd644c0f7 --- /dev/null +++ b/examples/store/package.json @@ -0,0 +1,36 @@ +{ + "name": "@examples/store", + "version": "0.34.0-canary.0", + "private": true, + "scripts": { + "build": "blitz prisma migrate deploy && blitz build", + "cy:open": "cypress open", + "cy:run": "cypress run || cypress run", + "test:server": "prisma generate && blitz prisma migrate deploy && blitz db seed && blitz build && blitz start -p 3099", + "test": "cross-env NODE_ENV=test start-server-and-test test:server http://localhost:3099 cy:run", + "posttest": "node assert-tree-shaking-works.js" + }, + "prisma": { + "schema": "db/schema.prisma" + }, + "prettier": { + "semi": false, + "printWidth": 100, + "bracketSpacing": false, + "trailingComma": "all" + }, + "dependencies": { + "@prisma/client": "2.24.1", + "blitz": "0.45.4", + "final-form": "4.20.1", + "prisma": "2.24.1", + "react": "0.0.0-experimental-6a589ad71", + "react-dom": "0.0.0-experimental-6a589ad71", + "react-final-form": "6.5.2" + }, + "devDependencies": { + "@types/react": "17.0.2", + "cypress": "6.2.1", + "start-server-and-test": "1.11.7" + } +} diff --git a/examples/store/public/favicon.ico b/examples/store/public/favicon.ico new file mode 100755 index 0000000000..a94b0f7a7f Binary files /dev/null and b/examples/store/public/favicon.ico differ diff --git a/examples/store/public/logo.png b/examples/store/public/logo.png new file mode 100644 index 0000000000..8f3a5a5dee Binary files /dev/null and b/examples/store/public/logo.png differ diff --git a/examples/store/public/zeit.svg b/examples/store/public/zeit.svg new file mode 100644 index 0000000000..dd3916c5f0 --- /dev/null +++ b/examples/store/public/zeit.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/examples/store/tsconfig.json b/examples/store/tsconfig.json new file mode 100644 index 0000000000..f7cd39f52e --- /dev/null +++ b/examples/store/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": "./", + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "exclude": ["node_modules"], + "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"] +} diff --git a/examples/store/types b/examples/store/types new file mode 120000 index 0000000000..8788aa2845 --- /dev/null +++ b/examples/store/types @@ -0,0 +1 @@ +../../types \ No newline at end of file diff --git a/examples/store/types.d.ts b/examples/store/types.d.ts new file mode 100644 index 0000000000..be6107f7dc --- /dev/null +++ b/examples/store/types.d.ts @@ -0,0 +1,8 @@ +declare module "*.scss" { + const content: {[className: string]: string} + export default content +} +declare module "*.css" { + const content: {[className: string]: string} + export default content +} diff --git a/examples/store/types.ts b/examples/store/types.ts new file mode 100644 index 0000000000..f215677526 --- /dev/null +++ b/examples/store/types.ts @@ -0,0 +1,9 @@ +export {} + +// This should not be needed. Usually it isn't but for some reason in this example it is +declare module "react" { + interface StyleHTMLAttributes extends React.HTMLAttributes { + jsx?: boolean + global?: boolean + } +} diff --git a/examples/store/utils/.keep b/examples/store/utils/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jest-unit.config.js b/jest-unit.config.js new file mode 100644 index 0000000000..ba82415013 --- /dev/null +++ b/jest-unit.config.js @@ -0,0 +1,37 @@ +const {jsWithBabel: tsjPreset} = require("ts-jest/preset") + +module.exports = { + testEnvironment: "node", + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + modulePathIgnorePatterns: ["/tmp", "/dist", "/templates"], + moduleNameMapper: {}, + setupFilesAfterEnv: ["/jest.setup.js"], + transform: { + ...tsjPreset.transform, + }, + transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"], + testMatch: ["/**/*.(spec|test).{ts,tsx,js,jsx}"], + testURL: "http://localhost", + // watchPlugins: [ + // require.resolve("jest-watch-typeahead/filename"), + // require.resolve("jest-watch-typeahead/testname"), + // ], + coverageReporters: ["json", "lcov", "text", "clover"], + // collectCoverage: !!`Boolean(process.env.CI)`, + collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"], + coveragePathIgnorePatterns: ["/templates/"], + // coverageThreshold: { + // global: { + // branches: 100, + // functions: 100, + // lines: 100, + // statements: 100, + // }, + // }, + globals: { + "ts-jest": { + tsconfig: __dirname + "/tsconfig.test.json", + isolatedModules: true, + }, + }, +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000000..e611ad9be6 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testMatch: ["**/*.test.js", "**/*.test.ts"], + verbose: true, + rootDir: "test", + modulePaths: ["/lib"], + moduleNameMapper: { + "^lib/(.+)$": "/lib/$1", + }, + globalSetup: "/jest-global-setup.js", + globalTeardown: "/jest-global-teardown.js", + setupFilesAfterEnv: ["/jest-setup-after-env.js"], + testEnvironment: "/jest-environment.js", +} diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000000..241ef461f2 --- /dev/null +++ b/lerna.json @@ -0,0 +1,16 @@ +{ + "version": "0.45.4", + "packages": ["packages/*"], + "npmClient": "yarn", + "useWorkspaces": true, + "command": { + "version": { + "exact": true + }, + "publish": { + "npmClient": "npm", + "allowBranch": ["master", "canary"], + "registry": "https://registry.npmjs.org/" + } + } +} diff --git a/nextjs/.alexignore b/nextjs/.alexignore new file mode 100644 index 0000000000..1bf6581c26 --- /dev/null +++ b/nextjs/.alexignore @@ -0,0 +1,2 @@ +CODE_OF_CONDUCT.md +examples/ diff --git a/nextjs/.alexrc b/nextjs/.alexrc new file mode 100644 index 0000000000..157d1da8cc --- /dev/null +++ b/nextjs/.alexrc @@ -0,0 +1,21 @@ +{ + "allow": [ + "attacks", + "color", + "dead", + "execute", + "executed", + "executes", + "execution", + "executions", + "failed", + "failure", + "failures", + "fire", + "fires", + "hook", + "hooks", + "host-hostess", + "invalid" + ] +} diff --git a/nextjs/.eslintignore b/nextjs/.eslintignore new file mode 100644 index 0000000000..9a638954d0 --- /dev/null +++ b/nextjs/.eslintignore @@ -0,0 +1,28 @@ +node_modules +**/.next/** +**/_next/** +**/dist/** +e2e-tests/** +examples/with-eslint/** +examples/with-typescript-eslint-jest/** +examples/with-kea/** +examples/with-custom-babel-config/** +examples/with-flow/** +examples/with-mobx-state-tree/** +examples/with-mobx/** +packages/next/bundles/webpack/packages/*.runtime.js +packages/next/compiled/**/* +packages/react-refresh-utils/**/*.js +packages/react-dev-overlay/lib/** +**/__tmp__/** +.github/actions/next-stats-action/.work +packages/next-codemod/transforms/__testfixtures__/**/* +packages/next-codemod/transforms/__tests__/**/* +packages/next-codemod/**/*.js +packages/next-codemod/**/*.d.ts +packages/next-env/**/*.d.ts +packages/create-next-app/templates/** +test/integration/async-modules/** +test/integration/eslint/** +test-timings.json +packages/next/build/swc/tests/fixture/** diff --git a/nextjs/.eslintrc.json b/nextjs/.eslintrc.json new file mode 100644 index 0000000000..2533f310f9 --- /dev/null +++ b/nextjs/.eslintrc.json @@ -0,0 +1,288 @@ +{ + "root": true, + "parser": "@babel/eslint-parser", + "plugins": ["react", "react-hooks", "jest", "import"], + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "node": true + }, + "parserOptions": { + "requireConfigFile": false, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + }, + "babelOptions": { + "presets": ["@babel/preset-env", "@babel/preset-react"], + "caller": { + // Eslint supports top level await when a parser for it is included. We enable the parser by default for Babel. + "supportsTopLevelAwait": true + } + } + }, + "settings": { + "react": { + "version": "detect" + }, + "import/internal-regex": "^next/" + }, + "overrides": [ + { + "files": ["test/**/*.test.js"], + "extends": ["plugin:jest/recommended"], + "rules": { + "jest/expect-expect": "off", + "jest/no-disabled-tests": "off", + "jest/no-conditional-expect": "off", + "jest/valid-title": "off", + "jest/no-interpolation-in-snapshots": "off" + } + }, + { "files": ["**/__tests__/**"], "env": { "jest": true } }, + { + "files": ["**/*.ts", "**/*.tsx"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + }, + "warnOnUnsupportedTypeScriptVersion": false + }, + "plugins": ["@typescript-eslint"], + "rules": { + // Already handled by TS + "no-dupe-class-members": "off", + "no-undef": "off", + + // Add TypeScript specific rules (and turn off ESLint equivalents) + "@typescript-eslint/consistent-type-assertions": "warn", + "no-array-constructor": "off", + "@typescript-eslint/no-array-constructor": "warn", + "@typescript-eslint/no-namespace": "error", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": [ + "warn", + { + "functions": false, + "classes": false, + "variables": false, + "typedefs": false + } + ], + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "none", + "ignoreRestSiblings": true + } + ], + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": [ + "error", + { + "allowShortCircuit": true, + "allowTernary": true, + "allowTaggedTemplates": true + } + ], + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": "warn" + } + }, + { + "files": [ + "test/**/*", + "examples/**/*", + "packages/create-next-app/templates/**/*" + ], + "rules": { "react/react-in-jsx-scope": "off" } + }, + { + "files": ["examples/**/*"], + "rules": { + "import/no-anonymous-default-export": [ + "error", + { + // React components: + "allowArrowFunction": false, + "allowAnonymousClass": false, + "allowAnonymousFunction": false, + + // Non-React stuff: + "allowArray": true, + "allowCallExpression": true, + "allowLiteral": true, + "allowObject": true + } + ] + } + }, + { + "files": ["packages/**"], + "rules": { + "no-shadow": ["warn", { "builtinGlobals": false }], + "import/no-extraneous-dependencies": [ + "off", + { "devDependencies": false } + ] + } + }, + { + "files": ["packages/**/*.tsx", "packages/**/*.ts"], + "rules": { + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "all", + "argsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ] + } + } + ], + "rules": { + "array-callback-return": "warn", + "default-case": ["warn", { "commentPattern": "^no default$" }], + "dot-location": ["warn", "property"], + "eqeqeq": ["warn", "smart"], + "new-parens": "warn", + "no-array-constructor": "warn", + "no-caller": "warn", + "no-cond-assign": ["warn", "except-parens"], + "no-const-assign": "warn", + "no-control-regex": "warn", + "no-delete-var": "warn", + "no-dupe-args": "warn", + "no-dupe-class-members": "warn", + "no-dupe-keys": "warn", + "no-duplicate-case": "warn", + "no-empty-character-class": "warn", + "no-empty-pattern": "warn", + "no-eval": "warn", + "no-ex-assign": "warn", + "no-extend-native": "warn", + "no-extra-bind": "warn", + "no-extra-label": "warn", + "no-fallthrough": "warn", + "no-func-assign": "warn", + "no-implied-eval": "warn", + "no-invalid-regexp": "warn", + "no-iterator": "warn", + "no-label-var": "warn", + "no-labels": ["warn", { "allowLoop": true, "allowSwitch": false }], + "no-lone-blocks": "warn", + "no-loop-func": "warn", + "no-mixed-operators": [ + "warn", + { + "groups": [ + ["&", "|", "^", "~", "<<", ">>", ">>>"], + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||"], + ["in", "instanceof"] + ], + "allowSamePrecedence": false + } + ], + "no-multi-str": "warn", + "no-native-reassign": "warn", + "no-negated-in-lhs": "warn", + "no-new-func": "warn", + "no-new-object": "warn", + "no-new-symbol": "warn", + "no-new-wrappers": "warn", + "no-obj-calls": "warn", + "no-octal": "warn", + "no-octal-escape": "warn", + "no-redeclare": ["off"], //blitz + "no-regex-spaces": "warn", + "no-restricted-syntax": ["warn", "WithStatement"], + "no-script-url": "warn", + "no-self-assign": "warn", + "no-self-compare": "warn", + "no-sequences": "warn", + "no-shadow-restricted-names": "warn", + "no-sparse-arrays": "warn", + "no-template-curly-in-string": "error", + "no-this-before-super": "warn", + "no-throw-literal": "warn", + "no-undef": "error", + "no-unexpected-multiline": "warn", + "no-unreachable": "warn", + "no-unused-expressions": [ + "error", + { + "allowShortCircuit": true, + "allowTernary": true, + "allowTaggedTemplates": true + } + ], + "no-unused-labels": "warn", + "no-unused-vars": [ + "warn", + { + "args": "none", + "ignoreRestSiblings": true + } + ], + "no-use-before-define": [ + "warn", + { + "functions": false, + "classes": false, + "variables": false + } + ], + "no-useless-computed-key": "warn", + "no-useless-concat": "warn", + "no-useless-constructor": "warn", + "no-useless-escape": "warn", + "no-useless-rename": [ + "warn", + { + "ignoreDestructuring": false, + "ignoreImport": false, + "ignoreExport": false + } + ], + "no-with": "warn", + "no-whitespace-before-property": "warn", + "react-hooks/exhaustive-deps": "warn", + "require-yield": "warn", + "rest-spread-spacing": ["warn", "never"], + "strict": ["warn", "never"], + "unicode-bom": ["warn", "never"], + "use-isnan": "warn", + "valid-typeof": "warn", + "getter-return": "warn", + "react/forbid-foreign-prop-types": ["warn", { "allowInPropTypes": true }], + "react/jsx-no-comment-textnodes": "warn", + "react/jsx-no-duplicate-props": "warn", + "react/jsx-no-target-blank": "warn", + "react/jsx-no-undef": "error", + "react/jsx-pascal-case": [ + "warn", + { + "allowAllCaps": true, + "ignore": [] + } + ], + "react/jsx-uses-react": "warn", + "react/jsx-uses-vars": "warn", + "react/no-danger-with-children": "warn", + "react/no-deprecated": "warn", + "react/no-direct-mutation-state": "warn", + "react/no-is-mounted": "warn", + "react/no-typos": "error", + "react/react-in-jsx-scope": "error", + "react/require-render-return": "error", + "react/style-prop-object": "warn", + "react-hooks/rules-of-hooks": "error" + } +} diff --git a/nextjs/.gitattributes b/nextjs/.gitattributes new file mode 100644 index 0000000000..73a97e3acb --- /dev/null +++ b/nextjs/.gitattributes @@ -0,0 +1 @@ +packages/next/compiled/** merge=keep-ours diff --git a/nextjs/.github/.kodiak.toml b/nextjs/.github/.kodiak.toml new file mode 100644 index 0000000000..a90b3113f6 --- /dev/null +++ b/nextjs/.github/.kodiak.toml @@ -0,0 +1,18 @@ +# .kodiak.toml +version = 1 + +[merge] +automerge_label = "ready to land" +require_automerge_label = false +method = "squash" +delete_branch_on_merge = true +optimistic_updates = true +prioritize_ready_to_merge = true +notify_on_conflict = false + +[merge.message] +title = "pull_request_title" +body = "pull_request_body" +include_pr_number = true +body_type = "markdown" +strip_html_comments = true diff --git a/nextjs/.github/CODEOWNERS b/nextjs/.github/CODEOWNERS new file mode 100644 index 0000000000..5b64af494d --- /dev/null +++ b/nextjs/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# Learn how to add code owners here: +# https://help.github.com/en/articles/about-code-owners + +* @timneutkens @ijjk @shuding @styfle @huozhi @padmaia +/docs/ @timneutkens @ijjk @shuding @styfle @huozhi @padmaia @leerob @lfades +/examples/ @timneutkens @ijjk @shuding @leerob @lfades diff --git a/nextjs/.github/ISSUE_TEMPLATE/1.bug_report.yml b/nextjs/.github/ISSUE_TEMPLATE/1.bug_report.yml new file mode 100644 index 0000000000..440f9434bf --- /dev/null +++ b/nextjs/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -0,0 +1,73 @@ +name: Bug Report +description: Create a bug report for the Next.js core +labels: 'template: bug' +body: + - type: markdown + attributes: + value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible. + - type: markdown + attributes: + value: If you leave out sections there is a high likelihood it will be moved to the GitHub Discussions "Help" section. + - type: markdown + attributes: + value: 'Please first verify if your issue exists in the Next.js canary release line: `npm install next@canary`.' + - type: markdown + attributes: + value: 'next@canary is the beta version of Next.js. It includes all features and fixes that are pending to land on the stable release line.' + - type: input + attributes: + label: What version of Next.js are you using? + description: 'For example: 10.0.1' + validations: + required: true + - type: input + attributes: + label: What version of Node.js are you using? + description: 'For example: 12.0.0' + validations: + required: true + - type: input + attributes: + label: What browser are you using? + description: 'For example: Chrome, Safari' + validations: + required: true + - type: input + attributes: + label: What operating system are you using? + description: 'For example: macOS, Windows' + validations: + required: true + - type: input + attributes: + label: How are you deploying your application? + description: 'For example: next start, next export, Vercel, Other platform' + validations: + required: true + - type: textarea + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce the behavior, please provide a clear code snippets that always reproduces the issue or a GitHub repository. Screenshots can be provided in the issue body below. + validations: + required: true + - type: markdown + attributes: + value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. + - type: markdown + attributes: + value: Contributors should be able to follow the steps provided in order to reproduce the bug. + - type: markdown + attributes: + value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance! diff --git a/nextjs/.github/ISSUE_TEMPLATE/2.example_bug_report.yml b/nextjs/.github/ISSUE_TEMPLATE/2.example_bug_report.yml new file mode 100644 index 0000000000..8535a441bf --- /dev/null +++ b/nextjs/.github/ISSUE_TEMPLATE/2.example_bug_report.yml @@ -0,0 +1,73 @@ +name: Example Bug Report +description: Create a bug report for the examples +labels: 'type: example,template: bug' +body: + - type: markdown + attributes: + value: Thanks for taking the time to file a examples bug report! Please fill out this form as completely as possible. + - type: markdown + attributes: + value: If you leave out sections there is a high likelihood it will be moved to the GitHub Discussions "Help" section. + - type: input + attributes: + label: What example does this report relate to? + description: 'For example: with-styled-components' + validations: + required: true + - type: input + attributes: + label: What version of Next.js are you using? + description: 'For example: 10.0.1' + validations: + required: true + - type: input + attributes: + label: What version of Node.js are you using? + description: 'For example: 12.0.0' + validations: + required: true + - type: input + attributes: + label: What browser are you using? + description: 'For example: Chrome, Safari' + validations: + required: true + - type: input + attributes: + label: What operating system are you using? + description: 'For example: macOS, Windows' + validations: + required: true + - type: input + attributes: + label: How are you deploying your application? + description: 'For example: next start, next export, Vercel, Other platform' + validations: + required: true + - type: textarea + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce the behavior, please provide a clear code snippets that always reproduces the issue or a GitHub repository. Screenshots can be provided in the issue body below. + validations: + required: true + - type: markdown + attributes: + value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. + - type: markdown + attributes: + value: Contributors should be able to follow the steps provided in order to reproduce the bug. + - type: markdown + attributes: + value: Thanks in advance! diff --git a/nextjs/.github/ISSUE_TEMPLATE/3.feature_request.yml b/nextjs/.github/ISSUE_TEMPLATE/3.feature_request.yml new file mode 100644 index 0000000000..2655aff44d --- /dev/null +++ b/nextjs/.github/ISSUE_TEMPLATE/3.feature_request.yml @@ -0,0 +1,28 @@ +name: Feature Request +description: Create a feature request for the Next.js core +labels: 'template: story' +body: + - type: markdown + attributes: + value: Thanks for taking the time to file a feature request! Please fill out this form as completely as possible. + - type: markdown + attributes: + value: 'Feature requests will be converted to the GitHub Discussions "Ideas" section.' + - type: textarea + attributes: + label: Describe the feature you'd like to request + description: A clear and concise description of what you want and what your use case is. + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true diff --git a/nextjs/.github/ISSUE_TEMPLATE/config.yml b/nextjs/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..5cb26d8d13 --- /dev/null +++ b/nextjs/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/vercel/next.js/discussions + about: Ask questions and discuss with other community members diff --git a/nextjs/.github/actions/next-stats-action/.gitignore b/nextjs/.github/actions/next-stats-action/.gitignore new file mode 100644 index 0000000000..d1de150893 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/.gitignore @@ -0,0 +1,3 @@ +**/node_modules +out.md +.work \ No newline at end of file diff --git a/nextjs/.github/actions/next-stats-action/Dockerfile b/nextjs/.github/actions/next-stats-action/Dockerfile new file mode 100644 index 0000000000..f533f918d9 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/Dockerfile @@ -0,0 +1,19 @@ +FROM node:14-buster + +LABEL com.github.actions.name="Next.js PR Stats" +LABEL com.github.actions.description="Compares stats of a PR with the main branch" +LABEL repository="https://github.com/vercel/next-stats-action" + +COPY . /next-stats + +# Install node_modules +RUN cd /next-stats && yarn install --production + +RUN git config --global user.email 'stats@localhost' +RUN git config --global user.name 'next stats' + +RUN apt update +RUN apt install apache2-utils -y + +COPY entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/nextjs/.github/actions/next-stats-action/README.md b/nextjs/.github/actions/next-stats-action/README.md new file mode 100644 index 0000000000..566a6296a6 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/README.md @@ -0,0 +1,93 @@ +# Next.js Stats GitHub Action + +> Downloads and runs project with provided configs gathering stats to compare branches + +See it in action at Next.js https://github.com/vercel/next.js + +## Getting Started + +1. Add a `.stats-app` folder to your project with a [`stats-config.js`](#stats-config) and any files to run against for example a test app that is to be built +2. Add the action to your [workflow](https://help.github.com/en/articles/configuring-a-workflow) +3. Enjoy the stats + +## Stats Config + +```TypeScript +const StatsConfig = { + // the Heading to show at the top of stats comments + commentHeading: 'Stats from current PR' | undefined, + commentReleaseHeading: 'Stats from current release' | undefined, + // the command to build your project if not done on post install + initialBuildCommand: undefined | string, + skipInitialInstall: undefined | boolean, + // the command to build the app (app source should be in `.stats-app`) + appBuildCommand: string, + appStartCommand: string | undefined, + // the main branch to compare against (what PRs will be merging into) + mainBranch: 'canary', + // the main repository path (relative to https://github.com/) + mainRepo: 'vercel/next.js', + // whether to attempt auto merging the main branch into PR before running stats + autoMergeMain: boolean | undefined, + // an array of configs for each run + configs: [ + { // first run's config + // title of the run + title: 'fastMode stats', + // whether to diff the outputted files (default: onOutputChange) + diff: 'onOutputChange' | false | undefined, + // config files to add before running diff (if `undefined` uses `configFiles`) + diffConfigFiles: [] | undefined, + // renames to apply to make file names deterministic + renames: [ + { + srcGlob: 'main-*.js', + dest: 'main.js' + } + ], + // config files to add before running (removed before successive runs) + configFiles: [ + { + path: './next.config.js', + content: 'module.exports = { fastMode: true }' + } + ], + // an array of file groups to diff/track + filesToTrack: [ + { + name: 'Pages', + globs: [ + 'build/pages/**/*.js' + ] + } + ], + // an array of URLs to fetch while `appStartCommand` is running + // will be output to fetched-pages/${pathname}.html + pagesToFetch: [ + 'https://localhost:$PORT/page-1' + ] + }, + { // second run's config + title: 'slowMode stats', + diff: false, + configFiles: [ + { + path: './next.config.js', + content: 'module.exports = { slowMode: true }' + } + ], + filesToTrack: [ + { + name: 'Main Bundles', + globs: [ + 'build/runtime/webpack-*.js', + 'build/runtime/main-*.js', + ] + } + ] + }, + ] +} + +module.exports = StatsConfig +``` diff --git a/nextjs/.github/actions/next-stats-action/entrypoint.sh b/nextjs/.github/actions/next-stats-action/entrypoint.sh new file mode 100755 index 0000000000..5f5c38de22 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -eu # stop on error + +export HOME=/root + +node /next-stats/src/index.js diff --git a/nextjs/.github/actions/next-stats-action/package.json b/nextjs/.github/actions/next-stats-action/package.json new file mode 100644 index 0000000000..a9fb8c4e08 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/package.json @@ -0,0 +1,19 @@ +{ + "name": "get-stats", + "version": "1.0.0", + "main": "src/index.js", + "license": "MIT", + "dependencies": { + "async-sema": "^3.1.0", + "fs-extra": "^8.1.0", + "get-port": "^5.0.0", + "glob": "^7.1.4", + "gzip-size": "^5.1.1", + "minimatch": "^3.0.4", + "node-fetch": "^2.6.0", + "prettier": "^1.18.2", + "pretty-bytes": "^5.3.0", + "pretty-ms": "^5.0.0", + "semver": "7.3.4" + } +} diff --git a/nextjs/.github/actions/next-stats-action/src/add-comment.js b/nextjs/.github/actions/next-stats-action/src/add-comment.js new file mode 100644 index 0000000000..2f2d9afc15 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/add-comment.js @@ -0,0 +1,274 @@ +const path = require('path') +const fs = require('fs').promises +const fetch = require('node-fetch') +const prettyMs = require('pretty-ms') +const logger = require('./util/logger') +const prettyBytes = require('pretty-bytes') +const { benchTitle } = require('./constants') + +const gzipIgnoreRegex = new RegExp(`(General|^Serverless|${benchTitle})`) + +const prettify = (val, type = 'bytes') => { + if (typeof val !== 'number') return 'N/A' + return type === 'bytes' ? prettyBytes(val) : prettyMs(val) +} + +const round = (num, places) => { + const placesFactor = Math.pow(10, places) + return Math.round(num * placesFactor) / placesFactor +} + +const shortenLabel = (itemKey) => + itemKey.length > 24 + ? `${itemKey.substr(0, 12)}..${itemKey.substr(itemKey.length - 12, 12)}` + : itemKey + +const twoMB = 2 * 1024 * 1024 + +module.exports = async function addComment( + results = [], + actionInfo, + statsConfig +) { + let comment = `# ${ + actionInfo.isRelease + ? statsConfig.commentReleaseHeading || 'Stats from current release' + : statsConfig.commentHeading || 'Stats from current PR' + }\n\n` + + const tableHead = `| | ${statsConfig.mainRepo} ${statsConfig.mainBranch} ${ + actionInfo.lastStableTag || '' + } | ${actionInfo.prRepo} ${actionInfo.prRef} | Change |\n| - | - | - | - |\n` + + for (let i = 0; i < results.length; i++) { + const result = results[i] + const isLastResult = i === results.length - 1 + let resultHasIncrease = false + let resultHasDecrease = false + let resultContent = '' + + Object.keys(result.mainRepoStats).forEach((groupKey) => { + const isBenchmark = groupKey === benchTitle + const mainRepoGroup = result.mainRepoStats[groupKey] + const diffRepoGroup = result.diffRepoStats[groupKey] + const itemKeys = new Set([ + ...Object.keys(mainRepoGroup), + ...Object.keys(diffRepoGroup), + ]) + let groupTable = tableHead + let mainRepoTotal = 0 + let diffRepoTotal = 0 + let totalChange = 0 + + itemKeys.forEach((itemKey) => { + const prettyType = itemKey.match(/(length|duration)/i) ? 'ms' : 'bytes' + const isGzipItem = itemKey.endsWith('gzip') + const mainItemVal = mainRepoGroup[itemKey] + const diffItemVal = diffRepoGroup[itemKey] + const useRawValue = isBenchmark && prettyType !== 'ms' + const mainItemStr = useRawValue + ? mainItemVal + : prettify(mainItemVal, prettyType) + + const diffItemStr = useRawValue + ? diffItemVal + : prettify(diffItemVal, prettyType) + + let change = '✓' + + // Don't show gzip values for serverless as they aren't + // deterministic currently + if (groupKey.startsWith('Serverless') && isGzipItem) return + // otherwise only show gzip values + else if (!isGzipItem && !groupKey.match(gzipIgnoreRegex)) return + + if ( + !itemKey.startsWith('buildDuration') || + (isBenchmark && itemKey.match(/req\/sec/)) + ) { + if (typeof mainItemVal === 'number') mainRepoTotal += mainItemVal + if (typeof diffItemVal === 'number') diffRepoTotal += diffItemVal + } + + // calculate the change + if (mainItemVal !== diffItemVal) { + if ( + typeof mainItemVal === 'number' && + typeof diffItemVal === 'number' + ) { + change = round(diffItemVal - mainItemVal, 2) + + // check if there is still a change after rounding + if (change !== 0) { + const absChange = Math.abs(change) + const warnIfNegative = isBenchmark && itemKey.match(/req\/sec/) + const warn = warnIfNegative + ? change < 0 + ? '⚠️ ' + : '' + : change > 0 + ? '⚠️ ' + : '' + change = `${warn}${change < 0 ? '-' : '+'}${ + useRawValue ? absChange : prettify(absChange, prettyType) + }` + } + } else { + change = 'N/A' + } + } + + groupTable += `| ${ + isBenchmark ? itemKey : shortenLabel(itemKey) + } | ${mainItemStr} | ${diffItemStr} | ${change} |\n` + }) + let groupTotalChange = '' + + totalChange = diffRepoTotal - mainRepoTotal + + if (totalChange !== 0) { + if (totalChange < 0) { + resultHasDecrease = true + groupTotalChange = ` Overall decrease ${isBenchmark ? '⚠️' : '✓'}` + } else { + if ( + (groupKey !== 'General' && totalChange > 5) || + totalChange > twoMB + ) { + resultHasIncrease = true + } + groupTotalChange = ` Overall increase ${isBenchmark ? '✓' : '⚠️'}` + } + } + + if (groupKey !== 'General' && groupKey !== benchTitle) { + let totalChangeSign = '' + + if (totalChange === 0) { + totalChange = '✓' + } else { + totalChangeSign = totalChange < 0 ? '-' : '⚠️ +' + } + totalChange = `${totalChangeSign}${ + typeof totalChange === 'number' + ? prettify(Math.abs(totalChange)) + : totalChange + }` + groupTable += `| Overall change | ${prettyBytes( + round(mainRepoTotal, 2) + )} | ${prettyBytes(round(diffRepoTotal, 2))} | ${totalChange} |\n` + } + + if (itemKeys.size > 0) { + resultContent += `
\n` + resultContent += `${groupKey}${groupTotalChange}\n\n` + resultContent += groupTable + resultContent += `\n
\n\n` + } + }) + + // add diffs + if (result.diffs) { + const diffHeading = '#### Diffs\n' + let diffContent = diffHeading + + Object.keys(result.diffs).forEach((itemKey) => { + const curDiff = result.diffs[itemKey] + diffContent += `
\n` + diffContent += `Diff for ${shortenLabel( + itemKey + )}\n\n` + + if (curDiff.length > 36 * 1000) { + diffContent += 'Diff too large to display' + } else { + diffContent += `\`\`\`diff\n${curDiff}\n\`\`\`` + } + diffContent += `\n
\n` + }) + + if (diffContent !== diffHeading) { + resultContent += diffContent + } + } + let increaseDecreaseNote = '' + + if (resultHasIncrease) { + increaseDecreaseNote = ' (Increase detected ⚠️)' + } else if (resultHasDecrease) { + increaseDecreaseNote = ' (Decrease detected ✓)' + } + + comment += `
\n` + comment += `${result.title}${increaseDecreaseNote}\n\n
\n\n` + comment += resultContent + comment += '
\n' + + if (!isLastResult) { + comment += `
\n` + } + } + if (process.env.LOCAL_STATS) { + const statsPath = path.resolve('pr-stats.md') + await fs.writeFile(statsPath, comment) + console.log(`Output PR stats to ${statsPath}`) + } else { + logger('\n--stats start--\n', comment, '\n--stats end--\n') + } + + if ( + actionInfo.customCommentEndpoint || + (actionInfo.githubToken && actionInfo.commentEndpoint) + ) { + logger(`Posting results to ${actionInfo.commentEndpoint}`) + + const body = { + body: comment, + ...(!actionInfo.githubToken + ? { + isRelease: actionInfo.isRelease, + commitId: actionInfo.commitId, + issueId: actionInfo.issueId, + } + : {}), + } + + if (actionInfo.customCommentEndpoint) { + logger(`Using body ${JSON.stringify({ ...body, body: 'OMITTED' })}`) + } + + try { + const res = await fetch(actionInfo.commentEndpoint, { + method: 'POST', + headers: { + ...(actionInfo.githubToken + ? { + Authorization: `bearer ${actionInfo.githubToken}`, + } + : { + 'content-type': 'application/json', + }), + }, + body: JSON.stringify(body), + }) + + if (!res.ok) { + logger.error(`Failed to post results ${res.status}`) + try { + logger.error(await res.text()) + } catch (_) { + /* no-op */ + } + } else { + logger('Successfully posted results') + } + } catch (err) { + logger.error(`Error occurred posting results`, err) + } + } else { + logger( + `Not posting results`, + actionInfo.githubToken ? 'No comment endpoint' : 'no GitHub token' + ) + } +} diff --git a/nextjs/.github/actions/next-stats-action/src/constants.js b/nextjs/.github/actions/next-stats-action/src/constants.js new file mode 100644 index 0000000000..ba3ec39b2f --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/constants.js @@ -0,0 +1,32 @@ +const path = require('path') + +const benchTitle = 'Page Load Tests' +const workDir = path.join(__dirname, '../.work') +const mainRepoName = 'main-repo' +const diffRepoName = 'diff-repo' +const mainRepoDir = path.join(workDir, mainRepoName) +const diffRepoDir = path.join(workDir, diffRepoName) +const statsAppDir = path.join(workDir, 'stats-app') +const diffingDir = path.join(workDir, 'diff') +const yarnEnvValues = { + YARN_CACHE_FOLDER: path.join(workDir, 'yarn-cache'), +} +const allowedConfigLocations = [ + './', + '.stats-app', + 'test/.stats-app', + '.github/.stats-app', +] + +module.exports = { + benchTitle, + workDir, + diffingDir, + mainRepoName, + diffRepoName, + mainRepoDir, + diffRepoDir, + statsAppDir, + yarnEnvValues, + allowedConfigLocations, +} diff --git a/nextjs/.github/actions/next-stats-action/src/index.js b/nextjs/.github/actions/next-stats-action/src/index.js new file mode 100644 index 0000000000..64b1b1c18e --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/index.js @@ -0,0 +1,140 @@ +const path = require('path') +const fs = require('fs-extra') +const exec = require('./util/exec') +const logger = require('./util/logger') +const runConfigs = require('./run') +const addComment = require('./add-comment') +const actionInfo = require('./prepare/action-info')() +const { mainRepoDir, diffRepoDir } = require('./constants') +const loadStatsConfig = require('./prepare/load-stats-config') +const { + cloneRepo, + checkoutRef, + mergeBranch, + getCommitId, + linkPackages, + getLastStable, +} = require('./prepare/repo-setup')(actionInfo) + +const allowedActions = new Set(['synchronize', 'opened']) + +if (!allowedActions.has(actionInfo.actionName) && !actionInfo.isRelease) { + logger( + `Not running for ${actionInfo.actionName} event action on repo: ${actionInfo.prRepo} and ref ${actionInfo.prRef}` + ) + process.exit(0) +} + +;(async () => { + try { + if (await fs.pathExists(path.join(__dirname, '../SKIP_NEXT_STATS.txt'))) { + console.log( + 'SKIP_NEXT_STATS.txt file present, exiting stats generation..' + ) + process.exit(0) + } + + const { stdout: gitName } = await exec( + 'git config user.name && git config user.email' + ) + console.log('git author result:', gitName) + + // clone PR/newer repository/ref first to get settings + if (!actionInfo.skipClone) { + await cloneRepo(actionInfo.prRepo, diffRepoDir) + await checkoutRef(actionInfo.prRef, diffRepoDir) + } + + // load stats config from allowed locations + const { statsConfig, relativeStatsAppDir } = loadStatsConfig() + + if (actionInfo.isLocal && actionInfo.prRef === statsConfig.mainBranch) { + throw new Error( + `'GITHUB_REF' can not be the same as mainBranch in 'stats-config.js'.\n` + + `This will result in comparing against the same branch` + ) + } + + if (actionInfo.isLocal) { + // make sure to use local repo location instead of the + // one provided in statsConfig + statsConfig.mainRepo = actionInfo.prRepo + } + + // clone main repository/ref + if (!actionInfo.skipClone) { + await cloneRepo(statsConfig.mainRepo, mainRepoDir) + await checkoutRef(statsConfig.mainBranch, mainRepoDir) + } + /* eslint-disable-next-line */ + actionInfo.commitId = await getCommitId(diffRepoDir) + + if (!actionInfo.skipClone) { + if (actionInfo.isRelease) { + logger('Release detected, resetting mainRepo to last stable tag') + const lastStableTag = await getLastStable(mainRepoDir, actionInfo.prRef) + if (!lastStableTag) throw new Error('failed to get last stable tag') + console.log('using latestStable', lastStableTag) + await checkoutRef(lastStableTag, mainRepoDir) + + /* eslint-disable-next-line */ + actionInfo.lastStableTag = lastStableTag + /* eslint-disable-next-line */ + actionInfo.commitId = await getCommitId(diffRepoDir) + + if (!actionInfo.customCommentEndpoint) { + /* eslint-disable-next-line */ + actionInfo.commentEndpoint = `https://api.github.com/repos/${statsConfig.mainRepo}/commits/${actionInfo.commitId}/comments` + } + } else if (statsConfig.autoMergeMain) { + logger('Attempting auto merge of main branch') + await mergeBranch(statsConfig.mainBranch, mainRepoDir, diffRepoDir) + } + } + + let mainRepoPkgPaths + let diffRepoPkgPaths + + // run install/initialBuildCommand + const repoDirs = [mainRepoDir, diffRepoDir] + + for (const dir of repoDirs) { + logger(`Running initial build for ${dir}`) + if (!actionInfo.skipClone) { + let buildCommand = `cd ${dir}${ + !statsConfig.skipInitialInstall + ? ' && yarn install --network-timeout 1000000' + : '' + }` + + if (statsConfig.initialBuildCommand) { + buildCommand += ` && ${statsConfig.initialBuildCommand}` + } + // allow 5 minutes node_modules install + building all packages + // in case of noisy environment slowing down initial repo build + await exec(buildCommand, false, { timeout: 5 * 60 * 1000 }) + } + + logger(`Linking packages in ${dir}`) + const pkgPaths = await linkPackages(dir) + + if (dir === mainRepoDir) mainRepoPkgPaths = pkgPaths + else diffRepoPkgPaths = pkgPaths + } + + // run the configs and post the comment + const results = await runConfigs(statsConfig.configs, { + statsConfig, + mainRepoPkgPaths, + diffRepoPkgPaths, + relativeStatsAppDir, + }) + await addComment(results, actionInfo, statsConfig) + logger('finished') + process.exit(0) + } catch (err) { + console.error('Error occurred generating stats:') + console.error(err) + process.exit(1) + } +})() diff --git a/nextjs/.github/actions/next-stats-action/src/prepare/action-info.js b/nextjs/.github/actions/next-stats-action/src/prepare/action-info.js new file mode 100644 index 0000000000..fff1ffc955 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/prepare/action-info.js @@ -0,0 +1,99 @@ +const path = require('path') +const logger = require('../util/logger') +const { execSync } = require('child_process') +const releaseTypes = new Set(['release', 'published']) + +module.exports = function actionInfo() { + let { + ISSUE_ID, + SKIP_CLONE, + GITHUB_REF, + LOCAL_STATS, + GIT_ROOT_DIR, + GITHUB_ACTION, + COMMENT_ENDPOINT, + GITHUB_REPOSITORY, + GITHUB_EVENT_PATH, + PR_STATS_COMMENT_TOKEN, + } = process.env + + delete process.env.GITHUB_TOKEN + delete process.env.PR_STATS_COMMENT_TOKEN + + // only use custom endpoint if we don't have a token + const commentEndpoint = !PR_STATS_COMMENT_TOKEN && COMMENT_ENDPOINT + + if (LOCAL_STATS === 'true') { + const cwd = process.cwd() + const parentDir = path.join(cwd, '../..') + + if (!GITHUB_REF) { + // get the current branch name + GITHUB_REF = execSync(`cd "${cwd}" && git rev-parse --abbrev-ref HEAD`) + .toString() + .trim() + } + if (!GIT_ROOT_DIR) { + GIT_ROOT_DIR = path.join(parentDir, '/') + } + if (!GITHUB_REPOSITORY) { + GITHUB_REPOSITORY = path.relative(parentDir, cwd) + } + if (!GITHUB_ACTION) { + GITHUB_ACTION = 'opened' + } + } + + const info = { + commentEndpoint, + skipClone: SKIP_CLONE, + actionName: GITHUB_ACTION, + githubToken: PR_STATS_COMMENT_TOKEN, + customCommentEndpoint: !!commentEndpoint, + gitRoot: GIT_ROOT_DIR || 'https://github.com/', + prRepo: GITHUB_REPOSITORY, + prRef: GITHUB_REF, + isLocal: LOCAL_STATS, + commitId: null, + issueId: ISSUE_ID, + isRelease: + GITHUB_REPOSITORY === 'vercel/next.js' && + (GITHUB_REF || '').includes('canary'), + } + + // get comment + if (GITHUB_EVENT_PATH) { + const event = require(GITHUB_EVENT_PATH) + info.actionName = event.action || info.actionName + + if (releaseTypes.has(info.actionName)) { + info.isRelease = true + } else { + // Since GITHUB_REPOSITORY and REF might not match the fork + // use event data to get repository and ref info + const prData = event['pull_request'] + + if (prData) { + info.prRepo = prData.head.repo.full_name + info.prRef = prData.head.ref + info.issueId = prData.number + + if (!info.commentEndpoint) { + info.commentEndpoint = prData._links.comments || '' + } + // comment endpoint might be under `href` + if (typeof info.commentEndpoint === 'object') { + info.commentEndpoint = info.commentEndpoint.href + } + } + } + } + + logger('Got actionInfo:') + logger.json({ + ...info, + githubToken: PR_STATS_COMMENT_TOKEN ? 'found' : 'missing', + }) + + return info +} diff --git a/nextjs/.github/actions/next-stats-action/src/prepare/load-stats-config.js b/nextjs/.github/actions/next-stats-action/src/prepare/load-stats-config.js new file mode 100644 index 0000000000..7dfdfec22f --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/prepare/load-stats-config.js @@ -0,0 +1,41 @@ +const path = require('path') +const logger = require('../util/logger') +const { diffRepoDir, allowedConfigLocations } = require('../constants') + +// load stats-config +function loadStatsConfig() { + let statsConfig + let relativeStatsAppDir + + for (const configPath of allowedConfigLocations) { + try { + relativeStatsAppDir = configPath + statsConfig = require(path.join( + diffRepoDir, + configPath, + 'stats-config.js' + )) + break + } catch (_) { + /* */ + } + } + + if (!statsConfig) { + throw new Error( + `Failed to locate \`.stats-app\`, allowed locations are: ${allowedConfigLocations.join( + ', ' + )}` + ) + } + + logger( + 'Got statsConfig at', + path.join(relativeStatsAppDir, 'stats-config.js'), + statsConfig, + '\n' + ) + return { statsConfig, relativeStatsAppDir } +} + +module.exports = loadStatsConfig diff --git a/nextjs/.github/actions/next-stats-action/src/prepare/repo-setup.js b/nextjs/.github/actions/next-stats-action/src/prepare/repo-setup.js new file mode 100644 index 0000000000..534ab34229 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/prepare/repo-setup.js @@ -0,0 +1,112 @@ +const path = require('path') +const fs = require('fs-extra') +const exec = require('../util/exec') +const { remove } = require('fs-extra') +const logger = require('../util/logger') +const semver = require('semver') + +module.exports = (actionInfo) => { + return { + async cloneRepo(repoPath = '', dest = '') { + await remove(dest) + await exec(`git clone ${actionInfo.gitRoot}${repoPath} ${dest}`) + }, + async checkoutRef(ref = '', repoDir = '') { + await exec(`cd ${repoDir} && git fetch && git checkout ${ref}`) + }, + async getLastStable(repoDir = '', ref) { + const { stdout } = await exec(`cd ${repoDir} && git tag -l`) + const tags = stdout.trim().split('\n') + let lastStableTag + + for (let i = tags.length - 1; i >= 0; i--) { + const curTag = tags[i] + // stable doesn't include `-canary` or `-beta` + if (!curTag.includes('-') && !ref.includes(curTag)) { + if (!lastStableTag || semver.gt(curTag, lastStableTag)) { + lastStableTag = curTag + } + } + } + return lastStableTag + }, + async getCommitId(repoDir = '') { + const { stdout } = await exec(`cd ${repoDir} && git rev-parse HEAD`) + return stdout.trim() + }, + async resetToRef(ref = '', repoDir = '') { + await exec(`cd ${repoDir} && git reset --hard ${ref}`) + }, + async mergeBranch(ref = '', origRepoDir = '', destRepoDir = '') { + await exec(`cd ${destRepoDir} && git remote add upstream ${origRepoDir}`) + await exec(`cd ${destRepoDir} && git fetch upstream`) + + try { + await exec(`cd ${destRepoDir} && git merge upstream/${ref}`) + logger('Auto merge of main branch successful') + } catch (err) { + logger.error('Failed to auto merge main branch:', err) + + if (err.stdout && err.stdout.includes('CONFLICT')) { + await exec(`cd ${destRepoDir} && git merge --abort`) + logger('aborted auto merge') + } + } + }, + async linkPackages(repoDir = '') { + const pkgPaths = new Map() + const pkgDatas = new Map() + let pkgs + + try { + pkgs = await fs.readdir(path.join(repoDir, 'packages')) + } catch (err) { + if (err.code === 'ENOENT') { + console.log('no packages to link') + return pkgPaths + } + throw err + } + + for (const pkg of pkgs) { + const pkgPath = path.join(repoDir, 'packages', pkg) + const packedPkgPath = path.join(pkgPath, `${pkg}-packed.tgz`) + + const pkgDataPath = path.join(pkgPath, 'package.json') + const pkgData = require(pkgDataPath) + const { name } = pkgData + pkgDatas.set(name, { + pkgDataPath, + pkg, + pkgPath, + pkgData, + packedPkgPath, + }) + pkgPaths.set(name, packedPkgPath) + } + + for (const pkg of pkgDatas.keys()) { + const { pkgDataPath, pkgData } = pkgDatas.get(pkg) + + for (const pkg of pkgDatas.keys()) { + const { packedPkgPath } = pkgDatas.get(pkg) + if (!pkgData.dependencies || !pkgData.dependencies[pkg]) continue + pkgData.dependencies[pkg] = packedPkgPath + } + await fs.writeFile( + pkgDataPath, + JSON.stringify(pkgData, null, 2), + 'utf8' + ) + } + + // wait to pack packages until after dependency paths have been updated + // to the correct versions + for (const pkgName of pkgDatas.keys()) { + const { pkg, pkgPath } = pkgDatas.get(pkgName) + await exec(`cd ${pkgPath} && yarn pack -f ${pkg}-packed.tgz`) + } + return pkgPaths + }, + } +} diff --git a/nextjs/.github/actions/next-stats-action/src/run/benchmark-url.js b/nextjs/.github/actions/next-stats-action/src/run/benchmark-url.js new file mode 100644 index 0000000000..e92956a8c8 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/run/benchmark-url.js @@ -0,0 +1,32 @@ +const exec = require('../util/exec') + +const parseField = (stdout = '', field = '') => { + return stdout.split(field).pop().trim().split(/\s/).shift().trim() +} + +// benchmark a url +async function benchmarkUrl( + url = '', + options = { + reqTimeout: 60, + concurrency: 50, + numRequests: 2500, + } +) { + const { numRequests, concurrency, reqTimeout } = options + + const { stdout } = await exec( + `ab -n ${numRequests} -c ${concurrency} -s ${reqTimeout} "${url}"` + ) + const totalTime = parseFloat(parseField(stdout, 'Time taken for tests:'), 10) + const failedRequests = parseInt(parseField(stdout, 'Failed requests:'), 10) + const avgReqPerSec = parseFloat(parseField(stdout, 'Requests per second:')) + + return { + totalTime, + avgReqPerSec, + failedRequests, + } +} + +module.exports = benchmarkUrl diff --git a/nextjs/.github/actions/next-stats-action/src/run/collect-diffs.js b/nextjs/.github/actions/next-stats-action/src/run/collect-diffs.js new file mode 100644 index 0000000000..eef07739d0 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/run/collect-diffs.js @@ -0,0 +1,112 @@ +const path = require('path') +const fs = require('fs-extra') +const exec = require('../util/exec') +const glob = require('../util/glob') +const logger = require('../util/logger') +const { statsAppDir, diffingDir } = require('../constants') + +module.exports = async function collectDiffs( + filesToTrack = [], + initial = false +) { + if (initial) { + logger('Setting up directory for diffing') + // set-up diffing directory + await fs.remove(diffingDir) + await fs.mkdirp(diffingDir) + await exec(`cd ${diffingDir} && git init`) + } else { + // remove any previous files in case they won't be overwritten + const toRemove = await glob('!(.git)', { cwd: diffingDir, dot: true }) + + await Promise.all( + toRemove.map((file) => fs.remove(path.join(diffingDir, file))) + ) + } + const diffs = {} + + await Promise.all( + filesToTrack.map(async (fileGroup) => { + const { globs } = fileGroup + const curFiles = [] + + await Promise.all( + globs.map(async (pattern) => { + curFiles.push(...(await glob(pattern, { cwd: statsAppDir }))) + }) + ) + + for (let file of curFiles) { + const absPath = path.join(statsAppDir, file) + + const diffDest = path.join(diffingDir, file) + await fs.copy(absPath, diffDest) + } + + if (curFiles.length > 0) { + await exec( + `cd "${process.env.LOCAL_STATS ? process.cwd() : diffingDir}" && ` + + `yarn prettier --write ${curFiles + .map((f) => path.join(diffingDir, f)) + .join(' ')}` + ) + } + }) + ) + + await exec(`cd ${diffingDir} && git add .`, true) + + if (initial) { + await exec(`cd ${diffingDir} && git commit -m 'initial commit'`) + } else { + let { stdout: renamedFiles } = await exec( + `cd ${diffingDir} && git diff --name-status HEAD` + ) + renamedFiles = renamedFiles + .trim() + .split('\n') + .filter((line) => line.startsWith('R')) + + diffs._renames = [] + + for (const line of renamedFiles) { + const [, prev, cur] = line.split('\t') + await fs.move(path.join(diffingDir, cur), path.join(diffingDir, prev)) + diffs._renames.push({ + prev, + cur, + }) + } + + await exec(`cd ${diffingDir} && git add .`) + + let { stdout: changedFiles } = await exec( + `cd ${diffingDir} && git diff --name-only HEAD` + ) + changedFiles = changedFiles.trim().split('\n') + + for (const file of changedFiles) { + const fileKey = path.basename(file) + const hasFile = await fs.exists(path.join(diffingDir, file)) + + if (!hasFile) { + diffs[fileKey] = 'deleted' + continue + } + + try { + let { stdout } = await exec( + `cd ${diffingDir} && git diff --minimal HEAD ${file}` + ) + stdout = (stdout.split(file).pop() || '').trim() + if (stdout.length > 0) { + diffs[fileKey] = stdout + } + } catch (err) { + console.error(`Failed to diff ${file}: ${err.message}`) + diffs[fileKey] = `failed to diff` + } + } + } + return diffs +} diff --git a/nextjs/.github/actions/next-stats-action/src/run/collect-stats.js b/nextjs/.github/actions/next-stats-action/src/run/collect-stats.js new file mode 100644 index 0000000000..d01d1a5e4f --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/run/collect-stats.js @@ -0,0 +1,149 @@ +const path = require('path') +const fs = require('fs-extra') +const getPort = require('get-port') +const fetch = require('node-fetch') +const glob = require('../util/glob') +const gzipSize = require('gzip-size') +const logger = require('../util/logger') +const { spawn } = require('../util/exec') +const { parse: urlParse } = require('url') +const benchmarkUrl = require('./benchmark-url') +const { statsAppDir, diffingDir, benchTitle } = require('../constants') + +module.exports = async function collectStats( + runConfig = {}, + statsConfig = {}, + fromDiff = false +) { + const stats = { + [benchTitle]: {}, + } + const orderedStats = { + [benchTitle]: {}, + } + const curDir = fromDiff ? diffingDir : statsAppDir + + const hasPagesToFetch = + Array.isArray(runConfig.pagesToFetch) && runConfig.pagesToFetch.length > 0 + + const hasPagesToBench = + Array.isArray(runConfig.pagesToBench) && runConfig.pagesToBench.length > 0 + + if ( + !fromDiff && + statsConfig.appStartCommand && + (hasPagesToFetch || hasPagesToBench) + ) { + const port = await getPort() + const child = spawn(statsConfig.appStartCommand, { + cwd: curDir, + env: { + PORT: port, + }, + stdio: 'pipe', + }) + let exitCode = null + let logStderr = true + child.stdout.on('data', (data) => process.stdout.write(data)) + child.stderr.on('data', (data) => logStderr && process.stderr.write(data)) + + child.on('exit', (code) => { + exitCode = code + }) + // give app a second to start up + await new Promise((resolve) => setTimeout(() => resolve(), 1500)) + + if (exitCode !== null) { + throw new Error( + `Failed to run \`${statsConfig.appStartCommand}\` process exited with code ${exitCode}` + ) + } + + if (hasPagesToFetch) { + const fetchedPagesDir = path.join(curDir, 'fetched-pages') + await fs.mkdirp(fetchedPagesDir) + + for (let url of runConfig.pagesToFetch) { + url = url.replace('$PORT', port) + const { pathname } = urlParse(url) + try { + const res = await fetch(url) + if (!res.ok) { + throw new Error(`Failed to fetch ${url} got status: ${res.status}`) + } + const responseText = (await res.text()).trim() + + let fileName = pathname === '/' ? '/index' : pathname + if (fileName.endsWith('/')) + fileName = fileName.substr(0, fileName.length - 1) + logger( + `Writing file to ${path.join(fetchedPagesDir, `${fileName}.html`)}` + ) + + await fs.writeFile( + path.join(fetchedPagesDir, `${fileName}.html`), + responseText, + 'utf8' + ) + } catch (err) { + logger.error(err) + } + } + } + + if (hasPagesToBench) { + // disable stderr so we don't clobber logs while benchmarking + // any pages that create logs + logStderr = false + + for (let url of runConfig.pagesToBench) { + url = url.replace('$PORT', port) + logger(`Benchmarking ${url}`) + + const results = await benchmarkUrl(url, runConfig.benchOptions) + logger(`Finished benchmarking ${url}`) + + const { pathname: key } = urlParse(url) + stats[benchTitle][`${key} failed reqs`] = results.failedRequests + stats[benchTitle][`${key} total time (seconds)`] = results.totalTime + + stats[benchTitle][`${key} avg req/sec`] = results.avgReqPerSec + } + } + child.kill() + } + + for (const fileGroup of runConfig.filesToTrack) { + const { name, globs } = fileGroup + const groupStats = {} + const curFiles = new Set() + + for (const pattern of globs) { + const results = await glob(pattern, { cwd: curDir, nodir: true }) + results.forEach((result) => curFiles.add(result)) + } + + for (const file of curFiles) { + const fileKey = path.basename(file) + const absPath = path.join(curDir, file) + try { + const fileInfo = await fs.stat(absPath) + groupStats[fileKey] = fileInfo.size + groupStats[`${fileKey} gzip`] = await gzipSize.file(absPath) + } catch (err) { + logger.error('Failed to get file stats', err) + } + } + stats[name] = groupStats + } + + for (const fileGroup of runConfig.filesToTrack) { + const { name } = fileGroup + orderedStats[name] = stats[name] + } + + if (stats[benchTitle]) { + orderedStats[benchTitle] = stats[benchTitle] + } + return orderedStats +} diff --git a/nextjs/.github/actions/next-stats-action/src/run/get-dir-size.js b/nextjs/.github/actions/next-stats-action/src/run/get-dir-size.js new file mode 100644 index 0000000000..291f09fe72 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/run/get-dir-size.js @@ -0,0 +1,21 @@ +const path = require('path') +const fs = require('fs-extra') + +// getDirSize recursively gets size of all files in a directory +async function getDirSize(dir, ctx = { size: 0 }) { + let subDirs = await fs.readdir(dir) + subDirs = subDirs.map((d) => path.join(dir, d)) + + await Promise.all( + subDirs.map(async (curDir) => { + const fileStat = await fs.stat(curDir) + if (fileStat.isDirectory()) { + return getDirSize(curDir, ctx) + } + ctx.size += fileStat.size + }) + ) + return ctx.size +} + +module.exports = getDirSize diff --git a/nextjs/.github/actions/next-stats-action/src/run/index.js b/nextjs/.github/actions/next-stats-action/src/run/index.js new file mode 100644 index 0000000000..e2159e9a89 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/run/index.js @@ -0,0 +1,199 @@ +const path = require('path') +const fs = require('fs-extra') +const glob = require('../util/glob') +const exec = require('../util/exec') +const logger = require('../util/logger') +const getDirSize = require('./get-dir-size') +const collectStats = require('./collect-stats') +const collectDiffs = require('./collect-diffs') +const { statsAppDir, diffRepoDir, yarnEnvValues } = require('../constants') + +async function runConfigs( + configs = [], + { statsConfig, relativeStatsAppDir, mainRepoPkgPaths, diffRepoPkgPaths }, + diffing = false +) { + const results = [] + + for (const config of configs) { + logger(`Running config: ${config.title}${diffing ? ' (diff)' : ''}`) + + let mainRepoStats + let diffRepoStats + let diffs + + for (const pkgPaths of [mainRepoPkgPaths, diffRepoPkgPaths]) { + let curStats = { + General: { + buildDuration: null, + buildDurationCached: null, + nodeModulesSize: null, + }, + } + + // if stats-config is in root of project we're analyzing + // the whole project so copy from each repo + const curStatsAppPath = path.join(diffRepoDir, relativeStatsAppDir) + + // clean statsAppDir + await fs.remove(statsAppDir) + await fs.copy(curStatsAppPath, statsAppDir) + + logger(`Copying ${curStatsAppPath} ${statsAppDir}`) + + // apply config files + for (const configFile of config.configFiles || []) { + const filePath = path.join(statsAppDir, configFile.path) + await fs.writeFile(filePath, configFile.content, 'utf8') + } + + // links local builds of the packages and installs dependencies + await linkPkgs(statsAppDir, pkgPaths) + + if (!diffing) { + curStats.General.nodeModulesSize = await getDirSize( + path.join(statsAppDir, 'node_modules') + ) + } + + const buildStart = Date.now() + await exec(`cd ${statsAppDir} && ${statsConfig.appBuildCommand}`, false, { + env: yarnEnvValues, + }) + curStats.General.buildDuration = Date.now() - buildStart + + // apply renames to get deterministic output names + for (const rename of config.renames) { + const results = await glob(rename.srcGlob, { cwd: statsAppDir }) + for (const result of results) { + let dest = rename.removeHash + ? result.replace(/(\.|-)[0-9a-f]{20}(\.|-)/g, '$1HASH$2') + : rename.dest + if (result === dest) continue + await fs.move( + path.join(statsAppDir, result), + path.join(statsAppDir, dest) + ) + } + } + + const collectedStats = await collectStats(config, statsConfig) + curStats = { + ...curStats, + ...collectedStats, + } + + const applyRenames = (renames, stats) => { + if (renames) { + for (const rename of renames) { + let { cur, prev } = rename + cur = path.basename(cur) + prev = path.basename(prev) + + Object.keys(stats).forEach((group) => { + if (stats[group][cur]) { + stats[group][prev] = stats[group][cur] + stats[group][prev + ' gzip'] = stats[group][cur + ' gzip'] + delete stats[group][cur] + delete stats[group][cur + ' gzip'] + } + }) + } + } + } + + if (mainRepoStats) { + diffRepoStats = curStats + + if (!diffing && config.diff !== false) { + for (const groupKey of Object.keys(curStats)) { + if (groupKey === 'General') continue + let changeDetected = config.diff === 'always' + + const curDiffs = await collectDiffs(config.filesToTrack) + changeDetected = changeDetected || Object.keys(curDiffs).length > 0 + + applyRenames(curDiffs._renames, diffRepoStats) + delete curDiffs._renames + + if (changeDetected) { + logger('Detected change, running diff') + diffs = await runConfigs( + [ + { + ...config, + configFiles: config.diffConfigFiles, + }, + ], + { + statsConfig, + mainRepoPkgPaths, + diffRepoPkgPaths, + relativeStatsAppDir, + }, + true + ) + delete diffs._renames + break + } + } + } + + if (diffing) { + // copy new files and get diff results + return collectDiffs(config.filesToTrack) + } + } else { + // set up diffing folder and copy initial files + await collectDiffs(config.filesToTrack, true) + + /* eslint-disable-next-line */ + mainRepoStats = curStats + } + + const secondBuildStart = Date.now() + await exec(`cd ${statsAppDir} && ${statsConfig.appBuildCommand}`, false, { + env: yarnEnvValues, + }) + curStats.General.buildDurationCached = Date.now() - secondBuildStart + } + + logger(`Finished running: ${config.title}`) + + results.push({ + title: config.title, + mainRepoStats, + diffRepoStats, + diffs, + }) + } + + return results +} + +async function linkPkgs(pkgDir = '', pkgPaths) { + await fs.remove(path.join(pkgDir, 'node_modules')) + + const pkgJsonPath = path.join(pkgDir, 'package.json') + const pkgData = require(pkgJsonPath) + + if (!pkgData.dependencies && !pkgData.devDependencies) return + + for (const pkg of pkgPaths.keys()) { + const pkgPath = pkgPaths.get(pkg) + + if (pkgData.dependencies && pkgData.dependencies[pkg]) { + pkgData.dependencies[pkg] = pkgPath + } else if (pkgData.devDependencies && pkgData.devDependencies[pkg]) { + pkgData.devDependencies[pkg] = pkgPath + } + } + await fs.writeFile(pkgJsonPath, JSON.stringify(pkgData, null, 2), 'utf8') + + await fs.remove(yarnEnvValues.YARN_CACHE_FOLDER) + await exec(`cd ${pkgDir} && yarn install`, false, { + env: yarnEnvValues, + }) +} + +module.exports = runConfigs diff --git a/nextjs/.github/actions/next-stats-action/src/util/exec.js b/nextjs/.github/actions/next-stats-action/src/util/exec.js new file mode 100644 index 0000000000..689bd5b295 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/util/exec.js @@ -0,0 +1,38 @@ +const logger = require('./logger') +const { promisify } = require('util') +const { exec: execOrig, spawn: spawnOrig } = require('child_process') + +const execP = promisify(execOrig) +const env = { + ...process.env, + GITHUB_TOKEN: '', + PR_STATS_COMMENT_TOKEN: '', +} + +function exec(command, noLog = false, opts = {}) { + if (!noLog) logger(`exec: ${command}`) + return execP(command, { + timeout: 180 * 1000, + ...opts, + env: { ...env, ...opts.env }, + }) +} + +exec.spawn = function spawn(command = '', opts = {}) { + logger(`spawn: ${command}`) + const child = spawnOrig('/bin/bash', ['-c', command], { + ...opts, + env: { + ...env, + ...opts.env, + }, + stdio: opts.stdio || 'inherit', + }) + + child.on('exit', (code, signal) => { + logger(`spawn exit (${code}, ${signal}): ${command}`) + }) + return child +} + +module.exports = exec diff --git a/nextjs/.github/actions/next-stats-action/src/util/glob.js b/nextjs/.github/actions/next-stats-action/src/util/glob.js new file mode 100644 index 0000000000..297e429897 --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/util/glob.js @@ -0,0 +1,3 @@ +const globOrig = require('glob') +const { promisify } = require('util') +module.exports = promisify(globOrig) diff --git a/nextjs/.github/actions/next-stats-action/src/util/logger.js b/nextjs/.github/actions/next-stats-action/src/util/logger.js new file mode 100644 index 0000000000..695d1ec68d --- /dev/null +++ b/nextjs/.github/actions/next-stats-action/src/util/logger.js @@ -0,0 +1,17 @@ +function logger(...args) { + console.log(...args) +} + +logger.json = (obj) => { + logger('\n', JSON.stringify(obj, null, 2), '\n') +} + +logger.error = (...args) => { + console.error(...args) +} + +logger.warn = (...args) => { + console.warn(...args) +} + +module.exports = logger diff --git a/nextjs/.github/labeler.json b/nextjs/.github/labeler.json new file mode 100644 index 0000000000..1499d280fb --- /dev/null +++ b/nextjs/.github/labeler.json @@ -0,0 +1,32 @@ +{ + "labels": { + "type: example": ["examples/**"], + "type: documentation": ["docs/**", "errors/**"], + "type: create-next-app": ["packages/create-next-app/**"], + "type: next": [ + "packages/next/**", + "packages/react-dev-overlay/**", + "packages/react-refresh-utils/**", + "packages/next-codemod/**" + ], + "created-by: Chrome Aurora": [ + { "type": "user", "pattern": "spanicker" }, + { "type": "user", "pattern": "housseindjirdeh" }, + { "type": "user", "pattern": "devknoll" }, + { "type": "user", "pattern": "janicklas-ralph" }, + { "type": "user", "pattern": "atcastle" }, + { "type": "user", "pattern": "kyliau" }, + { "type": "user", "pattern": "kara" } + ], + "created-by: Next.js team": [ + { "type": "user", "pattern": "ijjk" }, + { "type": "user", "pattern": "padmaia" }, + { "type": "user", "pattern": "huozhi" }, + { "type": "user", "pattern": "shuding" }, + { "type": "user", "pattern": "sokra" }, + { "type": "user", "pattern": "styfle" }, + { "type": "user", "pattern": "leerob" }, + { "type": "user", "pattern": "timneutkens" } + ] + } +} diff --git a/nextjs/.github/lock.yml b/nextjs/.github/lock.yml new file mode 100644 index 0000000000..a836a1e1e9 --- /dev/null +++ b/nextjs/.github/lock.yml @@ -0,0 +1,6 @@ +# Configuration for lock-threads - https://github.com/dessant/lock-threads + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 365 +# Comment to post before locking. Set to `false` to disable +lockComment: false diff --git a/nextjs/.github/pull_request_template.md b/nextjs/.github/pull_request_template.md new file mode 100644 index 0000000000..9e9b995c92 --- /dev/null +++ b/nextjs/.github/pull_request_template.md @@ -0,0 +1,24 @@ + + +## Bug + +- [ ] Related issues linked using `fixes #number` +- [ ] Integration tests added +- [ ] Errors have helpful link attached, see `contributing.md` + +## Feature + +- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. +- [ ] Related issues linked using `fixes #number` +- [ ] Integration tests added +- [ ] Documentation added +- [ ] Telemetry added. In case of a feature if it's used or not. +- [ ] Errors have helpful link attached, see `contributing.md` + +## Documentation / Examples + +- [ ] Make sure the linting passes diff --git a/nextjs/.github/workflows/build_native.yml b/nextjs/.github/workflows/build_native.yml new file mode 100644 index 0000000000..e542dd2a26 --- /dev/null +++ b/nextjs/.github/workflows/build_native.yml @@ -0,0 +1,154 @@ +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize] + paths: + - 'packages/next/build/swc/**' + +name: Build next-swc native binaries + +jobs: + build: + strategy: + matrix: + os: [ubuntu-18.04, macos-latest, windows-latest] + + name: stable - ${{ matrix.os }} - node@14 + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + + - name: Setup node + uses: actions/setup-node@v2 + with: + node-version: 14 + check-latest: true + + - name: Install + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + + - name: Cache cargo registry + uses: actions/cache@v1 + with: + path: ~/.cargo/registry + key: stable-${{ matrix.os }}-node@14-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v1 + with: + path: ~/.cargo/git + key: stable-${{ matrix.os }}-node@14-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache NPM dependencies + uses: actions/cache@v1 + with: + path: node_modules + key: npm-cache-${{ matrix.os }}-node@14-${{ hashFiles('yarn.lock') }} + + - name: 'Install dependencies' + run: yarn install --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 + + - name: 'Build' + run: yarn --cwd packages/next build-native + env: + MACOSX_DEPLOYMENT_TARGET: '10.13' + + - name: Upload artifact + uses: actions/upload-artifact@v2 + with: + name: next-swc-binaries + path: packages/next/native + + - name: Clear the cargo caches + run: | + cargo install cargo-cache --no-default-features --features ci-autoclean + cargo-cache + + build-apple-silicon: + name: stable - aarch64-apple-darwin - node@14 + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + - name: Setup node + uses: actions/setup-node@v2 + with: + node-version: 14 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + override: true + toolchain: nightly-2021-03-25 + target: aarch64-apple-darwin + + - name: Install dependencies + run: yarn install --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 + + - name: Cross build aarch64 + run: yarn --cwd packages/next build-native --target aarch64-apple-darwin + + - name: Upload artifact + uses: actions/upload-artifact@v2 + with: + name: next-swc-binaries + path: packages/next/native + + - name: Clear the cargo caches + run: | + cargo install cargo-cache --no-default-features --features ci-autoclean + cargo-cache + + commit: + needs: [build, build-apple-silicon] + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + if: ${{ github.event_name == 'workflow_dispatch' }} + - uses: actions/download-artifact@v2 + with: + name: next-swc-binaries + path: packages/next/native + if: ${{ github.event_name == 'workflow_dispatch' }} + - uses: EndBug/add-and-commit@v7 + with: + add: 'packages/next/native --force' + message: 'Build next-swc binaries' + if: ${{ github.event_name == 'workflow_dispatch' }} + + check: + needs: [build, build-apple-silicon] + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + if: ${{ github.event_name == 'pull_request' }} + - uses: actions/download-artifact@v2 + with: + name: next-swc-binaries + path: packages/next/native + if: ${{ github.event_name == 'pull_request' }} + - run: git diff --exit-code + if: ${{ github.event_name == 'pull_request' }} + + test: + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + if: ${{ github.event_name == 'pull_request' }} + - name: Install + if: ${{ github.event_name == 'pull_request' }} + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly-2021-03-25 + profile: minimal + - run: cd packages/next/build/swc && cargo test + if: ${{ github.event_name == 'pull_request' }} diff --git a/nextjs/.github/workflows/build_test_deploy.yml b/nextjs/.github/workflows/build_test_deploy.yml new file mode 100644 index 0000000000..0ee8034b1b --- /dev/null +++ b/nextjs/.github/workflows/build_test_deploy.yml @@ -0,0 +1,270 @@ +on: + push: + branches: [canary] + pull_request: + types: [opened, synchronize] + +name: Build, test, and deploy + +jobs: + build: + runs-on: ubuntu-latest + env: + NEXT_TELEMETRY_DISABLED: 1 + outputs: + docsChange: ${{ steps.docs-change.outputs.DOCS_CHANGE }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 25 + + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - run: yarn install --frozen-lockfile --check-files + - run: node run-tests.js --timings --write-timings -g 1/1 + - name: Check docs only change + run: echo ::set-output name=DOCS_CHANGE::$(node skip-docs-change.js echo 'not-docs-only-change') + id: docs-change + - run: echo ${{steps.docs-change.outputs.DOCS_CHANGE}} + - uses: actions/cache@v2 + id: cache-build + with: + path: ./* + key: ${{ github.sha }} + + lint: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + - run: ./scripts/check-manifests.js + - run: yarn lint + + checkPrecompiled: + name: Check Pre-compiled + runs-on: ubuntu-latest + needs: build + env: + NEXT_TELEMETRY_DISABLED: 1 + steps: + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + - run: ./scripts/check-pre-compiled.sh + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + testUnit: + name: Test Unit + runs-on: ubuntu-latest + needs: build + env: + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + steps: + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + + - run: node run-tests.js --timings --type unit -g 1/1 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + testIntegration: + name: Test Integration + runs-on: ubuntu-latest + needs: build + env: + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + strategy: + fail-fast: false + matrix: + group: [1, 2, 3, 4, 5, 6] + steps: + - run: echo ${{needs.build.outputs.docsChange}} + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + + # TODO: remove after we fix watchpack watching too much + - run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + - run: xvfb-run node run-tests.js --timings -g ${{ matrix.group }}/6 -c 3 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + testElectron: + name: Test Electron + runs-on: ubuntu-latest + needs: build + env: + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + TEST_ELECTRON: 1 + steps: + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + + # TODO: remove after we fix watchpack watching too much + - run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + - run: cd test/integration/with-electron/app && yarn + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + - run: xvfb-run node run-tests.js test/integration/with-electron/test/index.test.js + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + testYarnPnP: + runs-on: ubuntu-latest + needs: build + env: + NODE_OPTIONS: '--unhandled-rejections=strict' + YARN_COMPRESSION_LEVEL: '0' + steps: + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + + - run: bash ./scripts/test-pnp.sh + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + testsPass: + name: thank you, next + runs-on: ubuntu-latest + needs: [lint, checkPrecompiled, testIntegration, testUnit, testYarnPnP] + steps: + - run: exit 0 + + testLegacyWebpack: + name: Webpack 4 (Basic, Production, Acceptance) + runs-on: ubuntu-latest + needs: build + env: + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + NEXT_PRIVATE_TEST_WEBPACK4_MODE: 1 + + steps: + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + + - run: xvfb-run node run-tests.js test/integration/{basic,fallback-modules,link-ref,production,async-modules,font-optimization,ssr-ctx}/test/index.test.js test/acceptance/*.test.js + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + testFirefox: + name: Test Firefox (production) + runs-on: ubuntu-latest + needs: build + env: + HEADLESS: true + BROWSER_NAME: 'firefox' + NEXT_TELEMETRY_DISABLED: 1 + steps: + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + - run: node run-tests.js -c 1 test/integration/production/test/index.test.js + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + testSafari: + name: Test Safari (production) + runs-on: ubuntu-latest + needs: build + env: + BROWSERSTACK: true + BROWSER_NAME: 'safari' + NEXT_TELEMETRY_DISABLED: 1 + SKIP_LOCAL_SELENIUM_SERVER: true + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + steps: + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + - run: '[[ -z "$BROWSERSTACK_ACCESS_KEY" ]] && echo "Skipping for PR" || node run-tests.js -c 1 test/integration/production/test/index.test.js' + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + testSafariOld: + name: Test Safari 10.1 (nav) + runs-on: ubuntu-latest + needs: [build, testSafari] + env: + BROWSERSTACK: true + LEGACY_SAFARI: true + BROWSER_NAME: 'safari' + NEXT_TELEMETRY_DISABLED: 1 + SKIP_LOCAL_SELENIUM_SERVER: true + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + steps: + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + - run: '[[ -z "$BROWSERSTACK_ACCESS_KEY" ]] && echo "Skipping for PR" || node run-tests.js -c 1 test/integration/production-nav/test/index.test.js' + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + publishRelease: + name: Potentially publish release + runs-on: ubuntu-latest + needs: build + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + + - run: ./scripts/publish-release.sh + + releaseStats: + name: Release Stats + runs-on: ubuntu-latest + needs: [publishRelease] + steps: + - uses: actions/cache@v2 + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + - run: ./scripts/release-stats.sh + - uses: ./.github/actions/next-stats-action + env: + PR_STATS_COMMENT_TOKEN: ${{ secrets.PR_STATS_COMMENT_TOKEN }} diff --git a/nextjs/.github/workflows/cancel.yml b/nextjs/.github/workflows/cancel.yml new file mode 100644 index 0000000000..ce58d11cab --- /dev/null +++ b/nextjs/.github/workflows/cancel.yml @@ -0,0 +1,17 @@ +name: Cancel +on: + pull_request_target: + types: + - edited + - synchronize + +jobs: + cancel: + name: 'Cancel Previous Runs' + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: styfle/cancel-workflow-action@0.5.0 + with: + workflow_id: 444921, 444987 + access_token: ${{ github.token }} diff --git a/nextjs/.github/workflows/pull_request_stats.yml b/nextjs/.github/workflows/pull_request_stats.yml new file mode 100644 index 0000000000..4c4874cdb9 --- /dev/null +++ b/nextjs/.github/workflows/pull_request_stats.yml @@ -0,0 +1,19 @@ +on: + pull_request: + types: [opened, synchronize] + +name: Generate Pull Request Stats + +jobs: + stats: + name: PR Stats + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 25 + + - run: echo ::set-output name=DOCS_CHANGE::$(node skip-docs-change.js echo 'not-docs-only-change') + id: docs-change + - uses: ./.github/actions/next-stats-action + if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }} diff --git a/nextjs/.github/workflows/test_macos.yml b/nextjs/.github/workflows/test_macos.yml new file mode 100644 index 0000000000..573425c04a --- /dev/null +++ b/nextjs/.github/workflows/test_macos.yml @@ -0,0 +1,27 @@ +on: + push: + branches: [canary] + paths-ignore: + - 'bench/**' + - 'docs/**' + - 'errors/**' + - 'examples/**' + +name: Test macOS + +jobs: + testMacOS: + name: macOS (Basic, Production, Acceptance) + runs-on: macos-latest + env: + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + + steps: + - uses: actions/checkout@v2 + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - run: yarn install --frozen-lockfile --check-files || yarn install --frozen-lockfile --check-files + - run: node run-tests.js test/integration/production/test/index.test.js + - run: node run-tests.js test/integration/basic/test/index.test.js + - run: node run-tests.js test/acceptance/* diff --git a/nextjs/.github/workflows/test_react_next.yml b/nextjs/.github/workflows/test_react_next.yml new file mode 100644 index 0000000000..621fc228d7 --- /dev/null +++ b/nextjs/.github/workflows/test_react_next.yml @@ -0,0 +1,50 @@ +name: Test react@next + +jobs: + # build: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v2 + + # - run: yarn install --frozen-lockfile --check-files + # env: + # NEXT_TELEMETRY_DISABLED: 1 + + # - run: yarn upgrade react@next react-dom@next -W --dev + + # - uses: actions/cache@v2 + # id: cache-build + # with: + # path: ./* + # key: ${{ github.sha }} + + testAll: + name: Test All + runs-on: ubuntu-latest + # needs: build + env: + NEXT_TELEMETRY_DISABLED: 1 + HEADLESS: true + NEXT_PRIVATE_SKIP_SIZE_TESTS: true + NEXT_PRIVATE_REACT_ROOT: 1 + strategy: + fail-fast: false + matrix: + group: [1, 2, 3, 4, 5, 6] + steps: + # - uses: actions/cache@v2 + # id: restore-build + # with: + # path: ./* + # key: ${{ github.sha }} + + - uses: actions/checkout@v2 + + - run: yarn install --frozen-lockfile --check-files + + - run: yarn upgrade react@next react-dom@next -W --dev + + # TODO: remove after we fix watchpack watching too much + - run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + + - run: node run-tests.js --timings -g ${{ matrix.group }}/6 -c 3 diff --git a/nextjs/.gitignore b/nextjs/.gitignore new file mode 100644 index 0000000000..04a13223f0 --- /dev/null +++ b/nextjs/.gitignore @@ -0,0 +1,45 @@ +# build output +dist +.next +target + +# dependencies +node_modules +package-lock.json +yarn.lock +!/yarn.lock +test/node_modules + +# logs & pids +*.log +pids + +# coverage +.nyc_output +coverage + +# test output +test/**/out* +test/**/next-env.d.ts +test/**/blitz-env.d.ts +.DS_Store +/e2e-tests + +# Editors +**/.idea +**/.#* + +# examples +examples/**/out +examples/**/.env*.local + +pr-stats.md +test-timings.json + +# Vercel +.vercel +.now + +#blitz +/packages/next/db.json +/db.json diff --git a/nextjs/.gitrepo b/nextjs/.gitrepo new file mode 100644 index 0000000000..4f281e9bd3 --- /dev/null +++ b/nextjs/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme +; +[subrepo] + remote = git@github.com:blitz-js/next.js.git + branch = canary + commit = a29d5fd3bff5dfcda76580d26ce77135fb489f17 + parent = c261e5c98722ba47fbcbf61bfb0a5217edec550f + method = merge + cmdver = 0.4.3 diff --git a/nextjs/.node-version b/nextjs/.node-version new file mode 100644 index 0000000000..58a4133d91 --- /dev/null +++ b/nextjs/.node-version @@ -0,0 +1 @@ +16.13.0 diff --git a/nextjs/.npmrc b/nextjs/.npmrc new file mode 100644 index 0000000000..d1504dcdec --- /dev/null +++ b/nextjs/.npmrc @@ -0,0 +1,2 @@ +save-exact = true +tag-version-prefix="" diff --git a/nextjs/.prettierignore b/nextjs/.prettierignore new file mode 100644 index 0000000000..9e9bccaa01 --- /dev/null +++ b/nextjs/.prettierignore @@ -0,0 +1,22 @@ +node_modules +**/.next/** +**/_next/** +**/dist/** +packages/next/bundles/webpack/packages/*.runtime.js +packages/next/compiled/** +packages/react-refresh-utils/**/*.js +packages/react-refresh-utils/**/*.d.ts +packages/react-dev-overlay/lib/** +**/__tmp__/** +lerna.json +.github/actions/next-stats-action/.work +packages/next-codemod/transforms/__testfixtures__/**/* +packages/next-codemod/transforms/__tests__/**/* +packages/next-codemod/**/*.js +packages/next-codemod/**/*.d.ts +packages/next-env/**/*.d.ts +test-timings.json +test/**/out/** + +// Because file from nextjs upstream isn't formatted properly +nextjs/packages/next/build/webpack-config.ts diff --git a/nextjs/.prettierignore_staged b/nextjs/.prettierignore_staged new file mode 100644 index 0000000000..e888b26919 --- /dev/null +++ b/nextjs/.prettierignore_staged @@ -0,0 +1,8 @@ +**/.next/** +**/_next/** +**/dist/** +packages/next/compiled/**/* +packages/next/bundles/webpack/packages/*.runtime.js +lerna.json +packages/next-codemod/transforms/__testfixtures__/**/* +packages/next-codemod/transforms/__tests__/**/* diff --git a/nextjs/.prettierrc.json b/nextjs/.prettierrc.json new file mode 100644 index 0000000000..fd496a820e --- /dev/null +++ b/nextjs/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "semi": false +} diff --git a/nextjs/.vscode/launch.json b/nextjs/.vscode/launch.json new file mode 100644 index 0000000000..088ae3cf90 --- /dev/null +++ b/nextjs/.vscode/launch.json @@ -0,0 +1,69 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch app development", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "yarn", + "runtimeArgs": ["run", "debug", "dev", "test/integration/basic"], + "skipFiles": ["/**"], + "outFiles": ["${workspaceFolder}/packages/next/dist/**/*"], + "port": 9229 + }, + { + "name": "Launch app build", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "yarn", + "runtimeArgs": ["run", "debug", "build", "test/integration/basic"], + "skipFiles": ["/**"], + "port": 9229, + "outFiles": ["${workspaceFolder}/packages/next/dist/**/*"] + }, + { + "name": "Launch app build trace", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "yarn", + "runtimeArgs": ["run", "trace-debug", "build", "test/integration/basic"], + "skipFiles": ["/**"], + "port": 9229, + "outFiles": ["${workspaceFolder}/packages/next/dist/**/*"] + }, + { + "name": "Launch app production", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "yarn", + "runtimeArgs": ["run", "debug", "start", "test/integration/basic"], + "skipFiles": ["/**"], + "port": 9229 + }, + { + "type": "node", + "request": "attach", + "name": "Attach to existing debugger", + "port": 9229, + "skipFiles": ["/**"], + "outFiles": ["${workspaceFolder}/packages/next/dist/**/*"] + }, + { + "name": "Launch this example", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "yarn", + "runtimeArgs": ["run", "debug", "dev", "${fileDirname}"], + "skipFiles": ["/**"], + "port": 9229 + } + ] +} diff --git a/nextjs/.vscode/settings.json b/nextjs/.vscode/settings.json new file mode 100644 index 0000000000..9b9d414d6e --- /dev/null +++ b/nextjs/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "eslint.validate": [ + "javascript", + "javascriptreact", + { "language": "typescript", "autoFix": true }, + { "language": "typescriptreact", "autoFix": true } + ], + "debug.javascript.unmapMissingSources": true +} diff --git a/nextjs/CODE_OF_CONDUCT.md b/nextjs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..5b8d1a5be1 --- /dev/null +++ b/nextjs/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [coc@vercel.com](mailto:coc@vercel.com). All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/nextjs/SECURITY.md b/nextjs/SECURITY.md new file mode 100644 index 0000000000..294bff136e --- /dev/null +++ b/nextjs/SECURITY.md @@ -0,0 +1 @@ +Visit https://vercel.com/security to view the disclosure policy. diff --git a/nextjs/UPGRADING.md b/nextjs/UPGRADING.md new file mode 100644 index 0000000000..83373e6ae0 --- /dev/null +++ b/nextjs/UPGRADING.md @@ -0,0 +1 @@ +This document has been moved to [nextjs.org/docs/upgrading](https://nextjs.org/docs/upgrading). It's also available in this repository on [/docs/upgrading.md](/docs/upgrading.md). diff --git a/nextjs/azure-pipelines.yml b/nextjs/azure-pipelines.yml new file mode 100644 index 0000000000..37fd3bc6bd --- /dev/null +++ b/nextjs/azure-pipelines.yml @@ -0,0 +1,149 @@ +trigger: + # Only run latest commit for branches: + batch: true + # Do not run Azure CI for docs-only/example-only changes: + paths: + include: + - '*' + exclude: + - bench + - docs + - errors + - examples + # Do not run Azure on `canary`, `master`, or release tags. This unnecessarily + # increases the backlog, and the change was already tested on the PR. + branches: + include: + - '*' + exclude: + - canary + - master + - refs/tags/* + +pr: + # Do not run Azure CI for docs-only/example-only changes: + paths: + include: + - '*' + exclude: + - bench + - docs + - errors + - examples + +variables: + YARN_CACHE_FOLDER: $(Pipeline.Workspace)/.yarn + NEXT_TELEMETRY_DISABLED: '1' + node_version: ^12.0.0 + +stages: + - stage: Build + jobs: + - job: build + pool: + vmImage: 'windows-2019' + steps: + - script: echo $(Agent.BuildDirectory) + - script: dir + - script: dir $(System.DefaultWorkingDirectory) + - script: echo $(Build.SourceVersion) + - powershell: Get-MpComputerStatus + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: 'Install Node.js' + - task: Cache@2 + inputs: + # use deterministic cache key that is specific + # to this test run + key: $(Build.SourceVersion) + path: $(System.DefaultWorkingDirectory) + displayName: Cache Build + - script: | + yarn install --frozen-lockfile --check-files + displayName: 'Install dependencies' + - script: | + node run-tests.js --timings --write-timings --azure -g 1/1 + displayName: 'Fetch test timing data' + + - stage: Test + dependsOn: Build + jobs: + - job: test_ie11 + pool: + vmImage: 'windows-2019' + variables: + BROWSER_NAME: internet explorer + steps: + - checkout: none + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: 'Install Node.js' + - task: Cache@2 + inputs: + # use deterministic cache key that is specific + # to this test run + key: $(Build.SourceVersion) + path: $(System.DefaultWorkingDirectory) + displayName: Cache Build + - script: | + node run-tests.js -c 1 test/integration/production/test/index.test.js test/integration/css-client-nav/test/index.test.js test/integration/rewrites-has-condition/test/index.test.js + displayName: 'Run tests' + + - job: test_unit + pool: + vmImage: 'windows-2019' + steps: + - checkout: none + - script: | + wmic datafile where name="C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" get Version /value + displayName: 'List Chrome version' + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: 'Install Node.js' + - task: Cache@2 + inputs: + # use deterministic cache key that is specific + # to this test run + key: $(Build.SourceVersion) + path: $(System.DefaultWorkingDirectory) + displayName: Cache Build + - script: | + node run-tests.js -g 1/1 --timings --azure --type unit + displayName: 'Run tests' + # TODO: investigate re-enabling when stability matches running in + # tests in ubuntu environment + # - job: test_chrome_integration + # pool: + # vmImage: 'windows-2019' + # strategy: + # matrix: + # nodejs-1: + # group: 1/4 + # nodejs-2: + # group: 2/4 + # nodejs-3: + # group: 3/4 + # nodejs-4: + # group: 4/4 + # steps: + # - checkout: none + # - script: | + # wmic datafile where name="C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" get Version /value + # displayName: 'List Chrome version' + # - task: NodeTool@0 + # inputs: + # versionSpec: $(node_version) + # displayName: 'Install Node.js' + # - task: Cache@2 + # inputs: + # # use deterministic cache key that is specific + # # to this test run + # key: $(Build.SourceVersion) + # path: $(System.DefaultWorkingDirectory) + # displayName: Cache Build + # - script: | + # node run-tests.js -g $(group) --timings --azure + # displayName: 'Run tests' diff --git a/nextjs/bench/capture-trace.js b/nextjs/bench/capture-trace.js new file mode 100644 index 0000000000..d7419745c3 --- /dev/null +++ b/nextjs/bench/capture-trace.js @@ -0,0 +1,66 @@ +import { createServer } from 'http' +import { writeFileSync } from 'fs' + +const PORT = 9411 +const HOST = '0.0.0.0' + +const traces = [] + +const onReady = () => console.log(`Listening on http://${HOST}:${PORT}`) +const onRequest = async (req, res) => { + if ( + req.method !== 'POST' || + req.url !== '/api/v2/spans' || + (req.headers && req.headers['content-type']) !== 'application/json' + ) { + res.writeHead(200) + return res.end() + } + + try { + const body = JSON.parse(await getBody(req)) + for (const traceEvent of body) { + traces.push(traceEvent) + } + res.writeHead(200) + } catch (err) { + console.warn(err) + res.writeHead(500) + } + + res.end() +} + +const getBody = (req) => + new Promise((resolve, reject) => { + let data = '' + req.on('data', (chunk) => { + data += chunk + }) + req.on('end', () => { + if (!req.complete) { + return reject('Connection terminated before body was received.') + } + resolve(data) + }) + req.on('aborted', () => reject('Connection aborted.')) + req.on('error', () => reject('Connection error.')) + }) + +const main = () => { + const args = process.argv.slice(2) + const outFile = args[0] || `./trace-${Date.now()}.json` + + process.on('SIGINT', () => { + console.log(`\nSaving to ${outFile}...`) + writeFileSync(outFile, JSON.stringify(traces, null, 2)) + process.exit() + }) + + const server = createServer(onRequest) + server.listen(PORT, HOST, onReady) +} + +if (require.main === module) { + main() +} diff --git a/nextjs/bench/package.json b/nextjs/bench/package.json new file mode 100644 index 0000000000..d311332afd --- /dev/null +++ b/nextjs/bench/package.json @@ -0,0 +1,14 @@ +{ + "name": "next-bench", + "scripts": { + "build": "next build", + "start": "NODE_ENV=production npm run build && NODE_ENV=production next start", + "bench:stateless": "ab -c1 -n3000 http://0.0.0.0:3000/stateless", + "bench:stateless-big": "ab -c1 -n500 http://0.0.0.0:3000/stateless-big", + "bench:recursive-copy": "node recursive-copy/run" + }, + "dependencies": { + "fs-extra": "10.0.0", + "recursive-copy": "2.0.11" + } +} diff --git a/nextjs/bench/pages/stateless-big.js b/nextjs/bench/pages/stateless-big.js new file mode 100644 index 0000000000..87f340d66d --- /dev/null +++ b/nextjs/bench/pages/stateless-big.js @@ -0,0 +1,13 @@ +import React from 'react' + +export default () => { + return
    {items()}
+} + +const items = () => { + var out = new Array(10000) + for (let i = 0; i < out.length; i++) { + out[i] =
  • This is row {i + 1}
  • + } + return out +} diff --git a/nextjs/bench/pages/stateless.js b/nextjs/bench/pages/stateless.js new file mode 100644 index 0000000000..620ef04822 --- /dev/null +++ b/nextjs/bench/pages/stateless.js @@ -0,0 +1,2 @@ +import React from 'react' +export default () =>

    My component!

    diff --git a/nextjs/bench/readdir/.gitignore b/nextjs/bench/readdir/.gitignore new file mode 100644 index 0000000000..116caa1278 --- /dev/null +++ b/nextjs/bench/readdir/.gitignore @@ -0,0 +1 @@ +fixtures diff --git a/nextjs/bench/readdir/create-fixtures.sh b/nextjs/bench/readdir/create-fixtures.sh new file mode 100644 index 0000000000..afda00b81d --- /dev/null +++ b/nextjs/bench/readdir/create-fixtures.sh @@ -0,0 +1,4 @@ +# Uses https://github.com/divmain/fuzzponent +mkdir fixtures +cd fixtures +fuzzponent -d 2 -s 20 diff --git a/nextjs/bench/readdir/glob.js b/nextjs/bench/readdir/glob.js new file mode 100644 index 0000000000..170f4fb050 --- /dev/null +++ b/nextjs/bench/readdir/glob.js @@ -0,0 +1,24 @@ +import { join } from 'path' +import { promisify } from 'util' +import globMod from 'glob' + +const glob = promisify(globMod) +const resolveDataDir = join(__dirname, 'fixtures', '**/*') + +async function test() { + const time = process.hrtime() + await glob(resolveDataDir) + + const hrtime = process.hrtime(time) + const nanoseconds = hrtime[0] * 1e9 + hrtime[1] + const milliseconds = nanoseconds / 1e6 + console.log(milliseconds) +} + +async function run() { + for (let i = 0; i < 50; i++) { + await test() + } +} + +run() diff --git a/nextjs/bench/readdir/recursive-readdir.js b/nextjs/bench/readdir/recursive-readdir.js new file mode 100644 index 0000000000..2167f30336 --- /dev/null +++ b/nextjs/bench/readdir/recursive-readdir.js @@ -0,0 +1,21 @@ +import { join } from 'path' +import { recursiveReadDir } from 'next/dist/lib/recursive-readdir' +const resolveDataDir = join(__dirname, 'fixtures') + +async function test() { + const time = process.hrtime() + await recursiveReadDir(resolveDataDir, /\.js$/) + + const hrtime = process.hrtime(time) + const nanoseconds = hrtime[0] * 1e9 + hrtime[1] + const milliseconds = nanoseconds / 1e6 + console.log(milliseconds) +} + +async function run() { + for (let i = 0; i < 50; i++) { + await test() + } +} + +run() diff --git a/nextjs/bench/readme.md b/nextjs/bench/readme.md new file mode 100644 index 0000000000..71c7070d8f --- /dev/null +++ b/nextjs/bench/readme.md @@ -0,0 +1,29 @@ +# Next.js server-side benchmarks + +## Installation + +Follow the steps in [contributing.md](../contributing.md) + +Both benchmarks use `ab`. So make sure you have that installed. + +## Usage + +Before running the test: + +``` +npm run start +``` + +Then run one of these tests: + +- Stateless application which renders `

    My component!

    `. Runs 3000 http requests. + +``` +npm run bench:stateless +``` + +- Stateless application which renders `
  • This is row {i}
  • ` 10.000 times. Runs 500 http requests. + +``` +npm run bench:stateless-big +``` diff --git a/nextjs/bench/recursive-copy/run.js b/nextjs/bench/recursive-copy/run.js new file mode 100644 index 0000000000..ab12258004 --- /dev/null +++ b/nextjs/bench/recursive-copy/run.js @@ -0,0 +1,59 @@ +import { join } from 'path' +import { ensureDir, outputFile, remove } from 'fs-extra' +import recursiveCopyNpm from 'recursive-copy' +import { recursiveCopy as recursiveCopyCustom } from 'next/dist/lib/recursive-copy' + +const fixturesDir = join(__dirname, 'fixtures') +const srcDir = join(fixturesDir, 'src') +const destDir = join(fixturesDir, 'dest') + +const createSrcFolder = async () => { + await ensureDir(srcDir) + + const files = new Array(100) + .fill(undefined) + .map((x, i) => + join(srcDir, `folder${i % 5}`, `folder${i + (1 % 5)}`, `file${i}`) + ) + + await Promise.all(files.map((file) => outputFile(file, 'hello'))) +} + +async function run(fn) { + async function test() { + const start = process.hrtime() + + await fn(srcDir, destDir) + + const timer = process.hrtime(start) + const ms = (timer[0] * 1e9 + timer[1]) / 1e6 + return ms + } + + const ts = [] + + for (let i = 0; i < 10; i++) { + const t = await test() + await remove(destDir) + ts.push(t) + } + + const sum = ts.reduce((a, b) => a + b) + const nb = ts.length + const avg = sum / nb + console.log({ sum, nb, avg }) +} + +async function main() { + await createSrcFolder() + + console.log('test recursive-copy npm module') + await run(recursiveCopyNpm) + + console.log('test recursive-copy custom implementation') + await run(recursiveCopyCustom) + + await remove(fixturesDir) +} + +main() diff --git a/nextjs/bench/recursive-delete/.gitignore b/nextjs/bench/recursive-delete/.gitignore new file mode 100644 index 0000000000..87dd4420af --- /dev/null +++ b/nextjs/bench/recursive-delete/.gitignore @@ -0,0 +1 @@ +fixtures-* diff --git a/nextjs/bench/recursive-delete/recursive-delete.js b/nextjs/bench/recursive-delete/recursive-delete.js new file mode 100644 index 0000000000..e23ed3e6f2 --- /dev/null +++ b/nextjs/bench/recursive-delete/recursive-delete.js @@ -0,0 +1,15 @@ +import { join } from 'path' +import { recursiveDelete } from 'next/dist/lib/recursive-delete' +const resolveDataDir = join(__dirname, `fixtures-${process.argv[2]}`) + +async function test() { + const time = process.hrtime() + await recursiveDelete(resolveDataDir) + + const hrtime = process.hrtime(time) + const nanoseconds = hrtime[0] * 1e9 + hrtime[1] + const milliseconds = nanoseconds / 1e6 + console.log(milliseconds) +} + +test() diff --git a/nextjs/bench/recursive-delete/rimraf.js b/nextjs/bench/recursive-delete/rimraf.js new file mode 100644 index 0000000000..827cdaae77 --- /dev/null +++ b/nextjs/bench/recursive-delete/rimraf.js @@ -0,0 +1,18 @@ +import { join } from 'path' +import { promisify } from 'util' +import rimrafMod from 'rimraf' + +const rimraf = promisify(rimrafMod) +const resolveDataDir = join(__dirname, `fixtures-${process.argv[2]}`, '**/*') + +async function test() { + const time = process.hrtime() + await rimraf(resolveDataDir) + + const hrtime = process.hrtime(time) + const nanoseconds = hrtime[0] * 1e9 + hrtime[1] + const milliseconds = nanoseconds / 1e6 + console.log(milliseconds) +} + +test() diff --git a/nextjs/bench/recursive-delete/run.sh b/nextjs/bench/recursive-delete/run.sh new file mode 100755 index 0000000000..40a11ea9c5 --- /dev/null +++ b/nextjs/bench/recursive-delete/run.sh @@ -0,0 +1,69 @@ +# Uses https://github.com/divmain/fuzzponent +mkdir fixtures-1 +cd fixtures-1 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "rimraf 1" +node rimraf.js 1 + +mkdir fixtures-2 +cd fixtures-2 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "rimraf 2" +node rimraf.js 2 + +mkdir fixtures-3 +cd fixtures-3 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "rimraf 3" +node rimraf.js 3 + +mkdir fixtures-4 +cd fixtures-4 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "rimraf 4" +node rimraf.js 4 + +mkdir fixtures-5 +cd fixtures-5 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "rimraf 5" +node rimraf.js 5 + +echo "-----------" + +cd fixtures-1 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "recursive delete 1" +node recursive-delete.js 1 + +cd fixtures-2 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "recursive delete 2" +node recursive-delete.js 2 + +cd fixtures-3 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "recursive delete 3" +node recursive-delete.js 3 + +cd fixtures-4 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "recursive delete 4" +node recursive-delete.js 4 + +cd fixtures-5 +fuzzponent -d 2 -s 20 > output.txt +cd .. +echo "recursive delete 5" +node recursive-delete.js 5 + +rm -r fixtures-1 fixtures-2 fixtures-3 fixtures-4 fixtures-5 \ No newline at end of file diff --git a/nextjs/contributing.md b/nextjs/contributing.md new file mode 100644 index 0000000000..bd3bbfc5ca --- /dev/null +++ b/nextjs/contributing.md @@ -0,0 +1,198 @@ +# Contributing to Next.js + +Read about our [Commitment to Open Source](https://vercel.com/oss). + +1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. +2. Create a new branch: `git checkout -b MY_BRANCH_NAME` +3. Install yarn: `npm install -g yarn` +4. Install the dependencies: `yarn` +5. Run `yarn dev` to build and watch for code changes +6. In a new terminal, run `yarn types` to compile declaration files from TypeScript +7. The development branch is `canary` (this is the branch pull requests should be made against). On a release, the relevant parts of the changes in the `canary` branch are rebased into `master`. + +> You may need to run `yarn types` again if your types get outdated. + +To contribute to [our examples](examples), take a look at the [“Adding examples” +section](#adding-examples). + +## Building + +You can build the project, including all type definitions, with: + +```bash +yarn build +# - or - +yarn prepublish +``` + +If you need to clean the project for any reason, use `yarn clean`. + +## Adding warning/error descriptions + +In Next.js we have a system to add helpful links to warnings and errors. + +This allows for the logged message to be short while giving a broader description and instructions on how to solve the warning/error. + +In general all warnings and errors added should have these links attached. + +Below are the steps to add a new link: + +- Create a new markdown file under the `errors` directory based on `errors/template.md`: `cp errors/template.md errors/.md` +- Add the newly added file to `errors/manifest.json` +- Add the following url to your warning/error: `https://nextjs.org/docs/messages/`. For example to link to `errors/api-routes-static-export.md` you use the url: `https://nextjs.org/docs/messages/api-routes-static-export` + +## To run tests + +Make sure you have `chromedriver` installed for your Chrome version. You can install it with + +- `brew install --cask chromedriver` on Mac OS X +- `chocolatey install chromedriver` on Windows +- Or manually download the version that matches your installed chrome version (if there's no match, download a version under it, but not above) from the [chromedriver repo](https://chromedriver.storage.googleapis.com/index.html) and add the binary to `/node_modules/.bin` + +You may also have to [install Rust](https://www.rust-lang.org/tools/install) and build our native packages to see all tests pass locally. We check in binaries for the most common targets and those required for CI so that most people don't have to, but if you do not see a binary for your target in `packages/next/native`, you can build it by running `yarn --cwd packages/next build-native`. If you are working on the Rust code and you need to build the binaries for ci, you can manually trigger [the workflow](https://github.com/vercel/next.js/actions/workflows/build_native.yml) to build and commit with the "Run workflow" button. + +Running all tests: + +```sh +yarn testonly +``` + +If you would like to run the tests in headless mode (with the browser windows hidden) you can do + +```sh +yarn testheadless +``` + +If you would like to use a specific Chrome/Chromium binary to run tests you can specify it with + +```sh +CHROME_BIN='path/to/chrome/bin' yarn testonly +``` + +Running a specific test suite inside of the `test/integration` directory: + +```sh +yarn testonly --testPathPattern "production" +``` + +Running one test in the `production` test suite: + +```sh +yarn testonly --testPathPattern "production" -t "should allow etag header support" +``` + +## Running the integration apps + +Running examples can be done with: + +```sh +yarn next ./test/integration/basic +# OR +yarn next ./examples/basic-css/ +``` + +To figure out which pages are available for the given example, you can run: + +```sh +EXAMPLE=./test/integration/basic +(\ + cd $EXAMPLE/pages; \ + find . -type f \ + | grep -v '\.next' \ + | sed 's#^\.##' \ + | sed 's#index\.js##' \ + | sed 's#\.js$##' \ + | xargs -I{} echo localhost:3000{} \ +) +``` + +## Running your own app with locally compiled version of Next.js + +1. Move your app inside of the Next.js monorepo. + +2. Run with `yarn next-with-deps ./app-path-in-monorepo` + +This will use the version of `next` built inside of the Next.js monorepo and the main `yarn dev` monorepo command can be running to make changes to the local Next.js version at the same time (some changes might require re-running `yarn next-with-deps` to take affect). + +or + +1. In your app's `package.json`, replace: + + ```json + "next": "", + ``` + + with: + + ```json + "next": "file:/packages/next", + ``` + +2. In your app's root directory, make sure to remove `next` from `node_modules` with: + + ```sh + rm -rf ./node_modules/next + ``` + +3. In your app's root directory, run: + + ```sh + yarn + ``` + + to re-install all of the dependencies. + + Note that Next will be copied from the locally compiled version as opposed to from being downloaded from the NPM registry. + +4. Run your application as you normally would. + +5. To update your app's dependencies, after you've made changes to your local `next` repository. In your app's root directory, run: + + ```sh + yarn install --force + ``` + +## Adding examples + +When you add an example to the [examples](examples) directory, don’t forget to add a `README.md` file with the following format: + +- Replace `DIRECTORY_NAME` with the directory name you’re adding. +- Fill in `Example Name` and `Description`. +- To add additional installation instructions, please add it where appropriate. +- To add additional notes, add `## Notes` section at the end. +- Remove the `Deploy your own` section if your example can’t be immediately deployed to Vercel. +- Remove the `Preview` section if the example doesn't work on [StackBlitz](http://stackblitz.com/) and file an issue [here](https://github.com/stackblitz/webcontainer-core). + +````markdown +# Example Name + +Description + +## Preview + +Preview the example live on [StackBlitz](http://stackblitz.com/): + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/DIRECTORY_NAME) + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/DIRECTORY_NAME&project-name=DIRECTORY_NAME&repository-name=DIRECTORY_NAME) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +npx create-next-app --example DIRECTORY_NAME DIRECTORY_NAME-app +# or +yarn create next-app --example DIRECTORY_NAME DIRECTORY_NAME-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). +```` + +## Publishing + +Repository maintainers can use `yarn publish-canary` to publish a new version of all packages to npm. diff --git a/nextjs/data.sqlite b/nextjs/data.sqlite new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/docs/advanced-features/amp-support/adding-amp-components.md b/nextjs/docs/advanced-features/amp-support/adding-amp-components.md new file mode 100644 index 0000000000..63305791d5 --- /dev/null +++ b/nextjs/docs/advanced-features/amp-support/adding-amp-components.md @@ -0,0 +1,70 @@ +--- +description: Add components from the AMP community to AMP pages, and make your pages more interactive. +--- + +# Adding AMP Components + +The AMP community provides [many components](https://amp.dev/documentation/components/) to make AMP pages more interactive. Next.js will automatically import all components used on a page and there is no need to manually import AMP component scripts: + +```jsx +export const config = { amp: true } + +function MyAmpPage() { + const date = new Date() + + return ( +
    +

    Some time: {date.toJSON()}

    + + . + +
    + ) +} + +export default MyAmpPage +``` + +The above example uses the [`amp-timeago`](https://amp.dev/documentation/components/amp-timeago/?format=websites) component. + +By default, the latest version of a component is always imported. If you want to customize the version, you can use `next/head`, as in the following example: + +```jsx +import Head from 'next/head' + +export const config = { amp: true } + +function MyAmpPage() { + const date = new Date() + + return ( +
    + + + +// or + + +
    Home Page
    +
    + ) +} + +export default Home +``` + +### Useful Links + +- [Efficiently load third-party JavaScript](https://web.dev/efficiently-load-third-party-javascript/) diff --git a/nextjs/errors/no-title-in-document-head.md b/nextjs/errors/no-title-in-document-head.md new file mode 100644 index 0000000000..771e99674d --- /dev/null +++ b/nextjs/errors/no-title-in-document-head.md @@ -0,0 +1,30 @@ +# No Title in Document Head + +### Why This Error Occurred + +A `` element was defined within the `Head` component imported from `next/document`, which should only be used for any `<head>` code that is common for all pages. Title tags should be defined at the page-level using `next/head`. + +### Possible Ways to Fix It + +Within a page or component, import and use `next/head` to define a page title: + +```jsx +import Head from 'next/head' + +export class Home { + render() { + return ( + <div> + <Head> + <title>My page title + + + ) + } +} +``` + +### Useful links + +- [next/head](https://nextjs.org/docs/api-reference/next/head) +- [Custom Document](https://nextjs.org/docs/advanced-features/custom-document) diff --git a/nextjs/errors/no-unwanted-polyfillio.md b/nextjs/errors/no-unwanted-polyfillio.md new file mode 100644 index 0000000000..6f05aa9a72 --- /dev/null +++ b/nextjs/errors/no-unwanted-polyfillio.md @@ -0,0 +1,13 @@ +# Duplicate Polyfills from Polyfill.io + +#### Why This Error Occurred + +You are using Polyfill.io and including duplicate polyfills already shipped with Next.js. This increases page weight unnecessarily which can affect loading performance. + +#### Possible Ways to Fix It + +Remove all duplicate polyfills that are included with Polyfill.io. If you need to add polyfills but are not sure if Next.js already includes it, take a look at the list of [supported browsers and features](https://nextjs.org/docs/basic-features/supported-browsers-features) first. + +### Useful Links + +- [Supported Browsers and Features](https://nextjs.org/docs/basic-features/supported-browsers-features) diff --git a/nextjs/errors/non-dynamic-getstaticpaths-usage.md b/nextjs/errors/non-dynamic-getstaticpaths-usage.md new file mode 100644 index 0000000000..2f94c774b0 --- /dev/null +++ b/nextjs/errors/non-dynamic-getstaticpaths-usage.md @@ -0,0 +1,14 @@ +# getStaticPaths Used on Non-Dynamic Page + +#### Why This Error Occurred + +On a non-dynamic SSG page `getStaticPaths` was incorrectly exported as this can only be used on dynamic pages to return the paths to prerender. + +#### Possible Ways to Fix It + +Remove the `getStaticPaths` export on the non-dynamic page or rename the page to be a dynamic page. + +### Useful Links + +- [Dynamic Routes Documentation](https://nextjs.org/docs/routing/dynamic-routes) +- [`getStaticPaths` Documentation](https://nextjs.org/docs/routing/dynamic-routes) diff --git a/nextjs/errors/non-standard-node-env.md b/nextjs/errors/non-standard-node-env.md new file mode 100644 index 0000000000..238823e622 --- /dev/null +++ b/nextjs/errors/non-standard-node-env.md @@ -0,0 +1,29 @@ +# Non-Standard NODE_ENV + +#### Why This Error Occurred + +Your environment has a non-standard `NODE_ENV` value configured. + +This may be by accident, so if you're unaware where the value is coming from, check the following: + +- The `.env*` files in your project, if present +- Your `~/.bash_profile`, if present +- Your `~/.zshrc`, if present + +The greater React ecosystem treats `NODE_ENV` as a convention, only permitting three (3) values: + +- `production`: When your application is built with `next build` +- `development`: When your application is ran with `next dev` +- `test`: When your application is being tested (e.g. `jest`) + +Setting a non-standard `NODE_ENV` value may cause dependencies to behave unexpectedly, or worse, **break dead code elimination**. + +#### Possible Ways to Fix It + +To fix this error, identify the source of the erroneous `NODE_ENV` value and get rid of it: Next.js automatically sets the correct value for you. + +If you need the concept of different environments in your application, e.g. `staging`, you should use a different environment variable name like `APP_ENV`. + +### Useful Links + +- [Environment Variables](https://en.wikipedia.org/wiki/Environment_variable) diff --git a/nextjs/errors/opt-out-auto-static-optimization.md b/nextjs/errors/opt-out-auto-static-optimization.md new file mode 100644 index 0000000000..4c2115e84b --- /dev/null +++ b/nextjs/errors/opt-out-auto-static-optimization.md @@ -0,0 +1,37 @@ +# Opt-out of Automatic Static Optimization + +#### Why This Warning Occurred + +You are using `getInitialProps` in your [Custom ``](https://nextjs.org/docs/advanced-features/custom-app). + +This causes **all non-getStaticProps pages** to be executed on the server -- disabling [Automatic Static Optimization](https://nextjs.org/docs/advanced-features/automatic-static-optimization). + +#### Possible Ways to Fix It + +Be sure you meant to use `getInitialProps` in `pages/_app`! +There are some valid use cases for this, but it is often better to handle `getInitialProps` on a _per-page_ basis. + +Check for any [higher-order components](https://reactjs.org/docs/higher-order-components.html) that may have added `getInitialProps` to your [Custom ``](https://nextjs.org/docs/advanced-features/custom-app). + +If you previously copied the [Custom ``](https://nextjs.org/docs/advanced-features/custom-app) example, you may be able to remove your `getInitialProps`. + +The following `getInitialProps` does nothing and may be removed: + +```js +class MyApp extends App { + // Remove me, I do nothing! + static async getInitialProps({ Component, ctx }) { + let pageProps = {} + + if (Component.getInitialProps) { + pageProps = await Component.getInitialProps(ctx) + } + + return { pageProps } + } + + render() { + // ... + } +} +``` diff --git a/nextjs/errors/opt-out-automatic-prerendering.md b/nextjs/errors/opt-out-automatic-prerendering.md new file mode 100644 index 0000000000..f3db86439c --- /dev/null +++ b/nextjs/errors/opt-out-automatic-prerendering.md @@ -0,0 +1,35 @@ +# Opt-out of Automatic Static Optimization + +#### Why This Warning Occurred + +You are using `getInitialProps` in your [Custom ``](https://nextjs.org/docs/advanced-features/custom-app). + +This causes **all pages** to be executed on the server -- disabling [Automatic Static Optimization](https://nextjs.org/docs/advanced-features/automatic-static-optimization). + +#### Possible Ways to Fix It + +Be sure you meant to use `getInitialProps` in `pages/_app`! +There are some valid use cases for this, but it is often better to handle `getInitialProps` on a _per-page_ basis. + +If you previously copied the [Custom ``](https://nextjs.org/docs/advanced-features/custom-app) example, you may be able to remove your `getInitialProps`. + +The following `getInitialProps` does nothing and may be removed: + +```js +class MyApp extends App { + // Remove me, I do nothing! + static async getInitialProps({ Component, ctx }) { + let pageProps = {} + + if (Component.getInitialProps) { + pageProps = await Component.getInitialProps(ctx) + } + + return { pageProps } + } + + render() { + // ... + } +} +``` diff --git a/nextjs/errors/page-data-collection-timeout.md b/nextjs/errors/page-data-collection-timeout.md new file mode 100644 index 0000000000..12332bfb43 --- /dev/null +++ b/nextjs/errors/page-data-collection-timeout.md @@ -0,0 +1,18 @@ +# Collecting page data timed out after multiple attempts + +#### Why This Error Occurred + +Next.js tries to restart the worker pool of the page data collection when no progress happens for a while, to avoid hanging builds. + +When restarted it will retry all uncompleted jobs, but if a job was unsuccessfully attempted multiple times, this will lead to an error. + +#### Possible Ways to Fix It + +- Make sure that there is no infinite loop during execution. +- Make sure all Promises in `getStaticPaths` `resolve` or `reject` correctly. +- Avoid very long timeouts for network requests. +- Increase the timeout by changing the `experimental.pageDataCollectionTimeout` configuration option (default `60` in seconds). + +### Useful Links + +- [`getStaticPaths`](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation) diff --git a/nextjs/errors/page-without-valid-component.md b/nextjs/errors/page-without-valid-component.md new file mode 100644 index 0000000000..09d94fee00 --- /dev/null +++ b/nextjs/errors/page-without-valid-component.md @@ -0,0 +1,16 @@ +# Page Without Valid React Component + +#### Why This Error Occurred + +A page that does not export a valid React Component was found while analyzing the build output. + +This is a hard error because the page would error when rendered, and causes poor build performance. + +#### Possible Ways to Fix It + +Investigate the list of page(s) specified in the error message. +For each, you'll want to check if the file is meant to be a page. + +If the file is not meant to be a page, and instead, is a shared component or file, move the file to a different folder like `components` or `lib`. + +If the file is meant to be a page, double check you have an `export default` with the React Component instead of an `export`. If you're already using `export default`, make sure the returned value is a valid React Component. diff --git a/nextjs/errors/placeholder-blur-data-url.md b/nextjs/errors/placeholder-blur-data-url.md new file mode 100644 index 0000000000..2fc53099c9 --- /dev/null +++ b/nextjs/errors/placeholder-blur-data-url.md @@ -0,0 +1,15 @@ +# `placeholder=blur` without `blurDataURL` + +#### Why This Error Occurred + +You are attempting use the `next/image` component with `placeholder=blur` property but no `blurDataURL` property. + +The `blurDataURL` might be missing because you're using a string for `src` instead of a static import. + +Or `blurDataURL` might be missing because the static import is an unsupported image format. Only jpg, png, and webp are supported at this time. + +#### Possible Ways to Fix It + +- Add a [`blurDataURL`](https://nextjs.org/docs/api-reference/next/image#blurdataurl) property, the contents should be a small Data URL to represent the image +- Change the [`src`](https://nextjs.org/docs/api-reference/next/image#src) property to a static import with one of the supported file types: jpg, png, or webp +- Remove the [`placeholder`](https://nextjs.org/docs/api-reference/next/image#placeholder) property, effectively no blur effect diff --git a/nextjs/errors/popstate-state-empty.md b/nextjs/errors/popstate-state-empty.md new file mode 100644 index 0000000000..ccd98713a5 --- /dev/null +++ b/nextjs/errors/popstate-state-empty.md @@ -0,0 +1,14 @@ +# `popstate` called with empty state + +#### Why This Error Occurred + +When using the browser back button the popstate event is triggered. Next.js sees a +`popstate` event being triggered but `event.state` did not have `url` or `as`, causing a route change failure. + +#### Possible Ways to Fix It + +The only known cause of this issue is manually manipulating `window.history` instead of using `next/router`. Starting from version 9.5, Next.js will ignore `popstate` events that contain `event.state` not created by its own router. + +### Useful Links + +- [The issue this was reported in: #4994](https://github.com/vercel/next.js/issues/4994) diff --git a/nextjs/errors/postcss-function.md b/nextjs/errors/postcss-function.md new file mode 100644 index 0000000000..83b301eb9d --- /dev/null +++ b/nextjs/errors/postcss-function.md @@ -0,0 +1,30 @@ +# PostCSS Configuration Is a Function + +#### Why This Error Occurred + +The project's custom PostCSS configuration exports a function instead of an object. + +#### Possible Ways to Fix It + +Adjust the custom PostCSS configuration to not export a function. +Instead, return a plain object—if you need environment information, read it from `process.env`. + +**Before** + +```js +module.exports = ({ env }) => ({ + plugins: { + 'postcss-plugin': env === 'production' ? {} : false, + }, +}) +``` + +**After** + +```js +module.exports = { + plugins: { + 'postcss-plugin': process.env.NODE_ENV === 'production' ? {} : false, + }, +} +``` diff --git a/nextjs/errors/postcss-ignored-plugin.md b/nextjs/errors/postcss-ignored-plugin.md new file mode 100644 index 0000000000..e8c73f1c69 --- /dev/null +++ b/nextjs/errors/postcss-ignored-plugin.md @@ -0,0 +1,20 @@ +# Ignored PostCSS Plugin + +#### Why This Error Occurred + +The project's custom PostCSS configuration attempts to configure unnecessary plugins: + +- postcss-modules-values +- postcss-modules-scope +- postcss-modules-extract-imports +- postcss-modules-local-by-default +- postcss-modules + +#### Possible Ways to Fix It + +Remove the plugin specified in the error message from your custom PostCSS configuration. + +#### How do I configure CSS Modules? + +CSS Modules are supported in [Next.js' built-in CSS support](https://nextjs.org/docs/advanced-features/customizing-postcss-config). +You can [read more about how to use CSS Modules here](https://nextjs.org/docs/advanced-features/customizing-postcss-config). diff --git a/nextjs/errors/postcss-shape.md b/nextjs/errors/postcss-shape.md new file mode 100644 index 0000000000..f121c889d0 --- /dev/null +++ b/nextjs/errors/postcss-shape.md @@ -0,0 +1,129 @@ +# Invalid PostCSS Configuration + +#### Why This Error Occurred + +PostCSS configuration was provided in an unsupported shape. + +#### Possible Ways to Fix It + +PostCSS configuration must be defined in the following shape: + +```js +module.exports = { + plugins: [ + // A plugin that does not require configuration: + 'simple-plugin-example', + + // A plugin which needs a configuration object: + [ + 'plugin-with-configuration', + { + optionA: '...', + }, + ], + + // A plugin that is toggled on or off based on environment: + [ + 'plugin-toggled', + process.env.NODE_ENV === 'production' + ? { + optionA: '...', + } + : false, + ], + + // Boolean expressions are also valid. + // `true` enables the plugin, `false` disables the plugin: + ['plugin-toggled-2', true /* a === b, etc */], + ], +} +``` + +You can [read more about configuring PostCSS in Next.js here](https://nextjs.org/docs/advanced-features/customizing-postcss-config). + +#### Common Errors + +**Before: plugin is require()'d** + +```js +const pluginA = require('postcss-plugin-a') +module.exports = { + plugins: [require('postcss-plugin'), pluginA], +} +``` + +**After** + +```js +module.exports = { + plugins: ['postcss-plugin', 'postcss-plugin-a'], +} +``` + +--- + +**Before: plugin is instantiated with configuration** + +```js +module.exports = { + plugins: [ + require('postcss-plugin')({ + optionA: '...', + }), + ], +} +``` + +**After** + +```js +module.exports = { + plugins: [ + // Pay attention to this nested array. The first index is the plugin name, + // the second index is the configuration. + [ + 'postcss-plugin', + { + optionA: '...', + }, + ], + ], +} +``` + +--- + +**Before: plugin is missing configuration** + +```js +module.exports = { + plugins: [ + [ + 'postcss-plugin-1', + { + optionA: '...', + }, + ], + // This single-entry array is detected as misconfigured because it's + // missing the second element. To fix, unwrap the value. + ['postcss-plugin-2'], + ], +} +``` + +**After** + +```js +module.exports = { + plugins: [ + [ + 'postcss-plugin-1', + { + optionA: '...', + }, + ], + // Only string: + 'postcss-plugin-2', + ], +} +``` diff --git a/nextjs/errors/prefetch-true-deprecated.md b/nextjs/errors/prefetch-true-deprecated.md new file mode 100644 index 0000000000..77733fea94 --- /dev/null +++ b/nextjs/errors/prefetch-true-deprecated.md @@ -0,0 +1,21 @@ +# `prefetch={true}` is deprecated + +#### Why This Error Occurred + +See https://nextjs.org/blog/next-9#prefetching-in-viewport-links + + will automatically prefetch pages in the background as they appear in the view. If certain pages are rarely visited you can manually set prefetch to false, here's how: + +Next.js 9 will automatically prefetch components as they appear in-viewport. + +This feature improves the responsiveness of your application by making navigations to new pages quicker. + +Next.js uses an Intersection Observer to prefetch the assets necessary in the background. + +These requests have low-priority and yield to fetch() or XHR requests. Next.js will avoid automatically prefetching if the user has data-saver enabled. + +#### Possible Ways to Fix It + +The prefetch attribute is no longer needed, when set to true, example: `prefetch={true}`, remove the property. + +Prefetching can be turned off with `prefetch={false}`. diff --git a/nextjs/errors/prerender-error.md b/nextjs/errors/prerender-error.md new file mode 100644 index 0000000000..87e301c459 --- /dev/null +++ b/nextjs/errors/prerender-error.md @@ -0,0 +1,13 @@ +# Prerender Error + +#### Why This Error Occurred + +While prerendering a page an error occurred. This can occur for many reasons from adding non-pages e.g. `components` to your `pages` folder or expecting props to be populated which are not. + +#### Possible Ways to Fix It + +- Make sure to move any non-pages out of the `pages` folder +- Check for any code that assumes a prop is available even when it might not be. e.g., have default data for all dynamic pages' props. +- Check for any out of date modules that you might be relying on +- Make sure your component handles `fallback` if it is enabled in `getStaticPaths`. [Fallback docs](https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required) +- Make sure you are not trying to export (`next export`) pages that have server-side rendering enabled [(getServerSideProps)](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) diff --git a/nextjs/errors/production-start-no-build-id.md b/nextjs/errors/production-start-no-build-id.md new file mode 100644 index 0000000000..c6c9bfdb93 --- /dev/null +++ b/nextjs/errors/production-start-no-build-id.md @@ -0,0 +1,10 @@ +# Could not find a production build + +#### Why This Error Occurred + +When running `next start` or a custom server in production mode a production build is needed. + +#### Possible Ways to Fix It + +- Run `next build` to create a production build before booting up the production server. +- If your intention was to run the development server run `next dev` instead. diff --git a/nextjs/errors/promise-in-next-config.md b/nextjs/errors/promise-in-next-config.md new file mode 100644 index 0000000000..f39ce7786f --- /dev/null +++ b/nextjs/errors/promise-in-next-config.md @@ -0,0 +1,19 @@ +# Promise In Next Config + +#### Why This Error Occurred + +The webpack function in `next.config.js` returned a promise which is not supported in Next.js. For example, below is not supported: + +```js +module.exports = { + webpack: async function (config) { + return config + }, +} +``` + +#### Possible Ways to Fix It + +Check your `next.config.js` for `async` or `return Promise` + +Potentially a plugin is returning a `Promise` from the webpack function. diff --git a/nextjs/errors/public-next-folder-conflict.md b/nextjs/errors/public-next-folder-conflict.md new file mode 100644 index 0000000000..7168b9a1c0 --- /dev/null +++ b/nextjs/errors/public-next-folder-conflict.md @@ -0,0 +1,9 @@ +# Public \_next folder Conflict + +#### Why This Error Occurred + +In your `./public` folder you added a `_next` folder which conflicts with the internal usage of `_next`. Due to this conflicting this folder name is not allowed inside of your `public` folder. + +#### Possible Ways to Fix It + +Rename the `_next` folder in your `public` folder to something else or remove it. diff --git a/nextjs/errors/react-version.md b/nextjs/errors/react-version.md new file mode 100644 index 0000000000..7132541024 --- /dev/null +++ b/nextjs/errors/react-version.md @@ -0,0 +1,51 @@ +# Minimum React Version + +#### Why This Error Occurred + +Your project is using an old version of `react` or `react-dom` that does not +meet the suggested minimum version requirement. + +Next.js suggests using, at a minimum, `react@17.0.2` and `react-dom@17.0.2`. +Older versions of `react` and `react-dom` do work with Next.js, however, they do +not enable all of Next.js' features. + +For example, the following features are not enabled with old React versions: + +- [Fast Refresh](https://nextjs.org/docs/basic-features/fast-refresh): instantly + view edits to your app without losing component state +- Component stack trace in development: see the component tree that lead up to + an error +- Hydration mismatch warnings: trace down discrepancies in your React tree that + cause performance problems + +This list is not exhaustive, but illustrative in the value of upgrading React! + +#### Possible Ways to Fix It + +**Via npm** + +```bash +npm upgrade react@latest react-dom@latest +``` + +**Via Yarn** + +```bash +yarn add react@latest react-dom@latest +``` + +**Manually** Open your `package.json` and upgrade `react` and `react-dom`: + +```json +{ + "dependencies": { + "react": "^17.0.2", + "react-dom": "^17.0.2" + } +} +``` + +### Useful Links + +- [Fast Refresh blog post](https://nextjs.org/blog/next-9-4#fast-refresh) +- [Fast Refresh docs](https://nextjs.org/docs/basic-features/fast-refresh) diff --git a/nextjs/errors/render-no-starting-slash.md b/nextjs/errors/render-no-starting-slash.md new file mode 100644 index 0000000000..dc11b24eb9 --- /dev/null +++ b/nextjs/errors/render-no-starting-slash.md @@ -0,0 +1,9 @@ +# Rendering Without Starting Slash + +#### Why This Error Occurred + +When calling `app.render(req, res, path)` the `path` did not begin with a slash (`/`). This causes unexpected behavior and should be corrected. + +#### Possible Ways to Fix It + +Make sure the `path` being passed to `app.render` always starts with a slash (`/`) diff --git a/nextjs/errors/reserved-page-prop.md b/nextjs/errors/reserved-page-prop.md new file mode 100644 index 0000000000..ae83aaabab --- /dev/null +++ b/nextjs/errors/reserved-page-prop.md @@ -0,0 +1,9 @@ +# Reserved Page Prop + +#### Why This Error Occurred + +In a page's `getInitialProps` a reserved prop was returned. Currently the only reserved page prop is `url` for legacy reasons. + +#### Possible Ways to Fix It + +Change the name of the prop returned from `getInitialProps` to any other name. diff --git a/nextjs/errors/rewrite-auto-export-fallback.md b/nextjs/errors/rewrite-auto-export-fallback.md new file mode 100644 index 0000000000..a16fa71256 --- /dev/null +++ b/nextjs/errors/rewrite-auto-export-fallback.md @@ -0,0 +1,18 @@ +# Rewriting to Auto Export or Fallback Dynamic Route + +#### Why This Error Occurred + +One of your rewrites in your `next.config.js` point to a [dynamic route](https://nextjs.org/docs/routing/dynamic-routes) that is automatically statically optimized or is a [fallback SSG page](https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required). + +Rewriting to these pages are not yet supported since rewrites are not available client-side and the dynamic route params are unable to be parsed. Support for this may be added in a future release. + +#### Possible Ways to Fix It + +For fallback SSG pages you can add the page to the list of [prerendered paths](https://nextjs.org/docs/basic-features/data-fetching#the-paths-key-required). + +For static dynamic routes, you will currently need to either rewrite to non-dynamic route or opt the page out of the static optimization with [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) + +### Useful Links + +- [Dynamic Routes Documentation](https://nextjs.org/docs/routing/dynamic-routes) +- [Fallback Documentation](https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required) diff --git a/nextjs/errors/routes-must-be-array.md b/nextjs/errors/routes-must-be-array.md new file mode 100644 index 0000000000..59b1875653 --- /dev/null +++ b/nextjs/errors/routes-must-be-array.md @@ -0,0 +1,38 @@ +# Custom Routes must return an array + +#### Why This Error Occurred + +When defining custom routes an array wasn't returned from either `headers`, `rewrites`, or `redirects`. + +#### Possible Ways to Fix It + +Make sure to return an array that contains the routes. + +**Before** + +```js +// next.config.js +module.exports = { + async rewrites() { + return { + source: '/feedback', + destination: '/feedback/general', + } + }, +} +``` + +**After** + +```js +module.exports = { + async rewrites() { + return [ + { + source: '/feedback', + destination: '/feedback/general', + }, + ] + }, +} +``` diff --git a/nextjs/errors/sharp-missing-in-production.md b/nextjs/errors/sharp-missing-in-production.md new file mode 100644 index 0000000000..01fc602b4b --- /dev/null +++ b/nextjs/errors/sharp-missing-in-production.md @@ -0,0 +1,13 @@ +# Sharp Missing In Production + +#### Why This Error Occurred + +The `next/image` component's default loader uses the ['squoosh'](https://www.npmjs.com/package/@squoosh/lib) library for image resizing and optimization. This library is quick to install and suitable for a dev server environment. For a production environment, it is strongly recommended that you install the optional [`sharp`](https://www.npmjs.com/package/sharp). This package was not detected when leveraging the Image Optimization in production mode (`next start`). + +#### Possible Ways to Fix It + +Install `sharp` by running `yarn add sharp` in your project directory. + +### Useful Links + +- [Image Optimization Documentation](https://nextjs.org/docs/basic-features/image-optimization) diff --git a/nextjs/errors/ssg-fallback-true-export.md b/nextjs/errors/ssg-fallback-true-export.md new file mode 100644 index 0000000000..9696edc817 --- /dev/null +++ b/nextjs/errors/ssg-fallback-true-export.md @@ -0,0 +1,14 @@ +# SSG `fallback: true` Export Error + +#### Why This Error Occurred + +You attempted to export a page with a `fallback: true` return value from `getStaticPaths` which is invalid. `fallback: true` is meant for building pages on-demand after a build has occurred, exporting disables this functionality + +#### Possible Ways to Fix It + +If you would like the `fallback: true` behavior, `next export` should not be used. Instead follow the [deployment documentation](https://nextjs.org/docs/deployment) to deploy your incrementally generated static site. + +### Useful Links + +- [deployment documentation](https://nextjs.org/docs/deployment#vercel-recommended) +- [`fallback: true` documentation](https://nextjs.org/docs/basic-features/data-fetching#fallback-true) diff --git a/nextjs/errors/static-dir-deprecated.md b/nextjs/errors/static-dir-deprecated.md new file mode 100644 index 0000000000..76e13986ae --- /dev/null +++ b/nextjs/errors/static-dir-deprecated.md @@ -0,0 +1,38 @@ +# Static directory is deprecated + +#### Why This Error Occurred + +In versions prior to 9.0.6 the `static` directory was used to serve static assets in a Next.js application. This has been deprecated in favor of a `public` directory. + +The reason we want to support a `public` directory instead is to not require the `/static` prefix for assets anymore and there is no reason to maintain both paths. + +#### Possible Ways to Fix It + +You can move your `static` directory inside of the `public` directory and all URLs will stay the same as they were before. + +**Before** + +``` +static/ + my-image.jpg +pages/ + index.js +components/ + my-image.js +``` + +**After** + +``` +public/ + static/ + my-image.jpg +pages/ + index.js +components/ + my-image.js +``` + +### Useful Links + +- [Static file serving docs](https://nextjs.org/docs/basic-features/static-file-serving) diff --git a/nextjs/errors/static-page-generation-timeout.md b/nextjs/errors/static-page-generation-timeout.md new file mode 100644 index 0000000000..604e036487 --- /dev/null +++ b/nextjs/errors/static-page-generation-timeout.md @@ -0,0 +1,18 @@ +# Static page generation timed out after multiple attempts + +#### Why This Error Occurred + +Next.js tries to restart the worker pool of the static page generation when no progress happens for a while, to avoid hanging builds. + +When restarted it will retry all uncompleted jobs, but if a job was unsuccessfully attempted multiple times, this will lead to an error. + +#### Possible Ways to Fix It + +- Make sure that there is no infinite loop during execution. +- Make sure all Promises in `getStaticProps` `resolve` or `reject` correctly. +- Avoid very long timeouts for network requests. +- Increase the timeout by changing the `experimental.staticPageGenerationTimeout` configuration option (default `60` in seconds). + +### Useful Links + +- [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) diff --git a/nextjs/errors/template.md b/nextjs/errors/template.md new file mode 100644 index 0000000000..711da0c52a --- /dev/null +++ b/nextjs/errors/template.md @@ -0,0 +1,13 @@ +# + +#### Why This Error Occurred + + + +#### Possible Ways to Fix It + + + +### Useful Links + + diff --git a/nextjs/errors/threw-undefined.md b/nextjs/errors/threw-undefined.md new file mode 100644 index 0000000000..0d4129ef4f --- /dev/null +++ b/nextjs/errors/threw-undefined.md @@ -0,0 +1,9 @@ +# Threw undefined in render + +#### Why This Error Occurred + +Somewhere in your code you `throw` an `undefined` value. Since this isn't a valid error there isn't a stack trace. We show this error instead to let you know what to look for. + +#### Possible Ways to Fix It + +Look in your pages and find where an error could be throwing `undefined` diff --git a/nextjs/errors/undefined-webpack-config.md b/nextjs/errors/undefined-webpack-config.md new file mode 100644 index 0000000000..fb15fdb731 --- /dev/null +++ b/nextjs/errors/undefined-webpack-config.md @@ -0,0 +1,28 @@ +# Missing webpack config + +#### Why This Error Occurred + +The value returned from the custom `webpack` function in your `next.config.js` was undefined. This can occur from the initial config value not being returned. + +#### Possible Ways to Fix It + +Make sure to return the `webpack` config from your custom `webpack` function in your `next.config.js` + +```js +// next.config.js + +module.exports = { + webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { + // Note: we provide webpack above so you should not `require` it + // Perform customizations to webpack config + config.plugins.push(new webpack.IgnorePlugin(/\/__tests__\//)) + + // Important: return the modified config + return config + }, +} +``` + +### Useful Links + +- [Custom webpack config Documentation](https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config) diff --git a/nextjs/errors/url-deprecated.md b/nextjs/errors/url-deprecated.md new file mode 100644 index 0000000000..df352d8e07 --- /dev/null +++ b/nextjs/errors/url-deprecated.md @@ -0,0 +1,36 @@ +# Url is deprecated + +#### Why This Error Occurred + +In versions prior to 6.x the `url` property got magically injected into every `Page` component (every page inside the `pages` directory). + +The reason this is going away is that we want to make things very predictable and explicit. Having a magical url property coming out of nowhere doesn't aid that goal. + +> ⚠️ In some cases using React Dev Tools may trigger this warning even if you do not reference `url` anywhere in your code. Try temporarily disabling the extension and see if the warning persists. + +#### Possible Ways to Fix It + +https://nextjs.org/docs/advanced-features/codemods#url-to-withrouter + +Since Next 5 we provide a way to explicitly inject the Next.js router object into pages and all their descending components. +The `router` property that is injected will hold the same values as `url`, like `pathname`, `asPath`, and `query`. + +Here's an example of using `withRouter`: + +```js +import { withRouter } from 'next/router' + +class Page extends React.Component { + render() { + const { router } = this.props + console.log(router) + return
    {router.pathname}
    + } +} + +export default withRouter(Page) +``` + +We provide a codemod (a code to code transformation) to automatically change the `url` property usages to `withRouter`. + +You can find this codemod and instructions on how to run it here: https://nextjs.org/docs/advanced-features/codemods#url-to-withrouter diff --git a/nextjs/errors/webpack5.md b/nextjs/errors/webpack5.md new file mode 100644 index 0000000000..b8c994c549 --- /dev/null +++ b/nextjs/errors/webpack5.md @@ -0,0 +1,45 @@ +# Webpack 5 Adoption + +#### Why This Message Occurred + +Next.js has adopted webpack 5 as the default for compilation. We've spent a lot of effort into ensuring the transition from webpack 4 to 5 will be as smooth as possible. For example Next.js now comes with both webpack 4 and 5 allowing you to adopt webpack 5 by adding a flag to your `next.config.js`: + +```js +module.exports = { + // Webpack 5 is enabled by default + // You can still use webpack 4 while upgrading to the latest version of Next.js by adding the "webpack5: false" flag + webpack5: false, +} +``` + +Using webpack 5 in your application has many benefits, notably: + +- Improved Disk Caching: `next build` is significantly faster on subsequent builds +- Improved Fast Refresh: Fast Refresh work is prioritized +- Improved Long Term Caching of Assets: Deterministic code output that is less likely to change between builds +- Improved Tree Shaking +- Support for assets using `new URL("file.png", import.meta.url)` +- Support for web workers using `new Worker(new URL("worker.js", import.meta.url))` +- Support for `exports`/`imports` field in `package.json` + +In the past releases we have gradually rolled out webpack 5 to Next.js applications: + +- In Next.js 10.2 we automatically opted-in applications without custom webpack configuration in `next.config.js` +- In Next.js 10.2 we automatically opted-in applications that do not have a `next.config.js` +- In Next.js 11 webpack 5 was enabled by default for all applications. You can still opt-out and use webpack 4 to help with backwards compatibility using `webpack5: false` in `next.config.js` + +In the next major version webpack 4 support will be removed. + +#### Custom webpack configuration + +In case you do have custom webpack configuration, either through custom plugins or your own modifications you'll have to take a few steps to ensure your applications works with webpack 5. + +- When using `next-transpile-modules` make sure you use the latest version which includes [this patch](https://github.com/martpie/next-transpile-modules/pull/179) +- When using `@zeit/next-css` / `@zeit/next-sass` make sure you use the [built-in CSS/Sass support](https://nextjs.org/docs/basic-features/built-in-css-support) instead +- When using `@zeit/next-preact` use [this example](https://github.com/vercel/next-plugins/tree/master/packages/next-preact) instead +- When using `@zeit/next-source-maps` use the [built-in production Source Map support](https://nextjs.org/docs/advanced-features/source-maps) +- When using webpack plugins make sure they're upgraded to the latest version, in most cases the latest version will include webpack 5 support. In some cases these upgraded webpack plugins will only support webpack 5. + +### Useful Links + +In case you're running into issues you can connect with the community in [this help discussion](https://github.com/vercel/next.js/discussions/23498). diff --git a/nextjs/jest.config.js b/nextjs/jest.config.js new file mode 100644 index 0000000000..cf05e831db --- /dev/null +++ b/nextjs/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + testMatch: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'], + verbose: true, + rootDir: 'test', + modulePaths: ['/lib'], + globalSetup: '/jest-global-setup.js', + globalTeardown: '/jest-global-teardown.js', + setupFilesAfterEnv: ['/jest-setup-after-env.js'], + testEnvironment: '/jest-environment.js', +} diff --git a/nextjs/lerna.json b/nextjs/lerna.json new file mode 100644 index 0000000000..3bc6167a7d --- /dev/null +++ b/nextjs/lerna.json @@ -0,0 +1,16 @@ +{ + "npmClient": "yarn", + "useWorkspaces": true, + "packages": ["packages/*"], + "command": { + "version": { + "exact": true + }, + "publish": { + "npmClient": "npm", + "allowBranch": ["master", "canary"], + "registry": "https://registry.npmjs.org/" + } + }, + "version": "11.1.0" +} diff --git a/nextjs/license.md b/nextjs/license.md new file mode 100644 index 0000000000..b95c9edb40 --- /dev/null +++ b/nextjs/license.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Brandon Bayer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nextjs/lint-staged.config.js b/nextjs/lint-staged.config.js new file mode 100644 index 0000000000..7641970267 --- /dev/null +++ b/nextjs/lint-staged.config.js @@ -0,0 +1,28 @@ +const escape = require('shell-quote').quote +const { ESLint } = require('eslint') + +const eslint = new ESLint() +const isWin = process.platform === 'win32' + +module.exports = { + '**/*.{js,jsx,ts,tsx}': (filenames) => { + const escapedFileNames = filenames + .map((filename) => `"${isWin ? filename : escape([filename])}"`) + .join(' ') + return [ + `prettier --with-node-modules --ignore-path .prettierignore_staged --write ${escapedFileNames}`, + `eslint --no-ignore --max-warnings=0 --fix ${filenames + .filter((file) => !eslint.isPathIgnored(file)) + .map((f) => `"${f}"`) + .join(' ')}`, + ] + }, + '**/*.{json,md,mdx,css,html,yml,yaml,scss}': (filenames) => { + const escapedFileNames = filenames + .map((filename) => `"${isWin ? filename : escape([filename])}"`) + .join(' ') + return [ + `prettier --with-node-modules --ignore-path .prettierignore_staged --write ${escapedFileNames}`, + ] + }, +} diff --git a/nextjs/package.json b/nextjs/package.json new file mode 100644 index 0000000000..89e47f68f9 --- /dev/null +++ b/nextjs/package.json @@ -0,0 +1,162 @@ +{ + "version": "0.0.0", + "name": "nextjs-project", + "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "clean": "yarn lerna clean -y && yarn lerna bootstrap && yarn lerna exec 'rm -rf ./dist'", + "build": "yarn prepublish", + "lerna": "lerna", + "dev": "lerna run dev --stream --parallel", + "dev2": "while true; do yarn --check-files && yarn dev; done", + "testonly": "jest --runInBand", + "testonly:packages": "ultra -r --filter \"packages/*\" --concurrency 15 test", + "testheadless": "cross-env HEADLESS=true yarn testonly", + "testsafari": "cross-env BROWSER_NAME=safari yarn testonly", + "testfirefox": "cross-env BROWSER_NAME=firefox yarn testonly", + "testie": "cross-env BROWSER_NAME=\"internet explorer\" yarn testonly", + "testall": "yarn run testonly -- --ci --forceExit && lerna run --scope @next/codemod test", + "genstats": "cross-env LOCAL_STATS=true node .github/actions/next-stats-action/src/index.js", + "pretest": "yarn run lint", + "git-reset": "git reset --hard HEAD", + "git-clean": "git clean -d -x -e node_modules -e packages -f", + "test-take2": "yarn git-reset && yarn git-clean && yarn testall", + "test": "yarn run testall || yarn run test-take2", + "lint-typescript": "lerna run typescript", + "lint-eslint": "eslint . --ext js,jsx,ts,tsx --max-warnings=0", + "lint-no-typescript": "run-p prettier-check lint-eslint", + "lint": "run-p lint-typescript lint-eslint", + "lint-fix": "yarn prettier-fix && eslint . --ext js,jsx,ts,tsx --fix --max-warnings=0", + "prettier-check": "prettier --check .", + "prettier-fix": "prettier --write .", + "types": "lerna run types --stream", + "typescript": "lerna run typescript", + "prepublish": "lerna run prepublish", + "publish-canary": "git checkout canary && git pull && lerna version prerelease --preid canary --force-publish && release --pre --skip-questions", + "publish-stable": "lerna version --force-publish", + "lint-staged": "lint-staged", + "next-with-deps": "./scripts/next-with-deps.sh", + "next": "node --trace-deprecation --enable-source-maps packages/next/dist/bin/next", + "debug": "node --inspect packages/next/dist/bin/next" + }, + "pre-commit": "lint-staged", + "devDependencies": { + "@babel/eslint-parser": "7.13.14", + "@babel/plugin-proposal-object-rest-spread": "7.12.1", + "@babel/preset-flow": "7.12.1", + "@babel/preset-react": "7.12.10", + "@fullhuman/postcss-purgecss": "1.3.0", + "@mdx-js/loader": "0.18.0", + "@svgr/webpack": "5.5.0", + "@swc/core": "1.2.74", + "@testing-library/react": "11.2.5", + "@types/b64-lite": "1.3.0", + "@types/cheerio": "0.22.16", + "@types/cookie-session": "2.0.42", + "@types/fs-extra": "8.1.0", + "@types/http-proxy": "1.17.3", + "@types/jest": "26.0.20", + "@types/passport": "1.0.5", + "@types/sharp": "0.28.4", + "@types/string-hash": "1.1.1", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "@vercel/fetch": "6.1.1", + "@zeit/next-css": "1.0.2-canary.2", + "@zeit/next-sass": "1.0.2-canary.2", + "@zeit/next-typescript": "1.1.2-canary.0", + "abort-controller": "3.0.0", + "alex": "9.1.0", + "amphtml-validator": "1.0.33", + "async-sema": "3.0.1", + "babel-core": "7.0.0-bridge.0", + "babel-jest": "27.0.0-next.5", + "babel-plugin-tester": "10.0.0", + "browserslist": "4.16.6", + "browserstack-local": "1.4.0", + "cheerio": "0.22.0", + "clone": "2.1.2", + "cookie": "0.4.1", + "cors": "2.8.5", + "coveralls": "3.0.3", + "critters": "0.0.6", + "cross-env": "7.0.3", + "cross-spawn": "7.0.3", + "css-loader": "1.0.0", + "escape-string-regexp": "2.0.0", + "eslint": "7.21.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jest": "23.13.1", + "eslint-plugin-react": "^7.23.1", + "eslint-plugin-react-hooks": "^4.2.0", + "execa": "2.0.3", + "express": "4.17.0", + "faker": "5.5.3", + "faunadb": "2.6.1", + "firebase": "7.14.5", + "fs-extra": "^9.1.0", + "get-port": "5.1.1", + "glob": "7.1.7", + "gzip-size": "5.1.1", + "image-size": "1.0.0", + "is-animated": "2.0.0", + "isomorphic-unfetch": "3.0.0", + "jest": "27.0.0-next.8", + "ky": "0.19.1", + "ky-universal": "0.6.0", + "lerna": "4.0.0", + "lint-staged": "10.5.4", + "lost": "8.3.1", + "minimatch": "3.0.4", + "moment": "^2.24.0", + "node-fetch": "2.6.7", + "node-notifier": "5.4.0", + "node-sass": "5.0.0", + "npm-run-all": "4.1.5", + "nprogress": "0.2.0", + "pixrem": "5.0.0", + "pnpm": "5.14.3", + "postcss-nested": "4.2.1", + "postcss-pseudoelements": "5.0.0", + "postcss-short-size": "4.0.0", + "postcss-trolling": "0.1.7", + "pre-commit": "1.2.2", + "prettier": "2.2.1", + "pretty-bytes": "5.3.0", + "pretty-ms": "7.0.0", + "react": "0.0.0-experimental-6a589ad71", + "react-18": "npm:react@next", + "react-dom": "0.0.0-experimental-6a589ad71", + "react-dom-18": "npm:react-dom@next", + "react-ssr-prepass": "1.0.8", + "release": "6.3.0", + "request-promise-core": "1.1.2", + "rimraf": "^3.0.2", + "seedrandom": "3.0.5", + "selenium-standalone": "6.18.0", + "selenium-webdriver": "4.0.0-alpha.7", + "shell-quote": "1.7.2", + "sqlite": "4.0.22", + "sqlite3": "5.0.2", + "styled-components": "5.1.0", + "styled-jsx-plugin-postcss": "3.0.2", + "tailwindcss": "1.1.3", + "taskr": "1.1.0", + "tree-kill": "1.2.2", + "typescript": "4.5.2", + "ultra-runner": "3.10.5", + "wait-port": "0.2.2", + "web-streams-polyfill": "2.1.1", + "webpack-bundle-analyzer": "4.3.0", + "worker-loader": "3.0.7" + }, + "resolutions": { + "browserslist": "4.16.6", + "caniuse-lite": "1.0.30001274" + }, + "engines": { + "node": ">=12.0.0" + } +} diff --git a/nextjs/packages/create-next-app/README.md b/nextjs/packages/create-next-app/README.md new file mode 100644 index 0000000000..8cf5e7f8c8 --- /dev/null +++ b/nextjs/packages/create-next-app/README.md @@ -0,0 +1,38 @@ +# Create Next App + +The easiest way to get started with Next.js is by using `create-next-app`. This CLI tool enables you to quickly start building a new Next.js application, with everything set up for you. You can create a new app using the default Next.js template, or by using one of the [official Next.js examples](https://github.com/vercel/next.js/tree/canary/examples). To get started, use the following command: + +```bash +npx create-next-app +``` + +Or, for a [TypeScript project](https://github.com/vercel/next.js/blob/canary/docs/basic-features/typescript.md): + +```bash +npx create-next-app --typescript +``` + +To create a new app in a specific folder, you can send a name as an argument. For example, the following command will create a new Next.js app called `blog-app` in a folder with the same name: + +```bash +npx create-next-app blog-app +``` + +## Options + +`create-next-app` comes with the following options: + +- **--ts, --typescript** - Initialize as a TypeScript project. +- **-e, --example [name]|[github-url]** - An example to bootstrap the app with. You can use an example name from the [Next.js repo](https://github.com/vercel/next.js/tree/master/examples) or a GitHub URL. The URL can use any branch and/or subdirectory. +- **--example-path <path-to-example>** - In a rare case, your GitHub URL might contain a branch name with a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). In this case, you must specify the path to the example separately: `--example-path foo/bar` +- **--use-npm** - Explicitly tell the CLI to bootstrap the app using npm. To bootstrap using yarn we recommend to run `yarn create next-app` + +## Why use Create Next App? + +`create-next-app` allows you to create a new Next.js app within seconds. It is officially maintained by the creators of Next.js, and includes a number of benefits: + +- **Interactive Experience**: Running `npx create-next-app` (with no arguments) launches an interactive experience that guides you through setting up a project. +- **Zero Dependencies**: Initializing a project is as quick as one second. Create Next App has zero dependencies. +- **Offline Support**: Create Next App will automatically detect if you're offline and bootstrap your project using your local package cache. +- **Support for Examples**: Create Next App can bootstrap your application using an example from the Next.js examples collection (e.g. `npx create-next-app --example api-routes`). +- **Tested**: The package is part of the Next.js monorepo and tested using the same integration test suite as Next.js itself, ensuring it works as expected with every release. diff --git a/nextjs/packages/create-next-app/create-app.ts b/nextjs/packages/create-next-app/create-app.ts new file mode 100644 index 0000000000..2badeabb1b --- /dev/null +++ b/nextjs/packages/create-next-app/create-app.ts @@ -0,0 +1,306 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import retry from 'async-retry' +import chalk from 'chalk' +import cpy from 'cpy' +import fs from 'fs' +import os from 'os' +import path from 'path' +import { + downloadAndExtractExample, + downloadAndExtractRepo, + getRepoInfo, + hasExample, + hasRepo, + RepoInfo, +} from './helpers/examples' +import { makeDir } from './helpers/make-dir' +import { tryGitInit } from './helpers/git' +import { install } from './helpers/install' +import { isFolderEmpty } from './helpers/is-folder-empty' +import { getOnline } from './helpers/is-online' +import { shouldUseYarn } from './helpers/should-use-yarn' +import { isWriteable } from './helpers/is-writeable' + +export class DownloadError extends Error {} + +export async function createApp({ + appPath, + useNpm, + example, + examplePath, + typescript, +}: { + appPath: string + useNpm: boolean + example?: string + examplePath?: string + typescript?: boolean +}): Promise { + let repoInfo: RepoInfo | undefined + const template = typescript ? 'typescript' : 'default' + + if (example) { + let repoUrl: URL | undefined + + try { + repoUrl = new URL(example) + } catch (error) { + if (error.code !== 'ERR_INVALID_URL') { + console.error(error) + process.exit(1) + } + } + + if (repoUrl) { + if (repoUrl.origin !== 'https://github.com') { + console.error( + `Invalid URL: ${chalk.red( + `"${example}"` + )}. Only GitHub repositories are supported. Please use a GitHub URL and try again.` + ) + process.exit(1) + } + + repoInfo = await getRepoInfo(repoUrl, examplePath) + + if (!repoInfo) { + console.error( + `Found invalid GitHub URL: ${chalk.red( + `"${example}"` + )}. Please fix the URL and try again.` + ) + process.exit(1) + } + + const found = await hasRepo(repoInfo) + + if (!found) { + console.error( + `Could not locate the repository for ${chalk.red( + `"${example}"` + )}. Please check that the repository exists and try again.` + ) + process.exit(1) + } + } else if (example !== '__internal-testing-retry') { + const found = await hasExample(example) + + if (!found) { + console.error( + `Could not locate an example named ${chalk.red( + `"${example}"` + )}. It could be due to the following:\n`, + `1. Your spelling of example ${chalk.red( + `"${example}"` + )} might be incorrect.\n`, + `2. You might not be connected to the internet.` + ) + process.exit(1) + } + } + } + + const root = path.resolve(appPath) + + if (!(await isWriteable(path.dirname(root)))) { + console.error( + 'The application path is not writable, please check folder permissions and try again.' + ) + console.error( + 'It is likely you do not have write permissions for this folder.' + ) + process.exit(1) + } + + const appName = path.basename(root) + + await makeDir(root) + if (!isFolderEmpty(root, appName)) { + process.exit(1) + } + + const useYarn = useNpm ? false : shouldUseYarn() + const isOnline = !useYarn || (await getOnline()) + const originalDirectory = process.cwd() + + const displayedCommand = useYarn ? 'yarn' : 'npm' + console.log(`Creating a new Next.js app in ${chalk.green(root)}.`) + console.log() + + await makeDir(root) + process.chdir(root) + + if (example) { + /** + * If an example repository is provided, clone it. + */ + try { + if (repoInfo) { + const repoInfo2 = repoInfo + console.log( + `Downloading files from repo ${chalk.cyan( + example + )}. This might take a moment.` + ) + console.log() + await retry(() => downloadAndExtractRepo(root, repoInfo2), { + retries: 3, + }) + } else { + console.log( + `Downloading files for example ${chalk.cyan( + example + )}. This might take a moment.` + ) + console.log() + await retry(() => downloadAndExtractExample(root, example), { + retries: 3, + }) + } + } catch (reason) { + throw new DownloadError(reason) + } + // Copy our default `.gitignore` if the application did not provide one + const ignorePath = path.join(root, '.gitignore') + if (!fs.existsSync(ignorePath)) { + fs.copyFileSync( + path.join(__dirname, 'templates', template, 'gitignore'), + ignorePath + ) + } + + console.log('Installing packages. This might take a couple of minutes.') + console.log() + + await install(root, null, { useYarn, isOnline }) + console.log() + } else { + /** + * Otherwise, if an example repository is not provided for cloning, proceed + * by installing from a template. + */ + console.log(chalk.bold(`Using ${displayedCommand}.`)) + /** + * Create a package.json for the new project. + */ + const packageJson = { + name: appName, + version: '0.1.0', + private: true, + scripts: { + dev: 'next dev', + build: 'next build', + start: 'next start', + lint: 'next lint', + }, + } + /** + * Write it to disk. + */ + fs.writeFileSync( + path.join(root, 'package.json'), + JSON.stringify(packageJson, null, 2) + os.EOL + ) + /** + * These flags will be passed to `install()`. + */ + const installFlags = { useYarn, isOnline } + /** + * Default dependencies. + */ + const dependencies = ['react', 'react-dom', 'next'] + /** + * Default devDependencies. + */ + const devDependencies = ['eslint', 'eslint-config-next'] + /** + * TypeScript projects will have type definitions and other devDependencies. + */ + if (typescript) { + devDependencies.push('typescript', '@types/react') + } + /** + * Install package.json dependencies if they exist. + */ + if (dependencies.length) { + console.log() + console.log('Installing dependencies:') + for (const dependency of dependencies) { + console.log(`- ${chalk.cyan(dependency)}`) + } + console.log() + + await install(root, dependencies, installFlags) + } + /** + * Install package.json devDependencies if they exist. + */ + if (devDependencies.length) { + console.log() + console.log('Installing devDependencies:') + for (const devDependency of devDependencies) { + console.log(`- ${chalk.cyan(devDependency)}`) + } + console.log() + + const devInstallFlags = { devDependencies: true, ...installFlags } + await install(root, devDependencies, devInstallFlags) + } + console.log() + /** + * Copy the template files to the target directory. + */ + await cpy('**', root, { + parents: true, + cwd: path.join(__dirname, 'templates', template), + rename: (name) => { + switch (name) { + case 'gitignore': + case 'eslintrc.json': { + return '.'.concat(name) + } + // README.md is ignored by webpack-asset-relocator-loader used by ncc: + // https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227 + case 'README-template.md': { + return 'README.md' + } + default: { + return name + } + } + }, + }) + } + + if (tryGitInit(root)) { + console.log('Initialized a git repository.') + console.log() + } + + let cdpath: string + if (path.join(originalDirectory, appName) === appPath) { + cdpath = appName + } else { + cdpath = appPath + } + + console.log(`${chalk.green('Success!')} Created ${appName} at ${appPath}`) + console.log('Inside that directory, you can run several commands:') + console.log() + console.log(chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}dev`)) + console.log(' Starts the development server.') + console.log() + console.log(chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}build`)) + console.log(' Builds the app for production.') + console.log() + console.log(chalk.cyan(` ${displayedCommand} start`)) + console.log(' Runs the built app in production mode.') + console.log() + console.log('We suggest that you begin by typing:') + console.log() + console.log(chalk.cyan(' cd'), cdpath) + console.log( + ` ${chalk.cyan(`${displayedCommand} ${useYarn ? '' : 'run '}dev`)}` + ) + console.log() +} diff --git a/nextjs/packages/create-next-app/helpers/examples.ts b/nextjs/packages/create-next-app/helpers/examples.ts new file mode 100644 index 0000000000..92292a088f --- /dev/null +++ b/nextjs/packages/create-next-app/helpers/examples.ts @@ -0,0 +1,98 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import got from 'got' +import tar from 'tar' +import { Stream } from 'stream' +import { promisify } from 'util' + +const pipeline = promisify(Stream.pipeline) + +export type RepoInfo = { + username: string + name: string + branch: string + filePath: string +} + +export async function isUrlOk(url: string): Promise { + const res = await got.head(url).catch((e) => e) + return res.statusCode === 200 +} + +export async function getRepoInfo( + url: URL, + examplePath?: string +): Promise { + const [, username, name, t, _branch, ...file] = url.pathname.split('/') + const filePath = examplePath ? examplePath.replace(/^\//, '') : file.join('/') + + // Support repos whose entire purpose is to be a NextJS example, e.g. + // https://github.com/:username/:my-cool-nextjs-example-repo-name. + if (t === undefined) { + const infoResponse = await got( + `https://api.github.com/repos/${username}/${name}` + ).catch((e) => e) + if (infoResponse.statusCode !== 200) { + return + } + const info = JSON.parse(infoResponse.body) + return { username, name, branch: info['default_branch'], filePath } + } + + // If examplePath is available, the branch name takes the entire path + const branch = examplePath + ? `${_branch}/${file.join('/')}`.replace(new RegExp(`/${filePath}|/$`), '') + : _branch + + if (username && name && branch && t === 'tree') { + return { username, name, branch, filePath } + } +} + +export function hasRepo({ + username, + name, + branch, + filePath, +}: RepoInfo): Promise { + const contentsUrl = `https://api.github.com/repos/${username}/${name}/contents` + const packagePath = `${filePath ? `/${filePath}` : ''}/package.json` + + return isUrlOk(contentsUrl + packagePath + `?ref=${branch}`) +} + +export function hasExample(name: string): Promise { + return isUrlOk( + `https://api.github.com/repos/vercel/next.js/contents/examples/${encodeURIComponent( + name + )}/package.json` + ) +} + +export function downloadAndExtractRepo( + root: string, + { username, name, branch, filePath }: RepoInfo +): Promise { + return pipeline( + got.stream( + `https://codeload.github.com/${username}/${name}/tar.gz/${branch}` + ), + tar.extract( + { cwd: root, strip: filePath ? filePath.split('/').length + 1 : 1 }, + [`${name}-${branch}${filePath ? `/${filePath}` : ''}`] + ) + ) +} + +export function downloadAndExtractExample( + root: string, + name: string +): Promise { + if (name === '__internal-testing-retry') { + throw new Error('This is an internal example for testing the CLI.') + } + + return pipeline( + got.stream('https://codeload.github.com/vercel/next.js/tar.gz/canary'), + tar.extract({ cwd: root, strip: 3 }, [`next.js-canary/examples/${name}`]) + ) +} diff --git a/nextjs/packages/create-next-app/helpers/git.ts b/nextjs/packages/create-next-app/helpers/git.ts new file mode 100644 index 0000000000..80948348ac --- /dev/null +++ b/nextjs/packages/create-next-app/helpers/git.ts @@ -0,0 +1,48 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { execSync } from 'child_process' +import path from 'path' +import rimraf from 'rimraf' + +function isInGitRepository(): boolean { + try { + execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }) + return true + } catch (_) {} + return false +} + +function isInMercurialRepository(): boolean { + try { + execSync('hg --cwd . root', { stdio: 'ignore' }) + return true + } catch (_) {} + return false +} + +export function tryGitInit(root: string): boolean { + let didInit = false + try { + execSync('git --version', { stdio: 'ignore' }) + if (isInGitRepository() || isInMercurialRepository()) { + return false + } + + execSync('git init', { stdio: 'ignore' }) + didInit = true + + execSync('git checkout -b main', { stdio: 'ignore' }) + + execSync('git add -A', { stdio: 'ignore' }) + execSync('git commit -m "Initial commit from Create Next App"', { + stdio: 'ignore', + }) + return true + } catch (e) { + if (didInit) { + try { + rimraf.sync(path.join(root, '.git')) + } catch (_) {} + } + return false + } +} diff --git a/nextjs/packages/create-next-app/helpers/install.ts b/nextjs/packages/create-next-app/helpers/install.ts new file mode 100644 index 0000000000..f8855275a9 --- /dev/null +++ b/nextjs/packages/create-next-app/helpers/install.ts @@ -0,0 +1,109 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import chalk from 'chalk' +import spawn from 'cross-spawn' + +interface InstallArgs { + /** + * Indicate whether to install packages using Yarn. + */ + useYarn: boolean + /** + * Indicate whether there is an active Internet connection. + */ + isOnline: boolean + /** + * Indicate whether the given dependencies are devDependencies. + */ + devDependencies?: boolean +} + +/** + * Spawn a package manager installation with either Yarn or NPM. + * + * @returns A Promise that resolves once the installation is finished. + */ +export function install( + root: string, + dependencies: string[] | null, + { useYarn, isOnline, devDependencies }: InstallArgs +): Promise { + /** + * NPM-specific command-line flags. + */ + const npmFlags: string[] = [] + /** + * Yarn-specific command-line flags. + */ + const yarnFlags: string[] = [] + /** + * Return a Promise that resolves once the installation is finished. + */ + return new Promise((resolve, reject) => { + let args: string[] + let command: string = useYarn ? 'yarnpkg' : 'npm' + + if (dependencies && dependencies.length) { + /** + * If there are dependencies, run a variation of `{displayCommand} add`. + */ + if (useYarn) { + /** + * Call `yarn add --exact (--offline)? (-D)? ...`. + */ + args = ['add', '--exact'] + if (!isOnline) args.push('--offline') + args.push('--cwd', root) + if (devDependencies) args.push('--dev') + args.push(...dependencies) + } else { + /** + * Call `npm install [--save|--save-dev] ...`. + */ + args = ['install', '--save-exact'] + args.push(devDependencies ? '--save-dev' : '--save') + args.push(...dependencies) + } + } else { + /** + * If there are no dependencies, run a variation of `{displayCommand} + * install`. + */ + args = ['install'] + if (useYarn) { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log(chalk.yellow('Falling back to the local Yarn cache.')) + console.log() + args.push('--offline') + } + } else { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log() + } + } + } + /** + * Add any package manager-specific flags. + */ + if (useYarn) { + args.push(...yarnFlags) + } else { + args.push(...npmFlags) + } + /** + * Spawn the installation process. + */ + const child = spawn(command, args, { + stdio: 'inherit', + env: { ...process.env, ADBLOCK: '1', DISABLE_OPENCOLLECTIVE: '1' }, + }) + child.on('close', (code) => { + if (code !== 0) { + reject({ command: `${command} ${args.join(' ')}` }) + return + } + resolve() + }) + }) +} diff --git a/nextjs/packages/create-next-app/helpers/is-folder-empty.ts b/nextjs/packages/create-next-app/helpers/is-folder-empty.ts new file mode 100644 index 0000000000..838ddeb027 --- /dev/null +++ b/nextjs/packages/create-next-app/helpers/is-folder-empty.ts @@ -0,0 +1,60 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import chalk from 'chalk' +import fs from 'fs' +import path from 'path' + +export function isFolderEmpty(root: string, name: string): boolean { + const validFiles = [ + '.DS_Store', + '.git', + '.gitattributes', + '.gitignore', + '.gitlab-ci.yml', + '.hg', + '.hgcheck', + '.hgignore', + '.idea', + '.npmignore', + '.travis.yml', + 'LICENSE', + 'Thumbs.db', + 'docs', + 'mkdocs.yml', + 'npm-debug.log', + 'yarn-debug.log', + 'yarn-error.log', + ] + + const conflicts = fs + .readdirSync(root) + .filter((file) => !validFiles.includes(file)) + // Support IntelliJ IDEA-based editors + .filter((file) => !/\.iml$/.test(file)) + + if (conflicts.length > 0) { + console.log( + `The directory ${chalk.green(name)} contains files that could conflict:` + ) + console.log() + for (const file of conflicts) { + try { + const stats = fs.lstatSync(path.join(root, file)) + if (stats.isDirectory()) { + console.log(` ${chalk.blue(file)}/`) + } else { + console.log(` ${file}`) + } + } catch { + console.log(` ${file}`) + } + } + console.log() + console.log( + 'Either try using a new directory name, or remove the files listed above.' + ) + console.log() + return false + } + + return true +} diff --git a/nextjs/packages/create-next-app/helpers/is-online.ts b/nextjs/packages/create-next-app/helpers/is-online.ts new file mode 100644 index 0000000000..c2f3f83a93 --- /dev/null +++ b/nextjs/packages/create-next-app/helpers/is-online.ts @@ -0,0 +1,40 @@ +import { execSync } from 'child_process' +import dns from 'dns' +import url from 'url' + +function getProxy(): string | undefined { + if (process.env.https_proxy) { + return process.env.https_proxy + } + + try { + const httpsProxy = execSync('npm config get https-proxy').toString().trim() + return httpsProxy !== 'null' ? httpsProxy : undefined + } catch (e) { + return + } +} + +export function getOnline(): Promise { + return new Promise((resolve) => { + dns.lookup('registry.yarnpkg.com', (registryErr) => { + if (!registryErr) { + return resolve(true) + } + + const proxy = getProxy() + if (!proxy) { + return resolve(false) + } + + const { hostname } = url.parse(proxy) + if (!hostname) { + return resolve(false) + } + + dns.lookup(hostname, (proxyErr) => { + resolve(proxyErr == null) + }) + }) + }) +} diff --git a/nextjs/packages/create-next-app/helpers/is-writeable.ts b/nextjs/packages/create-next-app/helpers/is-writeable.ts new file mode 100644 index 0000000000..0b9e9abb4f --- /dev/null +++ b/nextjs/packages/create-next-app/helpers/is-writeable.ts @@ -0,0 +1,10 @@ +import fs from 'fs' + +export async function isWriteable(directory: string): Promise { + try { + await fs.promises.access(directory, (fs.constants || fs).W_OK) + return true + } catch (err) { + return false + } +} diff --git a/nextjs/packages/create-next-app/helpers/make-dir.ts b/nextjs/packages/create-next-app/helpers/make-dir.ts new file mode 100644 index 0000000000..00bfed2eb4 --- /dev/null +++ b/nextjs/packages/create-next-app/helpers/make-dir.ts @@ -0,0 +1,8 @@ +import fs from 'fs' + +export function makeDir( + root: string, + options = { recursive: true } +): Promise { + return fs.promises.mkdir(root, options) +} diff --git a/nextjs/packages/create-next-app/helpers/should-use-yarn.ts b/nextjs/packages/create-next-app/helpers/should-use-yarn.ts new file mode 100644 index 0000000000..c305361517 --- /dev/null +++ b/nextjs/packages/create-next-app/helpers/should-use-yarn.ts @@ -0,0 +1,14 @@ +import { execSync } from 'child_process' + +export function shouldUseYarn(): boolean { + try { + const userAgent = process.env.npm_config_user_agent + if (userAgent) { + return Boolean(userAgent && userAgent.startsWith('yarn')) + } + execSync('yarnpkg --version', { stdio: 'ignore' }) + return true + } catch (e) { + return false + } +} diff --git a/nextjs/packages/create-next-app/helpers/validate-pkg.ts b/nextjs/packages/create-next-app/helpers/validate-pkg.ts new file mode 100644 index 0000000000..ad30426c9c --- /dev/null +++ b/nextjs/packages/create-next-app/helpers/validate-pkg.ts @@ -0,0 +1,19 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import validateProjectName from 'validate-npm-package-name' + +export function validateNpmName( + name: string +): { valid: boolean; problems?: string[] } { + const nameValidation = validateProjectName(name) + if (nameValidation.validForNewPackages) { + return { valid: true } + } + + return { + valid: false, + problems: [ + ...(nameValidation.errors || []), + ...(nameValidation.warnings || []), + ], + } +} diff --git a/nextjs/packages/create-next-app/index.ts b/nextjs/packages/create-next-app/index.ts new file mode 100644 index 0000000000..a947521711 --- /dev/null +++ b/nextjs/packages/create-next-app/index.ts @@ -0,0 +1,200 @@ +#!/usr/bin/env node +/* eslint-disable import/no-extraneous-dependencies */ +import chalk from 'chalk' +import Commander from 'commander' +import path from 'path' +import prompts from 'prompts' +import checkForUpdate from 'update-check' +import { createApp, DownloadError } from './create-app' +import { shouldUseYarn } from './helpers/should-use-yarn' +import { validateNpmName } from './helpers/validate-pkg' +import packageJson from './package.json' + +let projectPath: string = '' + +const program = new Commander.Command(packageJson.name) + .version(packageJson.version) + .arguments('') + .usage(`${chalk.green('')} [options]`) + .action((name) => { + projectPath = name + }) + .option( + '--ts, --typescript', + ` + + Initialize as a TypeScript project. +` + ) + .option( + '--use-npm', + ` + + Explicitly tell the CLI to bootstrap the app using npm +` + ) + .option( + '-e, --example [name]|[github-url]', + ` + + An example to bootstrap the app with. You can use an example name + from the official Next.js repo or a GitHub URL. The URL can use + any branch and/or subdirectory +` + ) + .option( + '--example-path ', + ` + + In a rare case, your GitHub URL might contain a branch name with + a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). + In this case, you must specify the path to the example separately: + --example-path foo/bar +` + ) + .allowUnknownOption() + .parse(process.argv) + +async function run(): Promise { + if (typeof projectPath === 'string') { + projectPath = projectPath.trim() + } + + if (!projectPath) { + const res = await prompts({ + type: 'text', + name: 'path', + message: 'What is your project named?', + initial: 'my-app', + validate: (name) => { + const validation = validateNpmName(path.basename(path.resolve(name))) + if (validation.valid) { + return true + } + return 'Invalid project name: ' + validation.problems![0] + }, + }) + + if (typeof res.path === 'string') { + projectPath = res.path.trim() + } + } + + if (!projectPath) { + console.log() + console.log('Please specify the project directory:') + console.log( + ` ${chalk.cyan(program.name())} ${chalk.green('')}` + ) + console.log() + console.log('For example:') + console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-next-app')}`) + console.log() + console.log( + `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.` + ) + process.exit(1) + } + + const resolvedProjectPath = path.resolve(projectPath) + const projectName = path.basename(resolvedProjectPath) + + const { valid, problems } = validateNpmName(projectName) + if (!valid) { + console.error( + `Could not create a project called ${chalk.red( + `"${projectName}"` + )} because of npm naming restrictions:` + ) + + problems!.forEach((p) => console.error(` ${chalk.red.bold('*')} ${p}`)) + process.exit(1) + } + + if (program.example === true) { + console.error( + 'Please provide an example name or url, otherwise remove the example option.' + ) + process.exit(1) + return + } + + const example = typeof program.example === 'string' && program.example.trim() + try { + await createApp({ + appPath: resolvedProjectPath, + useNpm: !!program.useNpm, + example: example && example !== 'default' ? example : undefined, + examplePath: program.examplePath, + typescript: program.typescript, + }) + } catch (reason) { + if (!(reason instanceof DownloadError)) { + throw reason + } + + const res = await prompts({ + type: 'confirm', + name: 'builtin', + message: + `Could not download "${example}" because of a connectivity issue between your machine and GitHub.\n` + + `Do you want to use the default template instead?`, + initial: true, + }) + if (!res.builtin) { + throw reason + } + + await createApp({ + appPath: resolvedProjectPath, + useNpm: !!program.useNpm, + typescript: program.typescript, + }) + } +} + +const update = checkForUpdate(packageJson).catch(() => null) + +async function notifyUpdate(): Promise { + try { + const res = await update + if (res?.latest) { + const isYarn = shouldUseYarn() + + console.log() + console.log( + chalk.yellow.bold('A new version of `create-next-app` is available!') + ) + console.log( + 'You can update by running: ' + + chalk.cyan( + isYarn + ? 'yarn global add create-next-app' + : 'npm i -g create-next-app' + ) + ) + console.log() + } + process.exit() + } catch { + // ignore error + } +} + +run() + .then(notifyUpdate) + .catch(async (reason) => { + console.log() + console.log('Aborting installation.') + if (reason.command) { + console.log(` ${chalk.cyan(reason.command)} has failed.`) + } else { + console.log(chalk.red('Unexpected error. Please report it as a bug:')) + console.log(reason) + } + console.log() + + await notifyUpdate() + + process.exit(1) + }) diff --git a/nextjs/packages/create-next-app/package.json b/nextjs/packages/create-next-app/package.json new file mode 100644 index 0000000000..d307f81142 --- /dev/null +++ b/nextjs/packages/create-next-app/package.json @@ -0,0 +1,53 @@ +{ + "name": "create-next-app", + "version": "11.1.0", + "keywords": [ + "react", + "next", + "next.js" + ], + "description": "Create Next.js-powered React apps with one command", + "repository": { + "type": "git", + "url": "https://github.com/vercel/next.js", + "directory": "packages/create-next-app" + }, + "author": "Next.js Team ", + "license": "MIT", + "bin": { + "create-next-app": "./dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "ncc build ./index.ts -w -o dist/", + "prerelease": "rimraf ./dist/", + "release": "ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register", + "prepublish": "yarn release" + }, + "devDependencies": { + "@types/async-retry": "1.4.2", + "@types/node": "^12.6.8", + "@types/prompts": "2.0.1", + "@types/rimraf": "3.0.0", + "@types/tar": "4.0.3", + "@types/validate-npm-package-name": "3.0.0", + "@vercel/ncc": "0.25.1", + "async-retry": "1.3.1", + "chalk": "2.4.2", + "commander": "2.20.0", + "cpy": "7.3.0", + "cross-spawn": "6.0.5", + "got": "10.7.0", + "prompts": "2.1.0", + "rimraf": "3.0.0", + "tar": "4.4.18", + "typescript": "4.5.2", + "update-check": "1.5.4", + "validate-npm-package-name": "3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } +} diff --git a/nextjs/packages/create-next-app/templates/default/README-template.md b/nextjs/packages/create-next-app/templates/default/README-template.md new file mode 100644 index 0000000000..b12f3e33e7 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/README-template.md @@ -0,0 +1,34 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/nextjs/packages/create-next-app/templates/default/eslintrc b/nextjs/packages/create-next-app/templates/default/eslintrc new file mode 100644 index 0000000000..97a2bb84ef --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["next", "next/core-web-vitals"] +} diff --git a/nextjs/packages/create-next-app/templates/default/eslintrc.json b/nextjs/packages/create-next-app/templates/default/eslintrc.json new file mode 100644 index 0000000000..bffb357a71 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/nextjs/packages/create-next-app/templates/default/gitignore b/nextjs/packages/create-next-app/templates/default/gitignore new file mode 100644 index 0000000000..1437c53f70 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/nextjs/packages/create-next-app/templates/default/next.config.js b/nextjs/packages/create-next-app/templates/default/next.config.js new file mode 100644 index 0000000000..0d6071006a --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + reactStrictMode: true, +} diff --git a/nextjs/packages/create-next-app/templates/default/pages/_app.js b/nextjs/packages/create-next-app/templates/default/pages/_app.js new file mode 100644 index 0000000000..1e1cec9242 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/pages/_app.js @@ -0,0 +1,7 @@ +import '../styles/globals.css' + +function MyApp({ Component, pageProps }) { + return +} + +export default MyApp diff --git a/nextjs/packages/create-next-app/templates/default/pages/api/hello.js b/nextjs/packages/create-next-app/templates/default/pages/api/hello.js new file mode 100644 index 0000000000..df63de88fa --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/pages/api/hello.js @@ -0,0 +1,5 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction + +export default function handler(req, res) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/nextjs/packages/create-next-app/templates/default/pages/index.js b/nextjs/packages/create-next-app/templates/default/pages/index.js new file mode 100644 index 0000000000..08145bba9a --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/pages/index.js @@ -0,0 +1,69 @@ +import Head from 'next/head' +import Image from 'next/image' +import styles from '../styles/Home.module.css' + +export default function Home() { + return ( + + ) +} diff --git a/nextjs/packages/create-next-app/templates/default/public/favicon.ico b/nextjs/packages/create-next-app/templates/default/public/favicon.ico new file mode 100644 index 0000000000..718d6fea48 Binary files /dev/null and b/nextjs/packages/create-next-app/templates/default/public/favicon.ico differ diff --git a/nextjs/packages/create-next-app/templates/default/public/vercel.svg b/nextjs/packages/create-next-app/templates/default/public/vercel.svg new file mode 100644 index 0000000000..fbf0e25a65 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/nextjs/packages/create-next-app/templates/default/styles/Home.module.css b/nextjs/packages/create-next-app/templates/default/styles/Home.module.css new file mode 100644 index 0000000000..35454bb748 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/styles/Home.module.css @@ -0,0 +1,121 @@ +.container { + min-height: 100vh; + padding: 0 0.5rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; +} + +.main { + padding: 5rem 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.footer { + width: 100%; + height: 100px; + border-top: 1px solid #eaeaea; + display: flex; + justify-content: center; + align-items: center; +} + +.footer a { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; +} + +.title a { + color: #0070f3; + text-decoration: none; +} + +.title a:hover, +.title a:focus, +.title a:active { + text-decoration: underline; +} + +.title { + margin: 0; + line-height: 1.15; + font-size: 4rem; +} + +.title, +.description { + text-align: center; +} + +.description { + line-height: 1.5; + font-size: 1.5rem; +} + +.code { + background: #fafafa; + border-radius: 5px; + padding: 0.75rem; + font-size: 1.1rem; + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, + Bitstream Vera Sans Mono, Courier New, monospace; +} + +.grid { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + max-width: 800px; + margin-top: 3rem; +} + +.card { + margin: 1rem; + padding: 1.5rem; + text-align: left; + color: inherit; + text-decoration: none; + border: 1px solid #eaeaea; + border-radius: 10px; + transition: color 0.15s ease, border-color 0.15s ease; + width: 45%; +} + +.card:hover, +.card:focus, +.card:active { + color: #0070f3; + border-color: #0070f3; +} + +.card h2 { + margin: 0 0 1rem 0; + font-size: 1.5rem; +} + +.card p { + margin: 0; + font-size: 1.25rem; + line-height: 1.5; +} + +.logo { + height: 1em; + margin-left: 0.5rem; +} + +@media (max-width: 600px) { + .grid { + width: 100%; + flex-direction: column; + } +} diff --git a/nextjs/packages/create-next-app/templates/default/styles/globals.css b/nextjs/packages/create-next-app/templates/default/styles/globals.css new file mode 100644 index 0000000000..e5e2dcc23b --- /dev/null +++ b/nextjs/packages/create-next-app/templates/default/styles/globals.css @@ -0,0 +1,16 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} diff --git a/nextjs/packages/create-next-app/templates/typescript/README-template.md b/nextjs/packages/create-next-app/templates/typescript/README-template.md new file mode 100644 index 0000000000..c87e0421d2 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/README-template.md @@ -0,0 +1,34 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/nextjs/packages/create-next-app/templates/typescript/blitz-env.d.ts b/nextjs/packages/create-next-app/templates/typescript/blitz-env.d.ts new file mode 100644 index 0000000000..c6643fda12 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/blitz-env.d.ts @@ -0,0 +1,3 @@ +/// +/// +/// diff --git a/nextjs/packages/create-next-app/templates/typescript/eslintrc b/nextjs/packages/create-next-app/templates/typescript/eslintrc new file mode 100644 index 0000000000..97a2bb84ef --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["next", "next/core-web-vitals"] +} diff --git a/nextjs/packages/create-next-app/templates/typescript/eslintrc.json b/nextjs/packages/create-next-app/templates/typescript/eslintrc.json new file mode 100644 index 0000000000..bffb357a71 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/nextjs/packages/create-next-app/templates/typescript/gitignore b/nextjs/packages/create-next-app/templates/typescript/gitignore new file mode 100644 index 0000000000..1437c53f70 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/nextjs/packages/create-next-app/templates/typescript/next.config.js b/nextjs/packages/create-next-app/templates/typescript/next.config.js new file mode 100644 index 0000000000..8b61df4e50 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + reactStrictMode: true, +} diff --git a/nextjs/packages/create-next-app/templates/typescript/pages/_app.tsx b/nextjs/packages/create-next-app/templates/typescript/pages/_app.tsx new file mode 100644 index 0000000000..945e892613 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/pages/_app.tsx @@ -0,0 +1,7 @@ +import '../styles/globals.css' +import type { AppProps } from 'next/app' + +function MyApp({ Component, pageProps }: AppProps) { + return +} +export default MyApp diff --git a/nextjs/packages/create-next-app/templates/typescript/pages/api/hello.ts b/nextjs/packages/create-next-app/templates/typescript/pages/api/hello.ts new file mode 100644 index 0000000000..f8bcc7e5ca --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/pages/api/hello.ts @@ -0,0 +1,13 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + name: string +} + +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/nextjs/packages/create-next-app/templates/typescript/pages/index.tsx b/nextjs/packages/create-next-app/templates/typescript/pages/index.tsx new file mode 100644 index 0000000000..1ef0af1235 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/pages/index.tsx @@ -0,0 +1,72 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import Image from 'next/image' +import styles from '../styles/Home.module.css' + +const Home: NextPage = () => { + return ( + + ) +} + +export default Home diff --git a/nextjs/packages/create-next-app/templates/typescript/public/favicon.ico b/nextjs/packages/create-next-app/templates/typescript/public/favicon.ico new file mode 100644 index 0000000000..718d6fea48 Binary files /dev/null and b/nextjs/packages/create-next-app/templates/typescript/public/favicon.ico differ diff --git a/nextjs/packages/create-next-app/templates/typescript/public/vercel.svg b/nextjs/packages/create-next-app/templates/typescript/public/vercel.svg new file mode 100644 index 0000000000..fbf0e25a65 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/nextjs/packages/create-next-app/templates/typescript/styles/Home.module.css b/nextjs/packages/create-next-app/templates/typescript/styles/Home.module.css new file mode 100644 index 0000000000..35454bb748 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/styles/Home.module.css @@ -0,0 +1,121 @@ +.container { + min-height: 100vh; + padding: 0 0.5rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; +} + +.main { + padding: 5rem 0; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.footer { + width: 100%; + height: 100px; + border-top: 1px solid #eaeaea; + display: flex; + justify-content: center; + align-items: center; +} + +.footer a { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; +} + +.title a { + color: #0070f3; + text-decoration: none; +} + +.title a:hover, +.title a:focus, +.title a:active { + text-decoration: underline; +} + +.title { + margin: 0; + line-height: 1.15; + font-size: 4rem; +} + +.title, +.description { + text-align: center; +} + +.description { + line-height: 1.5; + font-size: 1.5rem; +} + +.code { + background: #fafafa; + border-radius: 5px; + padding: 0.75rem; + font-size: 1.1rem; + font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, + Bitstream Vera Sans Mono, Courier New, monospace; +} + +.grid { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + max-width: 800px; + margin-top: 3rem; +} + +.card { + margin: 1rem; + padding: 1.5rem; + text-align: left; + color: inherit; + text-decoration: none; + border: 1px solid #eaeaea; + border-radius: 10px; + transition: color 0.15s ease, border-color 0.15s ease; + width: 45%; +} + +.card:hover, +.card:focus, +.card:active { + color: #0070f3; + border-color: #0070f3; +} + +.card h2 { + margin: 0 0 1rem 0; + font-size: 1.5rem; +} + +.card p { + margin: 0; + font-size: 1.25rem; + line-height: 1.5; +} + +.logo { + height: 1em; + margin-left: 0.5rem; +} + +@media (max-width: 600px) { + .grid { + width: 100%; + flex-direction: column; + } +} diff --git a/nextjs/packages/create-next-app/templates/typescript/styles/globals.css b/nextjs/packages/create-next-app/templates/typescript/styles/globals.css new file mode 100644 index 0000000000..e5e2dcc23b --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/styles/globals.css @@ -0,0 +1,16 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} diff --git a/nextjs/packages/create-next-app/templates/typescript/tsconfig.json b/nextjs/packages/create-next-app/templates/typescript/tsconfig.json new file mode 100644 index 0000000000..ba49a00d04 --- /dev/null +++ b/nextjs/packages/create-next-app/templates/typescript/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/nextjs/packages/create-next-app/tsconfig.json b/nextjs/packages/create-next-app/tsconfig.json new file mode 100644 index 0000000000..e4edad9e12 --- /dev/null +++ b/nextjs/packages/create-next-app/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2019", + "moduleResolution": "node", + "strict": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": false + }, + "exclude": ["templates", "dist"] +} diff --git a/nextjs/packages/eslint-config-next/core-web-vitals.js b/nextjs/packages/eslint-config-next/core-web-vitals.js new file mode 100644 index 0000000000..312797f39e --- /dev/null +++ b/nextjs/packages/eslint-config-next/core-web-vitals.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('.'), 'plugin:@next/next/core-web-vitals'], +} diff --git a/nextjs/packages/eslint-config-next/index.js b/nextjs/packages/eslint-config-next/index.js new file mode 100644 index 0000000000..ce46a8fe28 --- /dev/null +++ b/nextjs/packages/eslint-config-next/index.js @@ -0,0 +1,82 @@ +/* + * @rushstack/eslint-patch is used to include plugins as dev + * dependencies instead of imposing them as peer dependencies + * + * https://www.npmjs.com/package/@rushstack/eslint-patch + */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + extends: [ + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@next/next/recommended', + ], + plugins: ['import', 'react', 'jsx-a11y'], + rules: { + 'import/no-anonymous-default-export': 'error', + 'import/no-webpack-loader-syntax': 'off', + 'react/display-name': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'jsx-a11y/alt-text': [ + 'warn', + { + elements: ['img'], + img: ['Image'], + }, + ], + 'jsx-a11y/aria-props': 'warn', + 'jsx-a11y/aria-proptypes': 'warn', + 'jsx-a11y/aria-unsupported-elements': 'warn', + 'jsx-a11y/role-has-required-aria-props': 'warn', + 'jsx-a11y/role-supports-aria-props': 'warn', + '@next/next/no-html-link-for-pages': 'off', // Until we add multi pages support to Blitz + }, + parser: './parser.js', + parserOptions: { + requireConfigFile: false, + sourceType: 'module', + allowImportExportEverywhere: true, + babelOptions: { + presets: ['next/babel'], + caller: { + // Eslint supports top level await when a parser for it is included. We enable the parser by default for Babel. + supportsTopLevelAwait: true, + }, + }, + }, + overrides: [ + { + files: ['**/*.ts?(x)'], + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + warnOnUnsupportedTypeScriptVersion: true, + }, + }, + ], + settings: { + react: { + version: 'detect', + }, + 'import/parsers': { + [require.resolve('@typescript-eslint/parser')]: ['.ts', '.tsx', '.d.ts'], + }, + 'import/resolver': { + [require.resolve('eslint-import-resolver-node')]: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + [require.resolve('eslint-import-resolver-typescript')]: { + alwaysTryTypes: true, + }, + }, + }, + env: { + browser: true, + node: true, + }, +} diff --git a/nextjs/packages/eslint-config-next/package.json b/nextjs/packages/eslint-config-next/package.json new file mode 100644 index 0000000000..10f17f5070 --- /dev/null +++ b/nextjs/packages/eslint-config-next/package.json @@ -0,0 +1,23 @@ +{ + "name": "eslint-config-blitz", + "version": "0.45.4", + "description": "Blitz.js eslint config", + "main": "index.js", + "license": "MIT", + "repository": { + "url": "blitz-js/blitz", + "directory": "packages/eslint-config-next" + }, + "dependencies": { + "@next/eslint-plugin-next": "11.1.0", + "@rushstack/eslint-patch": "^1.0.6", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-import-resolver-typescript": "^2.4.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.23.1", + "eslint-plugin-react-hooks": "^4.2.0" + } +} diff --git a/nextjs/packages/eslint-config-next/parser.js b/nextjs/packages/eslint-config-next/parser.js new file mode 100644 index 0000000000..9e9fd22cfc --- /dev/null +++ b/nextjs/packages/eslint-config-next/parser.js @@ -0,0 +1,9 @@ +const { + parse, + parseForESLint, +} = require('next/dist/compiled/babel/eslint-parser') + +module.exports = { + parse, + parseForESLint, +} diff --git a/nextjs/packages/eslint-plugin-next/README.md b/nextjs/packages/eslint-plugin-next/README.md new file mode 100644 index 0000000000..4e4f393c3a --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/README.md @@ -0,0 +1,4 @@ +# `@next/eslint-plugin-next` + +Documentation for `@next/eslint-plugin-next` can be found at: +https://nextjs.org/docs/basic-features/eslint#eslint-plugin diff --git a/nextjs/packages/eslint-plugin-next/jsconfig.json b/nextjs/packages/eslint-plugin-next/jsconfig.json new file mode 100644 index 0000000000..e4468a2aee --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2019" + }, + "exclude": ["node_modules"] +} diff --git a/nextjs/packages/eslint-plugin-next/lib/index.js b/nextjs/packages/eslint-plugin-next/lib/index.js new file mode 100644 index 0000000000..df01916cca --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/index.js @@ -0,0 +1,47 @@ +module.exports = { + rules: { + 'no-css-tags': require('./rules/no-css-tags'), + 'no-sync-scripts': require('./rules/no-sync-scripts'), + 'no-html-link-for-pages': require('./rules/no-html-link-for-pages'), + 'no-img-element': require('./rules/no-img-element'), + 'no-unwanted-polyfillio': require('./rules/no-unwanted-polyfillio'), + 'no-page-custom-font': require('./rules/no-page-custom-font'), + 'no-title-in-document-head': require('./rules/no-title-in-document-head'), + 'google-font-display': require('./rules/google-font-display'), + 'google-font-preconnect': require('./rules/google-font-preconnect'), + 'link-passhref': require('./rules/link-passhref'), + 'no-document-import-in-page': require('./rules/no-document-import-in-page'), + 'no-head-import-in-document': require('./rules/no-head-import-in-document'), + 'no-typos': require('./rules/no-typos'), + 'no-duplicate-head': require('./rules/no-duplicate-head'), + }, + configs: { + recommended: { + plugins: ['@next/next'], + rules: { + '@next/next/no-css-tags': 1, + '@next/next/no-sync-scripts': 1, + '@next/next/no-html-link-for-pages': 1, + '@next/next/no-img-element': 1, + '@next/next/no-unwanted-polyfillio': 1, + '@next/next/no-page-custom-font': 1, + '@next/next/no-title-in-document-head': 1, + '@next/next/google-font-display': 1, + '@next/next/google-font-preconnect': 1, + '@next/next/link-passhref': 1, + '@next/next/no-document-import-in-page': 2, + '@next/next/no-head-import-in-document': 2, + '@next/next/no-typos': 1, + '@next/next/no-duplicate-head': 2, + }, + }, + 'core-web-vitals': { + plugins: ['@next/next'], + extends: ['plugin:@next/next/recommended'], + rules: { + '@next/next/no-sync-scripts': 2, + '@next/next/no-html-link-for-pages': 2, + }, + }, + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/google-font-display.js b/nextjs/packages/eslint-plugin-next/lib/rules/google-font-display.js new file mode 100644 index 0000000000..1dc26c5ed7 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/google-font-display.js @@ -0,0 +1,56 @@ +const NodeAttributes = require('../utils/node-attributes.js') + +module.exports = { + meta: { + docs: { + description: + 'Ensure correct font-display property is assigned for Google Fonts', + recommended: true, + }, + }, + create: function (context) { + return { + JSXOpeningElement(node) { + let message + + if (node.name.name !== 'link') { + return + } + + const attributes = new NodeAttributes(node) + if (!attributes.has('href') || !attributes.hasValue('href')) { + return + } + + const hrefValue = attributes.value('href') + const isGoogleFont = + typeof hrefValue === 'string' && + hrefValue.startsWith('https://fonts.googleapis.com/css') + + if (isGoogleFont) { + const params = new URLSearchParams(hrefValue.split('?')[1]) + const displayValue = params.get('display') + + if (!params.has('display')) { + message = 'Display parameter is missing.' + } else if ( + displayValue === 'block' || + displayValue === 'fallback' || + displayValue === 'auto' + ) { + message = `${ + displayValue[0].toUpperCase() + displayValue.slice(1) + } behavior is not recommended.` + } + } + + if (message) { + context.report({ + node, + message: `${message} See https://nextjs.org/docs/messages/google-font-display.`, + }) + } + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js b/nextjs/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js new file mode 100644 index 0000000000..930545ae89 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js @@ -0,0 +1,41 @@ +const NodeAttributes = require('../utils/node-attributes.js') + +module.exports = { + meta: { + docs: { + description: 'Ensure preconnect is used with Google Fonts', + recommended: true, + }, + }, + create: function (context) { + return { + JSXOpeningElement(node) { + if (node.name.name !== 'link') { + return + } + + const attributes = new NodeAttributes(node) + if (!attributes.has('href') || !attributes.hasValue('href')) { + return + } + + const hrefValue = attributes.value('href') + const preconnectMissing = + !attributes.has('rel') || + !attributes.hasValue('rel') || + attributes.value('rel') !== 'preconnect' + + if ( + typeof hrefValue === 'string' && + hrefValue.startsWith('https://fonts.gstatic.com') && + preconnectMissing + ) { + context.report({ + node, + message: `Preconnect is missing. See https://nextjs.org/docs/messages/google-font-preconnect.`, + }) + } + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/link-passhref.js b/nextjs/packages/eslint-plugin-next/lib/rules/link-passhref.js new file mode 100644 index 0000000000..dc41a44051 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/link-passhref.js @@ -0,0 +1,63 @@ +const NodeAttributes = require('../utils/node-attributes.js') + +module.exports = { + meta: { + docs: { + description: + 'Ensure passHref is assigned if child of Link component is a custom component', + category: 'HTML', + recommended: true, + }, + fixable: null, + }, + + create: function (context) { + let linkImport = null + + return { + ImportDeclaration(node) { + if (node.source.value === 'next/link') { + linkImport = node.specifiers[0].local.name + } + }, + + JSXOpeningElement(node) { + if (node.name.name !== 'Link' || node.name.name !== linkImport) { + return + } + + const attributes = new NodeAttributes(node) + const children = node.parent.children + + if ( + !attributes.hasAny() || + !attributes.has('href') || + !children.some((attr) => attr.type === 'JSXElement') + ) { + return + } + + const hasPassHref = + attributes.has('passHref') && + (typeof attributes.value('passHref') === 'undefined' || + attributes.value('passHref') === true) + + const hasAnchorChild = children.some( + (attr) => + attr.type === 'JSXElement' && attr.openingElement.name.name === 'a' + ) + + if (!hasAnchorChild && !hasPassHref) { + context.report({ + node, + message: `passHref ${ + attributes.value('passHref') !== true + ? 'must be set to true' + : 'is missing' + }. See https://nextjs.org/docs/messages/link-passhref`, + }) + } + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-css-tags.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-css-tags.js new file mode 100644 index 0000000000..83b40f51f0 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-css-tags.js @@ -0,0 +1,36 @@ +module.exports = function (context) { + return { + JSXOpeningElement(node) { + if (node.name.name !== 'link') { + return + } + if (node.attributes.length === 0) { + return + } + + const attributes = node.attributes.filter( + (attr) => attr.type === 'JSXAttribute' + ) + if ( + attributes.find( + (attr) => + attr.name.name === 'rel' && attr.value.value === 'stylesheet' + ) && + attributes.find( + (attr) => + attr.name.name === 'href' && + attr.value.type === 'Literal' && + !/^https?/.test(attr.value.value) + ) + ) { + context.report({ + node, + message: + 'Do not include stylesheets manually. See: https://nextjs.org/docs/messages/no-css-tags.', + }) + } + }, + } +} + +module.exports.schema = [] diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-document-import-in-page.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-document-import-in-page.js new file mode 100644 index 0000000000..325974a93c --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-document-import-in-page.js @@ -0,0 +1,35 @@ +const path = require('path') + +module.exports = { + meta: { + docs: { + description: + 'Disallow importing next/document outside of pages/document.js', + recommended: true, + }, + }, + create: function (context) { + return { + ImportDeclaration(node) { + if (node.source.value !== 'next/document') { + return + } + + const page = context.getFilename().split('pages')[1] + const { name, dir } = path.parse(page) + if ( + !page || + name.startsWith('_document') || + (dir === '/_document' && name === 'index') + ) { + return + } + + context.report({ + node, + message: `next/document should not be imported outside of pages/_document.js. See https://nextjs.org/docs/messages/no-document-import-in-page.`, + }) + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-duplicate-head.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-duplicate-head.js new file mode 100644 index 0000000000..3a2df1205c --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-duplicate-head.js @@ -0,0 +1,55 @@ +module.exports = { + meta: { + docs: { + description: 'Enforce no duplicate usage of in pages/document.js', + recommended: true, + }, + }, + create: function (context) { + let documentImportName + return { + ImportDeclaration(node) { + if (node.source.value === 'next/document') { + const documentImport = node.specifiers.find( + ({ type }) => type === 'ImportDefaultSpecifier' + ) + if (documentImport && documentImport.local) { + documentImportName = documentImport.local.name + } + } + }, + ReturnStatement(node) { + const ancestors = context.getAncestors() + const documentClass = ancestors.find( + (ancestorNode) => + ancestorNode.type === 'ClassDeclaration' && + ancestorNode.superClass && + ancestorNode.superClass.name === documentImportName + ) + + if (!documentClass) { + return + } + + if (node.argument && node.argument.children) { + const headComponents = node.argument.children.filter( + (childrenNode) => + childrenNode.openingElement && + childrenNode.openingElement.name && + childrenNode.openingElement.name.name === 'Head' + ) + + if (headComponents.length > 1) { + for (let i = 1; i < headComponents.length; i++) { + context.report({ + node: headComponents[i], + message: + 'Do not include multiple instances of . See: https://nextjs.org/docs/messages/no-duplicate-head', + }) + } + } + } + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-head-import-in-document.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-head-import-in-document.js new file mode 100644 index 0000000000..5d91fc2279 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-head-import-in-document.js @@ -0,0 +1,36 @@ +const path = require('path') + +module.exports = { + meta: { + docs: { + description: 'Disallow importing next/head in pages/document.js', + recommended: true, + }, + }, + create: function (context) { + return { + ImportDeclaration(node) { + if (node.source.value !== 'next/head') { + return + } + + const document = context.getFilename().split('pages')[1] + if (!document) { + return + } + + const { name, dir } = path.parse(document) + + if ( + name.startsWith('_document') || + (dir === '/_document' && name === 'index') + ) { + context.report({ + node, + message: `next/head should not be imported in pages${document}. Import Head from next/document instead. See https://nextjs.org/docs/messages/no-head-import-in-document.`, + }) + } + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js new file mode 100644 index 0000000000..c1a6eda644 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js @@ -0,0 +1,116 @@ +// @ts-check +const path = require('path') +const fs = require('fs') +const getRootDir = require('../utils/get-root-dirs') +const { + getUrlFromPagesDirectories, + normalizeURL, + execOnce, +} = require('../utils/url') + +const pagesDirWarning = execOnce((pagesDirs) => { + console.warn( + `Pages directory cannot be found at ${pagesDirs.join(' or ')}. ` + + `If using a custom path, please configure with the no-html-link-for-pages rule in your eslint config file` + ) +}) + +// Cache for fs.existsSync lookup. +// Prevent multiple blocking IO requests that have already been calculated. +const fsExistsSyncCache = {} + +module.exports = { + meta: { + docs: { + description: 'Prohibit full page refresh for Next.js pages', + category: 'HTML', + recommended: true, + }, + fixable: null, // or "code" or "whitespace" + schema: [ + { + oneOf: [ + { + type: 'string', + }, + { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + }, + }, + ], + }, + ], + }, + + /** + * Creates an ESLint rule listener. + * + * @param {import('eslint').Rule.RuleContext} context - ESLint rule context + * @returns {import('eslint').Rule.RuleListener} An ESLint rule listener + */ + create: function (context) { + /** @type {(string|string[])[]} */ + const ruleOptions = context.options + const [customPagesDirectory] = ruleOptions + + const rootDirs = getRootDir(context) + + const pagesDirs = (customPagesDirectory + ? [customPagesDirectory] + : rootDirs.map((dir) => [ + path.join(dir, 'pages'), + path.join(dir, 'src', 'pages'), + ]) + ).flat() + + const foundPagesDirs = pagesDirs.filter((dir) => { + if (fsExistsSyncCache[dir] === undefined) { + fsExistsSyncCache[dir] = fs.existsSync(dir) + } + return fsExistsSyncCache[dir] + }) + if (foundPagesDirs.length === 0) { + pagesDirWarning(pagesDirs) + return {} + } + + const urls = getUrlFromPagesDirectories('/', foundPagesDirs) + return { + JSXOpeningElement(node) { + if (node.name.name !== 'a') { + return + } + + if (node.attributes.length === 0) { + return + } + + const href = node.attributes.find( + (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'href' + ) + + if (!href || href.value.type !== 'Literal') { + return + } + + const hrefPath = normalizeURL(href.value.value) + // Outgoing links are ignored + if (/^(https?:\/\/|\/\/)/.test(hrefPath)) { + return + } + + urls.forEach((url) => { + if (url.test(normalizeURL(hrefPath))) { + context.report({ + node, + message: `Do not use the HTML tag to navigate to ${hrefPath}. Use Link from 'next/link' instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages.`, + }) + } + }) + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-img-element.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-img-element.js new file mode 100644 index 0000000000..4a4a6fdf53 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-img-element.js @@ -0,0 +1,29 @@ +module.exports = { + meta: { + docs: { + description: 'Prohibit usage of HTML element', + category: 'HTML', + recommended: true, + }, + fixable: 'code', + }, + + create: function (context) { + return { + JSXOpeningElement(node) { + if (node.name.name !== 'img') { + return + } + + if (node.attributes.length === 0) { + return + } + + context.report({ + node, + message: `Do not use . Use Image from 'blitz' instead. See https://nextjs.org/docs/messages/no-img-element.`, + }) + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js new file mode 100644 index 0000000000..94ecaba8ee --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js @@ -0,0 +1,58 @@ +const NodeAttributes = require('../utils/node-attributes.js') + +module.exports = { + meta: { + docs: { + description: + 'Recommend adding custom font in a custom document and not in a specific page', + recommended: true, + }, + }, + create: function (context) { + let documentImportName + return { + ImportDeclaration(node) { + if (node.source.value === 'next/document') { + const documentImport = node.specifiers.find( + ({ type }) => type === 'ImportDefaultSpecifier' + ) + if (documentImport && documentImport.local) { + documentImportName = documentImport.local.name + } + } + }, + JSXOpeningElement(node) { + const documentClass = context + .getAncestors() + .find( + (ancestorNode) => + ancestorNode.type === 'ClassDeclaration' && + ancestorNode.superClass && + ancestorNode.superClass.name === documentImportName + ) + + if (documentClass || node.name.name !== 'link') { + return + } + + const attributes = new NodeAttributes(node) + if (!attributes.has('href') || !attributes.hasValue('href')) { + return + } + + const hrefValue = attributes.value('href') + const isGoogleFont = + typeof hrefValue === 'string' && + hrefValue.startsWith('https://fonts.googleapis.com/css') + + if (isGoogleFont) { + context.report({ + node, + message: + 'Custom fonts not added at the document level will only load for a single page. This is discouraged. See https://nextjs.org/docs/messages/no-page-custom-font.', + }) + } + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-sync-scripts.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-sync-scripts.js new file mode 100644 index 0000000000..fb4326c19f --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-sync-scripts.js @@ -0,0 +1,28 @@ +module.exports = function (context) { + return { + JSXOpeningElement(node) { + if (node.name.name !== 'script') { + return + } + if (node.attributes.length === 0) { + return + } + const attributeNames = node.attributes + .filter((attr) => attr.type === 'JSXAttribute') + .map((attr) => attr.name.name) + if ( + attributeNames.includes('src') && + !attributeNames.includes('async') && + !attributeNames.includes('defer') + ) { + context.report({ + node, + message: + 'External synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.', + }) + } + }, + } +} + +module.exports.schema = [] diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-title-in-document-head.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-title-in-document-head.js new file mode 100644 index 0000000000..a945566d7c --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-title-in-document-head.js @@ -0,0 +1,48 @@ +module.exports = { + meta: { + docs: { + description: 'Disallow using with Head from next/document', + }, + }, + create: function (context) { + let headFromNextDocument = false + return { + ImportDeclaration(node) { + if (node.source.value === 'next/document') { + if (node.specifiers.some(({ local }) => local.name === 'Head')) { + headFromNextDocument = true + } + } + }, + JSXElement(node) { + if (!headFromNextDocument) { + return + } + + if ( + node.openingElement && + node.openingElement.name && + node.openingElement.name.name !== 'Head' + ) { + return + } + + const titleTag = node.children.find( + (child) => + child.openingElement && + child.openingElement.name && + child.openingElement.name.type === 'JSXIdentifier' && + child.openingElement.name.name === 'title' + ) + + if (titleTag) { + context.report({ + node: titleTag, + message: + 'Titles should be defined at the page-level using next/head. See https://nextjs.org/docs/messages/no-title-in-document-head.', + }) + } + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-typos.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-typos.js new file mode 100644 index 0000000000..e517993c57 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-typos.js @@ -0,0 +1,107 @@ +const path = require('path') + +const NEXT_EXPORT_FUNCTIONS = [ + 'getStaticProps', + 'getStaticPaths', + 'getServerSideProps', +] + +// 0 is the exact match +const THRESHOLD = 1 + +// the minimum number of operations required to convert string a to string b. +function minDistance(a, b) { + const m = a.length + const n = b.length + + if (m < n) { + return minDistance(b, a) + } + + if (n === 0) { + return m + } + + let previousRow = Array.from({ length: n + 1 }, (_, i) => i) + + for (let i = 0; i < m; i++) { + const s1 = a[i] + let currentRow = [i + 1] + for (let j = 0; j < n; j++) { + const s2 = b[j] + const insertions = previousRow[j + 1] + 1 + const deletions = currentRow[j] + 1 + const substitutions = previousRow[j] + Number(s1 !== s2) + currentRow.push(Math.min(insertions, deletions, substitutions)) + } + previousRow = currentRow + } + return previousRow[previousRow.length - 1] +} + +module.exports = { + meta: { + docs: { + description: 'Prevent common typos', + category: 'Stylistic Issues', + recommended: true, + }, + fixable: null, + }, + + create: function (context) { + function checkTypos(node, name) { + if (NEXT_EXPORT_FUNCTIONS.includes(name)) { + return + } + + const potentialTypos = NEXT_EXPORT_FUNCTIONS.map((o) => ({ + option: o, + distance: minDistance(o, name), + })) + .filter(({ distance }) => distance <= THRESHOLD && distance > 0) + .sort((a, b) => a.distance - b.distance) + + if (potentialTypos.length) { + context.report({ + node, + message: `${name} may be a typo. Did you mean ${potentialTypos[0].option}?`, + }) + } + } + return { + ExportNamedDeclaration(node) { + const page = context.getFilename().split('pages')[1] + if (!page || path.parse(page).dir.startsWith('/api')) { + return + } + + const decl = node.declaration + + if (!decl) { + return + } + + switch (decl.type) { + case 'FunctionDeclaration': { + checkTypos(node, decl.id.name) + break + } + case 'VariableDeclaration': { + decl.declarations.forEach((d) => { + if (d.id.type !== 'Identifier') { + return + } + checkTypos(node, d.id.name) + }) + break + } + default: { + break + } + } + return + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/rules/no-unwanted-polyfillio.js b/nextjs/packages/eslint-plugin-next/lib/rules/no-unwanted-polyfillio.js new file mode 100644 index 0000000000..fa83aba952 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/rules/no-unwanted-polyfillio.js @@ -0,0 +1,109 @@ +const NEXT_POLYFILLED_FEATURES = [ + 'Array.prototype.@@iterator', + 'Array.prototype.copyWithin', + 'Array.prototype.fill', + 'Array.prototype.find', + 'Array.prototype.findIndex', + 'Array.prototype.flatMap', + 'Array.prototype.flat', + 'Array.from', + 'Array.prototype.includes', + 'Array.of', + 'Function.prototype.name', + 'fetch', + 'Map', + 'Number.EPSILON', + 'Number.Epsilon', + 'Number.isFinite', + 'Number.isNaN', + 'Number.isInteger', + 'Number.isSafeInteger', + 'Number.MAX_SAFE_INTEGER', + 'Number.MIN_SAFE_INTEGER', + 'Object.entries', + 'Object.getOwnPropertyDescriptor', + 'Object.getOwnPropertyDescriptors', + 'Object.is', + 'Object.keys', + 'Object.values', + 'Reflect', + 'Set', + 'Symbol', + 'Symbol.asyncIterator', + 'String.prototype.codePointAt', + 'String.prototype.endsWith', + 'String.fromCodePoint', + 'String.prototype.includes', + 'String.prototype.@@iterator', + 'String.prototype.padEnd', + 'String.prototype.padStart', + 'String.prototype.repeat', + 'String.raw', + 'String.prototype.startsWith', + 'String.prototype.trimEnd', + 'String.prototype.trimStart', + 'String.prototype.trim', + 'URL', + 'URLSearchParams', + 'WeakMap', + 'WeakSet', + 'Promise', + 'Promise.prototype.finally', + 'es2015', // Should be covered by babel-preset-env instead. + 'es2016', // Should be covered by babel-preset-env instead. + 'es2017', // Should be covered by babel-preset-env instead. + 'es2018', // Should be covered by babel-preset-env instead. + 'es2019', // Should be covered by babel-preset-env instead. + 'es5', // Should be covered by babel-preset-env instead. + 'es6', // Should be covered by babel-preset-env instead. + 'es7', // Should be covered by babel-preset-env instead. +] + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +module.exports = { + meta: { + docs: { + description: + 'Prohibit unwanted features to be listed in Polyfill.io tag.', + category: 'HTML', + recommended: true, + }, + fixable: null, // or "code" or "whitespace" + }, + + create: function (context) { + return { + 'JSXOpeningElement[name.name=script][attributes.length>0]'(node) { + const srcNode = node.attributes.find( + (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'src' + ) + if (!srcNode || srcNode.value.type !== 'Literal') { + return + } + const src = srcNode.value.value + if ( + src.startsWith('https://cdn.polyfill.io/v2/') || + src.startsWith('https://polyfill.io/v3/') + ) { + const featureQueryString = new URL(src).searchParams.get('features') + const featuresRequested = (featureQueryString || '').split(',') + const unwantedFeatures = featuresRequested.filter((feature) => + NEXT_POLYFILLED_FEATURES.includes(feature) + ) + if (unwantedFeatures.length > 0) { + context.report({ + node, + message: `No duplicate polyfills from Polyfill.io are allowed. ${unwantedFeatures.join( + ', ' + )} ${ + unwantedFeatures.length > 1 ? 'are' : 'is' + } already shipped with Next.js. See: https://nextjs.org/docs/messages/no-unwanted-polyfillio.`, + }) + } + } + }, + } + }, +} diff --git a/nextjs/packages/eslint-plugin-next/lib/utils/get-root-dirs.js b/nextjs/packages/eslint-plugin-next/lib/utils/get-root-dirs.js new file mode 100644 index 0000000000..9a65aa9920 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/utils/get-root-dirs.js @@ -0,0 +1,40 @@ +// @ts-check +const glob = require('glob') + +/** + * Process a Next.js root directory glob. + * + * @param {string} rootDir - A Next.js root directory glob. + * @returns {string[]} - An array of Root directories. + */ +const processRootDir = (rootDir) => { + // Ensures we only match folders. + if (!rootDir.endsWith('/')) rootDir += '/' + return glob.sync(rootDir) +} + +/** + * Gets one or more Root + * + * @param {import('eslint').Rule.RuleContext} context - ESLint rule context + * @returns An array of root directories. + */ +const getRootDirs = (context) => { + let rootDirs = [context.getCwd()] + + /** @type {{rootDir?:string|string[]}|undefined} */ + const nextSettings = context.settings.next || {} + let rootDir = nextSettings.rootDir + + if (typeof rootDir === 'string') { + rootDirs = processRootDir(rootDir) + } else if (Array.isArray(rootDir)) { + rootDirs = rootDir + .map((dir) => (typeof dir === 'string' ? processRootDir(dir) : [])) + .flat() + } + + return rootDirs +} + +module.exports = getRootDirs diff --git a/nextjs/packages/eslint-plugin-next/lib/utils/node-attributes.js b/nextjs/packages/eslint-plugin-next/lib/utils/node-attributes.js new file mode 100644 index 0000000000..c9eb583275 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/utils/node-attributes.js @@ -0,0 +1,52 @@ +// Return attributes and values of a node in a convenient way: +/* example: + <ExampleElement attr1="15" attr2> + { attr1: { + hasValue: true, + value: 15 + }, + attr2: { + hasValue: false + } +Inclusion of hasValue is in case an eslint rule cares about boolean values +explicitely assigned to attribute vs the attribute being used as a flag +*/ +class NodeAttributes { + constructor(ASTnode) { + this.attributes = {} + ASTnode.attributes.forEach((attribute) => { + if (!attribute.type || attribute.type !== 'JSXAttribute') { + return + } + this.attributes[attribute.name.name] = { + hasValue: !!attribute.value, + } + if (attribute.value) { + if (attribute.value.value) { + this.attributes[attribute.name.name].value = attribute.value.value + } else if (attribute.value.expression) { + this.attributes[attribute.name.name].value = + attribute.value.expression.value + } + } + }) + } + hasAny() { + return !!Object.keys(this.attributes).length + } + has(attrName) { + return !!this.attributes[attrName] + } + hasValue(attrName) { + return !!this.attributes[attrName].hasValue + } + value(attrName) { + if (!this.attributes[attrName]) { + return true + } + + return this.attributes[attrName].value + } +} + +module.exports = NodeAttributes diff --git a/nextjs/packages/eslint-plugin-next/lib/utils/nodeAttributes.js b/nextjs/packages/eslint-plugin-next/lib/utils/nodeAttributes.js new file mode 100644 index 0000000000..c9eb583275 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/utils/nodeAttributes.js @@ -0,0 +1,52 @@ +// Return attributes and values of a node in a convenient way: +/* example: + <ExampleElement attr1="15" attr2> + { attr1: { + hasValue: true, + value: 15 + }, + attr2: { + hasValue: false + } +Inclusion of hasValue is in case an eslint rule cares about boolean values +explicitely assigned to attribute vs the attribute being used as a flag +*/ +class NodeAttributes { + constructor(ASTnode) { + this.attributes = {} + ASTnode.attributes.forEach((attribute) => { + if (!attribute.type || attribute.type !== 'JSXAttribute') { + return + } + this.attributes[attribute.name.name] = { + hasValue: !!attribute.value, + } + if (attribute.value) { + if (attribute.value.value) { + this.attributes[attribute.name.name].value = attribute.value.value + } else if (attribute.value.expression) { + this.attributes[attribute.name.name].value = + attribute.value.expression.value + } + } + }) + } + hasAny() { + return !!Object.keys(this.attributes).length + } + has(attrName) { + return !!this.attributes[attrName] + } + hasValue(attrName) { + return !!this.attributes[attrName].hasValue + } + value(attrName) { + if (!this.attributes[attrName]) { + return true + } + + return this.attributes[attrName].value + } +} + +module.exports = NodeAttributes diff --git a/nextjs/packages/eslint-plugin-next/lib/utils/url.js b/nextjs/packages/eslint-plugin-next/lib/utils/url.js new file mode 100644 index 0000000000..838b40c4d3 --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/lib/utils/url.js @@ -0,0 +1,117 @@ +const fs = require('fs') +const path = require('path') + +// Cache for fs.lstatSync lookup. +// Prevent multiple blocking IO requests that have already been calculated. +const fsLstatSyncCache = {} +const fsLstatSync = (source) => { + fsLstatSyncCache[source] = fsLstatSyncCache[source] || fs.lstatSync(source) + return fsLstatSyncCache[source] +} + +/** + * Checks if the source is a directory. + * @param {string} source + */ +function isDirectory(source) { + return fsLstatSync(source).isDirectory() +} + +/** + * Checks if the source is a directory. + * @param {string} source + */ +function isSymlink(source) { + return fsLstatSync(source).isSymbolicLink() +} + +/** + * Gets the possible URLs from a directory. + * @param {string} urlprefix + * @param {string[]} directories + */ +function getUrlFromPagesDirectories(urlPrefix, directories) { + return Array.from( + // De-duplicate similar pages across multiple directories. + new Set( + directories + .map((directory) => parseUrlForPages(urlPrefix, directory)) + .flat() + .map( + // Since the URLs are normalized we add `^` and `$` to the RegExp to make sure they match exactly. + (url) => `^${normalizeURL(url)}$` + ) + ) + ).map((urlReg) => new RegExp(urlReg)) +} + +// Cache for fs.readdirSync lookup. +// Prevent multiple blocking IO requests that have already been calculated. +const fsReadDirSyncCache = {} + +/** + * Recursively parse directory for page URLs. + * @param {string} urlprefix + * @param {string} directory + */ +function parseUrlForPages(urlprefix, directory) { + fsReadDirSyncCache[directory] = + fsReadDirSyncCache[directory] || fs.readdirSync(directory) + const res = [] + fsReadDirSyncCache[directory].forEach((fname) => { + if (/(\.(j|t)sx?)$/.test(fname)) { + fname = fname.replace(/\[.*\]/g, '.*') + if (/^index(\.(j|t)sx?)$/.test(fname)) { + res.push(`${urlprefix}${fname.replace(/^index(\.(j|t)sx?)$/, '')}`) + } + res.push(`${urlprefix}${fname.replace(/(\.(j|t)sx?)$/, '')}`) + } else { + const dirPath = path.join(directory, fname) + if (isDirectory(dirPath) && !isSymlink(dirPath)) { + res.push(...parseUrlForPages(urlprefix + fname + '/', dirPath)) + } + } + }) + return res +} + +/** + * Takes a URL and does the following things. + * - Replaces `index.html` with `/` + * - Makes sure all URLs are have a trailing `/` + * - Removes query string + * @param {string} url + */ +function normalizeURL(url) { + if (!url) { + return + } + url = url.split('?')[0] + url = url.split('#')[0] + url = url = url.replace(/(\/index\.html)$/, '/') + // Empty URLs should not be trailed with `/`, e.g. `#heading` + if (url === '') { + return url + } + url = url.endsWith('/') ? url : url + '/' + return url +} + +function execOnce(fn) { + let used = false + let result + + return (...args) => { + if (!used) { + used = true + result = fn(...args) + } + return result + } +} + +module.exports = { + getUrlFromPagesDirectories, + normalizeURL, + execOnce, +} diff --git a/nextjs/packages/eslint-plugin-next/package.json b/nextjs/packages/eslint-plugin-next/package.json new file mode 100644 index 0000000000..04d0c77edd --- /dev/null +++ b/nextjs/packages/eslint-plugin-next/package.json @@ -0,0 +1,21 @@ +{ + "private": true, + "name": "@next/eslint-plugin-next", + "version": "11.1.0", + "description": "ESLint plugin for NextJS.", + "main": "lib/index.js", + "license": "MIT", + "repository": { + "url": "vercel/next.js", + "directory": "packages/eslint-plugin-next" + }, + "files": [ + "lib" + ], + "dependencies": { + "glob": "7.1.7" + }, + "devDependencies": { + "@types/eslint": "7.28.0" + } +} diff --git a/nextjs/packages/installer/.gitignore b/nextjs/packages/installer/.gitignore new file mode 100644 index 0000000000..775069de51 --- /dev/null +++ b/nextjs/packages/installer/.gitignore @@ -0,0 +1,15 @@ +*.log +.DS_Store +node_modules +.rts2_cache_cjs +.rts2_cache_esm +.rts2_cache_umd +.rts2_cache_system +dist +tmp +.blitz + +# good directory to use for testing app generation +_app + +!templates/**/.env diff --git a/nextjs/packages/installer/README.md b/nextjs/packages/installer/README.md new file mode 100644 index 0000000000..b7d6e95b55 --- /dev/null +++ b/nextjs/packages/installer/README.md @@ -0,0 +1,5 @@ +# `installer` + +The installer package houses all of the types, classes, and utilities for building a Blitz Recipe. A Blitz recipe is effectively just a list of steps, represented as an array of objects that conform to one of the types in the `Executor` union type (`NewFileExecutor | AddDependencyExecutor | FileTransformExecutor`). These executors are processed by the framework, executed interactively by the user, and ultimately run to install new packages to an existing Blitz app. + +You can find the implementation of all Executors in the `executors/` directory, stock transforms that we'll be supplying to authors in `transforms/`, and various utilities in `utils/`, including a `paths` utility that the user can access for common paths to modify such as `_document.tsx`. diff --git a/nextjs/packages/installer/jest.config.js b/nextjs/packages/installer/jest.config.js new file mode 100644 index 0000000000..a1d97949ab --- /dev/null +++ b/nextjs/packages/installer/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: '../../../jest-unit.config.js', +} diff --git a/nextjs/packages/installer/jest.setup.js b/nextjs/packages/installer/jest.setup.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/packages/installer/package.json b/nextjs/packages/installer/package.json new file mode 100644 index 0000000000..cf24538a3e --- /dev/null +++ b/nextjs/packages/installer/package.json @@ -0,0 +1,50 @@ +{ + "name": "@blitzjs/installer", + "version": "0.45.4", + "description": "Package installation for the Blitz CLI", + "homepage": "https://github.com/blitz-js/blitz#readme", + "license": "MIT", + "scripts": { + "test": "jest", + "test:watch": "jest --watch" + }, + "author": { + "name": "Brandon Bayer", + "email": "b@bayer.ws", + "url": "https://twitter.com/flybayer" + }, + "main": "dist/blitzjs-installer.cjs.js", + "module": "dist/blitzjs-installer.esm.js", + "types": "dist/blitzjs-installer.cjs.d.ts", + "files": [ + "dist" + ], + "keywords": [ + "blitz", + "installer" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/blitz-js/blitz.git" + }, + "dependencies": { + "@babel/core": "7.12.10", + "@babel/plugin-transform-typescript": "7.12.1", + "@blitzjs/generator": "0.45.4", + "@mrleebo/prisma-ast": "0.2.6", + "@prisma/sdk": "3.9.1", + "@types/jscodeshift": "0.11.2", + "ast-types": "0.14.2", + "cross-spawn": "7.0.3", + "diff": "5.0.0", + "enquirer": "2.3.6", + "fs-extra": "^9.1.0", + "globby": "11.0.2", + "ink": "3.2.0", + "ink-spinner": "4.0.3", + "ink-testing-library": "2.1.0", + "jscodeshift": "0.13.0", + "recast": "0.20.5" + }, + "gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922" +} diff --git a/nextjs/packages/installer/src/components/enter-to-continue.tsx b/nextjs/packages/installer/src/components/enter-to-continue.tsx new file mode 100644 index 0000000000..22d7db85b7 --- /dev/null +++ b/nextjs/packages/installer/src/components/enter-to-continue.tsx @@ -0,0 +1,12 @@ +import { Text } from 'ink' +import * as React from 'react' +import { Newline } from './newline' + +export const EnterToContinue: React.FC<{ message?: string }> = ({ + message = 'Press ENTER to continue', +}) => ( + <> + <Newline /> + <Text bold>{message}</Text> + </> +) diff --git a/nextjs/packages/installer/src/components/newline.tsx b/nextjs/packages/installer/src/components/newline.tsx new file mode 100644 index 0000000000..7ccb6cc585 --- /dev/null +++ b/nextjs/packages/installer/src/components/newline.tsx @@ -0,0 +1,6 @@ +import { Box } from 'ink' +import * as React from 'react' + +export const Newline: React.FC<{ count?: number }> = ({ count = 1 }) => { + return <Box paddingBottom={count} /> +} diff --git a/nextjs/packages/installer/src/executors/add-dependency-executor.tsx b/nextjs/packages/installer/src/executors/add-dependency-executor.tsx new file mode 100644 index 0000000000..20fce7bc07 --- /dev/null +++ b/nextjs/packages/installer/src/executors/add-dependency-executor.tsx @@ -0,0 +1,216 @@ +import { spawn } from 'cross-spawn' +import * as fs from 'fs-extra' +import { Box, Text } from 'ink' +import Spinner from 'ink-spinner' +import * as path from 'path' +import * as React from 'react' +import { Newline } from '../components/newline' +import { RecipeCLIArgs } from '../types' +import { useEnterToContinue } from '../utils/use-enter-to-continue' +import { useUserInput } from '../utils/use-user-input' +import { + Executor, + executorArgument, + ExecutorConfig, + getExecutorArgument, +} from './executor' + +interface NpmPackage { + name: string + // defaults to latest published + version?: string + // defaults to false + isDevDep?: boolean +} + +export interface Config extends ExecutorConfig { + packages: executorArgument<NpmPackage[]> +} + +export function isAddDependencyExecutor( + executor: ExecutorConfig +): executor is Config { + return (executor as Config).packages !== undefined +} + +export const type = 'add-dependency' + +function Package({ pkg, loading }: { pkg: NpmPackage; loading: boolean }) { + return ( + <Text> + {` `} + {loading ? <Spinner /> : '📦'} + {` ${pkg.name}@${pkg.version}`} + </Text> + ) +} + +const DependencyList = ({ + lede = 'Hang tight! Installing dependencies...', + depsLoading = false, + devDepsLoading = false, + packages, +}: { + lede?: string + depsLoading?: boolean + devDepsLoading?: boolean + packages: NpmPackage[] +}) => { + const prodPackages = packages.filter((p) => !p.isDevDep) + const devPackages = packages.filter((p) => p.isDevDep) + return ( + <Box flexDirection="column"> + <Text>{lede}</Text> + <Newline /> + {prodPackages.length ? <Text>Dependencies to be installed:</Text> : null} + {prodPackages.map((pkg) => ( + <Package key={pkg.name} pkg={pkg} loading={depsLoading} /> + ))} + <Newline /> + {devPackages.length ? ( + <Text>Dev Dependencies to be installed:</Text> + ) : null} + {devPackages.map((pkg) => ( + <Package key={pkg.name} pkg={pkg} loading={devDepsLoading} /> + ))} + </Box> + ) +} + +/** + * Exported for unit testing purposes + */ +export function getPackageManager() { + if (fs.existsSync(path.resolve('yarn.lock'))) { + return 'yarn' + } + return 'npm' +} + +/** + * Exported for unit testing purposes + */ +export async function installPackages(packages: NpmPackage[], isDev = false) { + const packageManager = getPackageManager() + const isNPM = packageManager === 'npm' + const pkgInstallArg = isNPM ? 'install' : 'add' + const args: string[] = [pkgInstallArg] + + if (isDev) { + args.push(isNPM ? '--save-dev' : '-D') + } + packages.forEach((pkg) => { + pkg.version ? args.push(`${pkg.name}@${pkg.version}`) : args.push(pkg.name) + }) + await new Promise((resolve) => { + const cp = spawn(packageManager, args, { + stdio: ['inherit', 'pipe', 'pipe'], + }) + cp.on('exit', resolve) + }) +} + +export const Commit: Executor['Commit'] = ({ + cliArgs, + cliFlags, + step, + onChangeCommitted, +}) => { + const userInput = useUserInput(cliFlags) + const [depsInstalled, setDepsInstalled] = React.useState(false) + const [devDepsInstalled, setDevDepsInstalled] = React.useState(false) + + const handleChangeCommitted = React.useCallback(() => { + const packages = (step as Config).packages + const dependencies = packages.length === 1 ? 'dependency' : 'dependencies' + onChangeCommitted(`Installed ${packages.length} ${dependencies}`) + }, [onChangeCommitted, step]) + + React.useEffect(() => { + async function installDeps() { + const packagesToInstall = getExecutorArgument( + (step as Config).packages, + cliArgs + ).filter((p) => !p.isDevDep) + await installPackages(packagesToInstall) + setDepsInstalled(true) + } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + installDeps() + }, [cliArgs, step]) + + React.useEffect(() => { + if (!depsInstalled) return + async function installDevDeps() { + const packagesToInstall = getExecutorArgument( + (step as Config).packages, + cliArgs + ).filter((p) => p.isDevDep) + await installPackages(packagesToInstall, true) + setDevDepsInstalled(true) + } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + installDevDeps() + }, [cliArgs, depsInstalled, step]) + + React.useEffect(() => { + if (depsInstalled && devDepsInstalled) { + handleChangeCommitted() + } + }, [depsInstalled, devDepsInstalled, handleChangeCommitted]) + + if (!isAddDependencyExecutor(step)) { + onChangeCommitted() + return null + } + + const childProps: CommitChildProps = { + depsInstalled, + devDepsInstalled, + handleChangeCommitted, + step, + cliArgs, + } + + if (userInput) return <CommitWithInput {...childProps} /> + else return <CommitWithoutInput {...childProps} /> +} + +interface CommitChildProps { + depsInstalled: boolean + devDepsInstalled: boolean + handleChangeCommitted: () => void + step: Config + cliArgs: RecipeCLIArgs +} + +const CommitWithInput = ({ + depsInstalled, + devDepsInstalled, + handleChangeCommitted, + step, + cliArgs, +}: CommitChildProps) => { + useEnterToContinue(handleChangeCommitted, depsInstalled && devDepsInstalled) + + return ( + <DependencyList + depsLoading={!depsInstalled} + devDepsLoading={!devDepsInstalled} + packages={getExecutorArgument(step.packages, cliArgs)} + /> + ) +} + +const CommitWithoutInput = ({ + depsInstalled, + devDepsInstalled, + step, + cliArgs, +}: CommitChildProps) => ( + <DependencyList + depsLoading={!depsInstalled} + devDepsLoading={!devDepsInstalled} + packages={getExecutorArgument(step.packages, cliArgs)} + /> +) diff --git a/nextjs/packages/installer/src/executors/executor.tsx b/nextjs/packages/installer/src/executors/executor.tsx new file mode 100644 index 0000000000..09ef140083 --- /dev/null +++ b/nextjs/packages/installer/src/executors/executor.tsx @@ -0,0 +1,74 @@ +import { Box, Text } from 'ink' +import * as React from 'react' +import { Newline } from '../components/newline' +import { RecipeCLIArgs, RecipeCLIFlags } from '../types' + +export interface ExecutorConfig { + successIcon?: string + stepId: string | number + stepName: string + stepType: string + // a bit to display to the user to give context to the change + explanation: string +} + +export interface Executor { + type: string + Propose?: React.FC<{ + step: ExecutorConfig + onProposalAccepted: (data?: any) => void + cliArgs: RecipeCLIArgs + cliFlags: RecipeCLIFlags + }> + Commit: React.FC<{ + step: ExecutorConfig + proposalData?: any + onChangeCommitted: (data?: any) => void + cliArgs: RecipeCLIArgs + cliFlags: RecipeCLIFlags + }> +} + +type dynamicExecutorArgument<T> = (cliArgs: RecipeCLIArgs) => T + +function isDynamicExecutorArgument<T>( + input: executorArgument<T> +): input is dynamicExecutorArgument<T> { + return typeof (input as dynamicExecutorArgument<T>) === 'function' +} + +export type executorArgument<T> = T | dynamicExecutorArgument<T> + +export function Frontmatter({ executor }: { executor: ExecutorConfig }) { + const lineLength = executor.stepName.length + 6 + const verticalBorder = `+${new Array(lineLength).fill('–').join('')}+` + return ( + <Box flexDirection="column" paddingBottom={1}> + <Newline /> + <Box flexDirection="column"> + <Text color="#8a3df0" bold> + {verticalBorder} + </Text> + <Text color="#8a3df0" bold> + ⎪   {executor.stepName}   ⎪ + </Text> + <Text color="#8a3df0" bold> + {verticalBorder} + </Text> + </Box> + <Text color="gray" italic> + {executor.explanation} + </Text> + </Box> + ) +} + +export function getExecutorArgument<T>( + input: executorArgument<T>, + cliArgs: RecipeCLIArgs +): T { + if (isDynamicExecutorArgument(input)) { + return input(cliArgs) + } + return input +} diff --git a/nextjs/packages/installer/src/executors/file-prompt.ts b/nextjs/packages/installer/src/executors/file-prompt.ts new file mode 100644 index 0000000000..0bcca9d44d --- /dev/null +++ b/nextjs/packages/installer/src/executors/file-prompt.ts @@ -0,0 +1,36 @@ +import { prompt as enquirer } from 'enquirer' +import globby from 'globby' + +enum SearchType { + file, + directory, +} + +interface FilePromptOptions { + globFilter?: string + getChoices?(context: any): string[] + searchType?: SearchType + context: any +} + +function getMatchingFiles(filter: string = ''): Promise<string[]> { + return globby(filter, { expandDirectories: true }) +} + +export async function filePrompt(options: FilePromptOptions): Promise<string> { + const choices = options.getChoices + ? options.getChoices(options.context) + : await getMatchingFiles(options.globFilter) + if (choices.length === 1) { + return choices[0] + } + const results: { file: string } = await enquirer({ + type: 'autocomplete', + name: 'file', + message: 'Select the target file', + // @ts-ignore + limit: 10, + choices, + }) + return results.file +} diff --git a/nextjs/packages/installer/src/executors/file-transform-executor.tsx b/nextjs/packages/installer/src/executors/file-transform-executor.tsx new file mode 100644 index 0000000000..8ac9af1ae9 --- /dev/null +++ b/nextjs/packages/installer/src/executors/file-transform-executor.tsx @@ -0,0 +1,199 @@ +import { createPatch } from 'diff' +import * as fs from 'fs-extra' +import { Box, Text } from 'ink' +import Spinner from 'ink-spinner' +import * as React from 'react' +import { EnterToContinue } from '../components/enter-to-continue' +import { RecipeCLIArgs } from '../types' +import { + processFile, + stringProcessFile, + StringTransformer, + transform, + Transformer, + TransformStatus, +} from '../utils/transform' +import { useEnterToContinue } from '../utils/use-enter-to-continue' +import { useUserInput } from '../utils/use-user-input' +import { + Executor, + executorArgument, + ExecutorConfig, + getExecutorArgument, +} from './executor' +import { filePrompt } from './file-prompt' + +export interface Config extends ExecutorConfig { + selectTargetFiles?(cliArgs: RecipeCLIArgs): any[] + singleFileSearch?: executorArgument<string> + transform?: Transformer + transformPlain?: StringTransformer +} + +export function isFileTransformExecutor( + executor: ExecutorConfig +): executor is Config { + return ( + (executor as Config).transform !== undefined || + (executor as Config).transformPlain !== undefined + ) +} + +export const type = 'file-transform' +export const Propose: Executor['Propose'] = ({ + cliArgs, + cliFlags, + onProposalAccepted, + step, +}) => { + const userInput = useUserInput(cliFlags) + const [diff, setDiff] = React.useState<string | null>(null) + const [error, setError] = React.useState<Error | null>(null) + const [filePath, setFilePath] = React.useState('') + const [proposalAccepted, setProposalAccepted] = React.useState(false) + + const acceptProposal = React.useCallback(() => { + setProposalAccepted(true) + onProposalAccepted(filePath) + }, [onProposalAccepted, filePath]) + + React.useEffect(() => { + async function generateDiff() { + const fileToTransform: string = await filePrompt({ + context: cliArgs, + globFilter: getExecutorArgument( + (step as Config).singleFileSearch, + cliArgs + ), + getChoices: (step as Config).selectTargetFiles, + }) + setFilePath(fileToTransform) + const originalFile = fs.readFileSync(fileToTransform).toString('utf-8') + const newFile = await ((step as Config).transformPlain + ? stringProcessFile(originalFile, (step as Config).transformPlain!) + : processFile(originalFile, (step as Config).transform!)) + return createPatch(fileToTransform, originalFile, newFile) + } + + generateDiff().then(setDiff, setError) + }, [cliArgs, step]) + + // Let the renderer deal with errors from file transformers, otherwise the + // process would just hang. + if (error) throw error + + if (!diff) { + return ( + <Box> + <Text> + <Spinner /> + Generating file diff... + </Text> + </Box> + ) + } + + const childProps: ProposeChildProps = { + diff, + filePath, + proposalAccepted, + acceptProposal, + } + + if (userInput) return <ProposeWithInput {...childProps} /> + else return <ProposeWithoutInput {...childProps} /> +} + +interface ProposeChildProps { + diff: string + filePath: string + proposalAccepted: boolean + acceptProposal: () => void +} + +const Diff = ({ diff }: { diff: string }) => ( + <> + {diff + .split('\n') + .slice(2) + .map((line, idx) => { + let styleProps: any = {} + if (line.startsWith('-') && !line.startsWith('---')) { + styleProps.bold = true + styleProps.color = 'red' + } else if (line.startsWith('+') && !line.startsWith('+++')) { + styleProps.bold = true + styleProps.color = 'green' + } + return ( + <Text {...styleProps} key={idx}> + {line} + </Text> + ) + })} + </> +) + +const ProposeWithInput = ({ + diff, + filePath, + proposalAccepted, + acceptProposal, +}: ProposeChildProps) => { + useEnterToContinue(acceptProposal, filePath !== '' && !proposalAccepted) + + return ( + <Box flexDirection="column"> + <Diff diff={diff} /> + <EnterToContinue message="The above changes will be made. Press ENTER to continue" /> + </Box> + ) +} + +const ProposeWithoutInput = ({ + diff, + filePath, + proposalAccepted, + acceptProposal, +}: ProposeChildProps) => { + React.useEffect(() => { + if (filePath !== '' && !proposalAccepted) { + acceptProposal() + } + }, [acceptProposal, filePath, proposalAccepted]) + + return ( + <Box flexDirection="column"> + <Diff diff={diff} /> + </Box> + ) +} + +export const Commit: Executor['Commit'] = ({ + onChangeCommitted, + proposalData: filePath, + step, +}) => { + React.useEffect(() => { + void (async function () { + const results = await transform( + async (original) => + await ((step as Config).transformPlain + ? stringProcessFile(original, (step as Config).transformPlain!) + : processFile(original, (step as Config).transform!)), + [filePath] + ) + if (results.some((r) => r.status === TransformStatus.Failure)) { + console.error(results) + } + onChangeCommitted(`Modified file: ${filePath}`) + })() + }, [filePath, onChangeCommitted, step]) + + return ( + <Box> + <Spinner /> + <Text>Applying file changes</Text> + </Box> + ) +} diff --git a/nextjs/packages/installer/src/executors/new-file-executor.tsx b/nextjs/packages/installer/src/executors/new-file-executor.tsx new file mode 100644 index 0000000000..c4f63221f6 --- /dev/null +++ b/nextjs/packages/installer/src/executors/new-file-executor.tsx @@ -0,0 +1,158 @@ +import { Generator, GeneratorOptions, SourceRootType } from '@blitzjs/generator' +import { Box, Text } from 'ink' +import { useEffect, useState } from 'react' +import * as React from 'react' +import { EnterToContinue } from '../components/enter-to-continue' +import { useEnterToContinue } from '../utils/use-enter-to-continue' +import { useUserInput } from '../utils/use-user-input' +import { + Executor, + executorArgument, + ExecutorConfig, + getExecutorArgument, +} from './executor' + +export interface Config extends ExecutorConfig { + targetDirectory?: executorArgument<string> + templatePath: executorArgument<string> + templateValues: executorArgument<{ [key: string]: string }> + destinationPathPrompt?: executorArgument<string> +} + +export function isNewFileExecutor( + executor: ExecutorConfig +): executor is Config { + return (executor as Config).templatePath !== undefined +} + +export const type = 'new-file' + +interface TempGeneratorOptions extends GeneratorOptions { + targetDirectory?: string + templateRoot: string + templateValues: any +} + +class TempGenerator extends Generator<TempGeneratorOptions> { + sourceRoot: SourceRootType + targetDirectory: string + templateValues: any + returnResults = true + + constructor(options: TempGeneratorOptions) { + super(options) + this.sourceRoot = { type: 'absolute', path: options.templateRoot } + this.templateValues = options.templateValues + this.targetDirectory = options.targetDirectory || '.' + } + + getTemplateValues() { + return this.templateValues + } + + getTargetDirectory() { + return this.targetDirectory + } +} + +export const Commit: Executor['Commit'] = ({ + cliArgs, + cliFlags, + onChangeCommitted, + step, +}) => { + const userInput = useUserInput(cliFlags) + const generatorArgs = React.useMemo( + () => ({ + destinationRoot: '.', + targetDirectory: getExecutorArgument( + (step as Config).targetDirectory, + cliArgs + ), + templateRoot: getExecutorArgument((step as Config).templatePath, cliArgs), + templateValues: getExecutorArgument( + (step as Config).templateValues, + cliArgs + ), + }), + [cliArgs, step] + ) + const [fileCreateOutput, setFileCreateOutput] = useState('') + const [changeCommited, setChangeCommited] = useState(false) + const fileCreateLines = fileCreateOutput.split('\n') + const handleChangeCommitted = React.useCallback(() => { + setChangeCommited(true) + onChangeCommitted( + `Successfully created ${fileCreateLines + .map((l) => l.split(' ').slice(1).join('').trim()) + .join(', ')}` + ) + }, [fileCreateLines, onChangeCommitted]) + + useEffect(() => { + async function createNewFiles() { + if (!fileCreateOutput) { + const generator = new TempGenerator(generatorArgs) + const results = ((await generator.run()) as unknown) as string + setFileCreateOutput(results) + } + } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + createNewFiles() + }, [fileCreateOutput, generatorArgs]) + + const childProps: CommitChildProps = { + changeCommited, + fileCreateOutput, + handleChangeCommitted, + } + + if (userInput) return <CommitWithInput {...childProps} /> + else return <CommitWithoutInput {...childProps} /> +} + +interface CommitChildProps { + changeCommited: boolean + fileCreateOutput: string + handleChangeCommitted: () => void +} + +const CommitWithInput = ({ + changeCommited, + fileCreateOutput, + handleChangeCommitted, +}: CommitChildProps) => { + useEnterToContinue( + handleChangeCommitted, + !changeCommited && fileCreateOutput !== '' + ) + + return ( + <Box flexDirection="column"> + {fileCreateOutput !== '' && ( + <> + <Text>{fileCreateOutput}</Text> + <EnterToContinue /> + </> + )} + </Box> + ) +} + +const CommitWithoutInput = ({ + changeCommited, + fileCreateOutput, + handleChangeCommitted, +}: CommitChildProps) => { + React.useEffect(() => { + if (!changeCommited && fileCreateOutput !== '') { + handleChangeCommitted() + } + }, [changeCommited, fileCreateOutput, handleChangeCommitted]) + + return ( + <Box flexDirection="column"> + {fileCreateOutput !== '' && <Text>{fileCreateOutput}</Text>} + </Box> + ) +} diff --git a/nextjs/packages/installer/src/executors/print-message-executor.tsx b/nextjs/packages/installer/src/executors/print-message-executor.tsx new file mode 100644 index 0000000000..641424e604 --- /dev/null +++ b/nextjs/packages/installer/src/executors/print-message-executor.tsx @@ -0,0 +1,87 @@ +import { Box, Text } from 'ink' +import * as React from 'react' +import { EnterToContinue } from '../components/enter-to-continue' +import { useEnterToContinue } from '../utils/use-enter-to-continue' +import { useUserInput } from '../utils/use-user-input' +import { + Executor, + executorArgument, + ExecutorConfig, + getExecutorArgument, +} from './executor' + +export interface Config extends ExecutorConfig { + message: executorArgument<string> +} + +export const type = 'print-message' + +export const Commit: Executor['Commit'] = ({ + cliArgs, + cliFlags, + onChangeCommitted, + step, +}) => { + const userInput = useUserInput(cliFlags) + const generatorArgs = React.useMemo( + () => ({ + message: getExecutorArgument((step as Config).message, cliArgs), + stepName: getExecutorArgument((step as Config).stepName, cliArgs), + }), + [cliArgs, step] + ) + const [changeCommited, setChangeCommited] = React.useState(false) + + const handleChangeCommitted = React.useCallback(() => { + setChangeCommited(true) + onChangeCommitted(generatorArgs.stepName) + }, [onChangeCommitted, generatorArgs]) + + const childProps: CommitChildProps = { + changeCommited, + generatorArgs, + handleChangeCommitted, + } + + if (userInput) return <CommitWithInput {...childProps} /> + else return <CommitWithoutInput {...childProps} /> +} + +interface CommitChildProps { + changeCommited: boolean + generatorArgs: { message: string; stepName: string } + handleChangeCommitted: () => void +} + +const CommitWithInput = ({ + changeCommited, + generatorArgs, + handleChangeCommitted, +}: CommitChildProps) => { + useEnterToContinue(handleChangeCommitted, !changeCommited) + + return ( + <Box flexDirection="column"> + <Text>{generatorArgs.message}</Text> + <EnterToContinue /> + </Box> + ) +} + +const CommitWithoutInput = ({ + changeCommited, + generatorArgs, + handleChangeCommitted, +}: CommitChildProps) => { + React.useEffect(() => { + if (!changeCommited) { + handleChangeCommitted() + } + }, [changeCommited, handleChangeCommitted]) + + return ( + <Box flexDirection="column"> + <Text>{generatorArgs.message}</Text> + </Box> + ) +} diff --git a/nextjs/packages/installer/src/executors/run-command-executor.tsx b/nextjs/packages/installer/src/executors/run-command-executor.tsx new file mode 100644 index 0000000000..ae377b6084 --- /dev/null +++ b/nextjs/packages/installer/src/executors/run-command-executor.tsx @@ -0,0 +1,161 @@ +import { spawn } from 'cross-spawn' +import { Box, Text } from 'ink' +import Spinner from 'ink-spinner' +import * as React from 'react' +import { Newline } from '../components/newline' +import { RecipeCLIArgs } from '../types' +import { useEnterToContinue } from '../utils/use-enter-to-continue' +import { useUserInput } from '../utils/use-user-input' +import { Executor, ExecutorConfig, getExecutorArgument } from './executor' + +export type CliCommand = string | [string, ...string[]] + +export interface Config extends ExecutorConfig { + command: CliCommand +} +export interface CommitChildProps { + commandInstalled: boolean + handleChangeCommitted: () => void + command: CliCommand + cliArgs: RecipeCLIArgs + step: Config +} + +export const type = 'run-command' + +function Command({ + command, + loading, +}: { + command: CliCommand + loading: boolean +}) { + return ( + <Text> + {` `} + {loading ? <Spinner /> : '✅'} + {` ${typeof command === 'string' ? command : command.join(' ')}`} + </Text> + ) +} + +const CommandList = ({ + lede = 'Hang tight! Running...', + commandLoading = false, + step, + command, +}: { + lede?: string + commandLoading?: boolean + step: Config + command: CliCommand +}) => { + return ( + <Box flexDirection="column"> + <Text>{lede}</Text> + <Newline /> + <Command key={step.stepId} command={command} loading={commandLoading} /> + </Box> + ) +} + +/** + * INFO: Exported for unit testing purposes + * + * This function calls the defined command with their optional arguments if defined + * + * @param {CliCommand} input The Command and arguments + * @return Promise<void> + * + * @example await executeCommand("ls") + * @example await executeCommand(["ls"]) + * @example await executeCommand(["ls", ...["-a", "-l"]]) + */ +export async function executeCommand(input: CliCommand): Promise<void> { + // from https://stackoverflow.com/a/43766456/9950655 + const argsRegex = /("[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'|\/[^/\\]*(?:\\[\S\s][^/\\]*)*\/[gimy]*(?=\s|$)|(?:\\\s|\S)+)/g + const command: string[] = Array.isArray(input) + ? input + : input.match(argsRegex) || [] + + if (command.length === 0) { + throw new Error(`The command is too short: \`${JSON.stringify(input)}\``) + } + + await new Promise((resolve) => { + const cp = spawn(command[0], command.slice(1), { + stdio: ['inherit', 'pipe', 'pipe'], + }) + cp.on('exit', resolve) + }) +} + +export const Commit: Executor['Commit'] = ({ + cliArgs, + cliFlags, + step, + onChangeCommitted, +}) => { + const userInput = useUserInput(cliFlags) + const [commandInstalled, setCommandInstalled] = React.useState(false) + const executorCommand = getExecutorArgument((step as Config).command, cliArgs) + + const handleChangeCommitted = React.useCallback(() => { + onChangeCommitted(`Executed command ${executorCommand}`) + }, [executorCommand, onChangeCommitted]) + + React.useEffect(() => { + async function runCommand() { + await executeCommand(executorCommand) + setCommandInstalled(true) + } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + runCommand() + }, [cliArgs, step, executorCommand]) + + React.useEffect(() => { + if (commandInstalled) { + handleChangeCommitted() + } + }, [commandInstalled, handleChangeCommitted]) + + const childProps: CommitChildProps = { + commandInstalled, + handleChangeCommitted, + command: executorCommand, + cliArgs, + step: step as Config, + } + + if (userInput) return <CommitWithInput {...childProps} /> + else return <CommitWithoutInput {...childProps} /> +} + +const CommitWithInput = ({ + commandInstalled, + handleChangeCommitted, + command, + step, +}: CommitChildProps) => { + useEnterToContinue(handleChangeCommitted, commandInstalled) + + return ( + <CommandList + commandLoading={!commandInstalled} + step={step} + command={command} + /> + ) +} + +const CommitWithoutInput = ({ + commandInstalled, + command, + step, +}: CommitChildProps) => ( + <CommandList + commandLoading={!commandInstalled} + step={step} + command={command} + /> +) diff --git a/nextjs/packages/installer/src/index.ts b/nextjs/packages/installer/src/index.ts new file mode 100644 index 0000000000..9789eca254 --- /dev/null +++ b/nextjs/packages/installer/src/index.ts @@ -0,0 +1,12 @@ +export * from './recipe-executor' +export * from './recipe-builder' +export * from './executors/executor' +export { type as AddDependencyType } from './executors/add-dependency-executor' +export { type as FileTransformType } from './executors/file-transform-executor' +export { type as NewFileType } from './executors/new-file-executor' +export { type as PrintMessageType } from './executors/print-message-executor' + +export * from './utils/paths' +export * from './transforms' +export { customTsParser } from './utils/transform' +export type { Program, RecipeCLIArgs, RecipeCLIFlags } from './types' diff --git a/nextjs/packages/installer/src/recipe-builder.ts b/nextjs/packages/installer/src/recipe-builder.ts new file mode 100644 index 0000000000..d4a30f3c74 --- /dev/null +++ b/nextjs/packages/installer/src/recipe-builder.ts @@ -0,0 +1,97 @@ +import * as AddDependencyExecutor from './executors/add-dependency-executor' +import * as TransformFileExecutor from './executors/file-transform-executor' +import * as NewFileExecutor from './executors/new-file-executor' +import * as PrintMessageExecutor from './executors/print-message-executor' +import * as RunCommandExecutor from './executors/run-command-executor' +import { ExecutorConfigUnion, RecipeExecutor } from './recipe-executor' +import { RecipeMeta } from './types' + +export interface IRecipeBuilder { + setName(name: string): IRecipeBuilder + setDescription(description: string): IRecipeBuilder + printMessage( + step: Omit<Omit<PrintMessageExecutor.Config, 'stepType'>, 'explanation'> + ): IRecipeBuilder + setOwner(owner: string): IRecipeBuilder + setRepoLink(repoLink: string): IRecipeBuilder + addAddDependenciesStep( + step: Omit<AddDependencyExecutor.Config, 'stepType'> + ): IRecipeBuilder + addNewFilesStep( + step: Omit<NewFileExecutor.Config, 'stepType'> + ): IRecipeBuilder + addTransformFilesStep( + step: Omit<TransformFileExecutor.Config, 'stepType'> + ): IRecipeBuilder + addRunCommandStep( + step: Omit<RunCommandExecutor.Config, 'stepType'> + ): IRecipeBuilder + + build(): RecipeExecutor<any> +} + +export function RecipeBuilder(): IRecipeBuilder { + const steps: ExecutorConfigUnion[] = [] + const meta: Partial<RecipeMeta> = {} + + return { + setName(name: string) { + meta.name = name + return this + }, + setDescription(description: string) { + meta.description = description + return this + }, + printMessage(step: Omit<PrintMessageExecutor.Config, 'stepType'>) { + steps.push({ + stepType: PrintMessageExecutor.type, + ...step, + }) + return this + }, + setOwner(owner: string) { + meta.owner = owner + return this + }, + setRepoLink(repoLink: string) { + meta.repoLink = repoLink + return this + }, + addAddDependenciesStep( + step: Omit<AddDependencyExecutor.Config, 'stepType'> + ) { + steps.push({ + stepType: AddDependencyExecutor.type, + ...step, + }) + return this + }, + addNewFilesStep(step: Omit<NewFileExecutor.Config, 'stepType'>) { + steps.push({ + stepType: NewFileExecutor.type, + ...step, + }) + return this + }, + addTransformFilesStep( + step: Omit<TransformFileExecutor.Config, 'stepType'> + ) { + steps.push({ + stepType: TransformFileExecutor.type, + ...step, + }) + return this + }, + addRunCommandStep(step: Omit<RunCommandExecutor.Config, 'stepType'>) { + steps.push({ + stepType: RunCommandExecutor.type, + ...step, + }) + return this + }, + build() { + return new RecipeExecutor(meta as RecipeMeta, steps) + }, + } +} diff --git a/nextjs/packages/installer/src/recipe-executor.tsx b/nextjs/packages/installer/src/recipe-executor.tsx new file mode 100644 index 0000000000..16d3e0dbb6 --- /dev/null +++ b/nextjs/packages/installer/src/recipe-executor.tsx @@ -0,0 +1,52 @@ +import { render } from 'ink' +import { baseLogger } from 'next/dist/server/lib/logging' +import React from 'react' +import * as AddDependencyExecutor from './executors/add-dependency-executor' +import * as FileTransformExecutor from './executors/file-transform-executor' +import * as NewFileExecutor from './executors/new-file-executor' +import * as PrintMessageExecutor from './executors/print-message-executor' +import { RecipeRenderer } from './recipe-renderer' +import { RecipeCLIArgs, RecipeCLIFlags, RecipeMeta } from './types' +// const debug = require('debug')("blitz:installer") + +type ExecutorConfig = + | AddDependencyExecutor.Config + | FileTransformExecutor.Config + | NewFileExecutor.Config + | PrintMessageExecutor.Config + +export type { ExecutorConfig as ExecutorConfigUnion } + +export class RecipeExecutor<Options extends RecipeMeta> { + private readonly steps: ExecutorConfig[] + private readonly options: Options + + constructor(options: Options, steps: ExecutorConfig[]) { + this.options = options + this.steps = steps + } + + async run( + cliArgs: RecipeCLIArgs = {}, + cliFlags: RecipeCLIFlags = { yesToAll: false } + ): Promise<void> { + try { + const { waitUntilExit } = render( + <RecipeRenderer + cliArgs={cliArgs} + cliFlags={cliFlags} + steps={this.steps} + recipeMeta={this.options} + />, + { exitOnCtrlC: false } + ) + await waitUntilExit() + baseLogger({ displayDateTime: false, displayLogLevel: false }).info( + `\n🎉 The ${this.options.name} recipe has been installed!\n` + ) + } catch (e) { + baseLogger({ displayDateTime: false }).error(e as any) + return + } + } +} diff --git a/nextjs/packages/installer/src/recipe-renderer.tsx b/nextjs/packages/installer/src/recipe-renderer.tsx new file mode 100644 index 0000000000..a201dc4ebf --- /dev/null +++ b/nextjs/packages/installer/src/recipe-renderer.tsx @@ -0,0 +1,308 @@ +import { Box, Text, useApp, useInput } from 'ink' +import React from 'react' +import { EnterToContinue } from './components/enter-to-continue' +import { Newline } from './components/newline' +import * as AddDependencyExecutor from './executors/add-dependency-executor' +import { Executor, ExecutorConfig, Frontmatter } from './executors/executor' +import * as FileTransformExecutor from './executors/file-transform-executor' +import * as NewFileExecutor from './executors/new-file-executor' +import * as PrintMessageExecutor from './executors/print-message-executor' +import * as RunCommandExecutor from './executors/run-command-executor' +import { RecipeCLIArgs, RecipeCLIFlags, RecipeMeta } from './types' +import { useEnterToContinue } from './utils/use-enter-to-continue' +import { useUserInput } from './utils/use-user-input' + +enum Action { + SkipStep, + ProposeChange, + ApplyChange, + CommitApproved, + CompleteChange, +} + +enum Status { + Pending, + Proposed, + ReadyToCommit, + Committing, + Committed, +} + +const ExecutorMap: { [key: string]: Executor } = { + [AddDependencyExecutor.type]: AddDependencyExecutor, + [NewFileExecutor.type]: NewFileExecutor, + [PrintMessageExecutor.type]: PrintMessageExecutor, + [FileTransformExecutor.type]: FileTransformExecutor, + [RunCommandExecutor.type]: RunCommandExecutor, +} as const + +interface State { + steps: { + executor: ExecutorConfig + status: Status + proposalData?: any + successMsg: string + }[] + current: number +} + +function recipeReducer(state: State, action: { type: Action; data?: any }) { + const newState = { ...state } + switch (action.type) { + case Action.ProposeChange: + newState.steps[newState.current].status = Status.Proposed + break + case Action.CommitApproved: + newState.steps[newState.current].status = Status.ReadyToCommit + newState.steps[newState.current].proposalData = action.data + break + case Action.ApplyChange: + newState.steps[newState.current].status = Status.Committing + break + case Action.CompleteChange: + newState.steps[newState.current].status = Status.Committed + newState.steps[newState.current].successMsg = action.data as string + newState.current = Math.min( + newState.current + 1, + newState.steps.length - 1 + ) + break + case Action.SkipStep: + newState.current += 1 + break + } + return newState +} + +interface RecipeProps { + cliArgs: RecipeCLIArgs + cliFlags: RecipeCLIFlags + steps: ExecutorConfig[] + recipeMeta: RecipeMeta +} + +const DispatchContext = React.createContext< + React.Dispatch<{ type: Action; data?: any }> +>(() => {}) + +function WelcomeMessage({ + recipeMeta, + enterToContinue = true, +}: { + recipeMeta: RecipeMeta + enterToContinue?: boolean +}) { + return ( + <Box flexDirection="column"> + <Text color="#8a3df0" bold> + Recipe: {recipeMeta.name} + </Text> + <Newline /> + <Text color="gray"> + <Text italic>{recipeMeta.description}</Text> + </Text> + <Newline /> + <Text color="gray"> + Repo: <Text italic>{recipeMeta.repoLink}</Text> + </Text> + <Text color="gray"> + Author: <Text italic>{recipeMeta.owner}</Text> + </Text> + {enterToContinue && <EnterToContinue />} + </Box> + ) +} + +function StepMessages({ state }: { state: State }) { + const messages = state.steps + .map((step) => ({ + msg: step.successMsg, + icon: step.executor.successIcon ?? '✅', + })) + .filter((s) => s.msg) + + return ( + <> + {messages.map(({ msg, icon }, index) => ( + <Text key={msg + index} color="green"> + {msg === '\n' ? '' : icon} {msg} + </Text> + ))} + </> + ) +} + +function StepExecutor({ + cliArgs, + cliFlags, + proposalData, + step, + status, +}: { + step: ExecutorConfig + status: Status + cliArgs: RecipeCLIArgs + cliFlags: RecipeCLIFlags + proposalData?: any +}) { + const { Propose, Commit }: Executor = ExecutorMap[step.stepType] + const dispatch = React.useContext(DispatchContext) + + const handleProposalAccepted = React.useCallback( + (msg) => { + dispatch({ type: Action.CommitApproved, data: msg }) + }, + [dispatch] + ) + const handleChangeCommitted = React.useCallback( + (msg) => { + dispatch({ type: Action.CompleteChange, data: msg }) + }, + [dispatch] + ) + + React.useEffect(() => { + if (status === Status.Pending) { + dispatch({ type: Action.ProposeChange }) + } else if (status === Status.ReadyToCommit) { + dispatch({ type: Action.ApplyChange }) + } + if (status === Status.Proposed && !Propose) { + dispatch({ type: Action.CommitApproved }) + } + }, [dispatch, status, Propose]) + + return ( + <Box flexDirection="column"> + {status !== Status.Committed ? <Frontmatter executor={step} /> : null} + {[Status.Proposed].includes(status) && Propose ? ( + <Propose + cliArgs={cliArgs} + cliFlags={cliFlags} + step={step} + onProposalAccepted={handleProposalAccepted} + /> + ) : null} + {[Status.Committing].includes(status) ? ( + <Commit + cliArgs={cliArgs} + cliFlags={cliFlags} + proposalData={proposalData} + step={step} + onChangeCommitted={handleChangeCommitted} + /> + ) : null} + </Box> + ) +} + +export function RecipeRenderer({ + cliArgs, + cliFlags, + steps, + recipeMeta, +}: RecipeProps) { + const userInput = useUserInput(cliFlags) + const { exit } = useApp() + const [state, dispatch] = React.useReducer(recipeReducer, { + current: userInput ? -1 : 0, + steps: steps.map((e) => ({ + executor: e, + status: Status.Pending, + successMsg: '', + })), + }) + + if (steps.length === 0) { + exit(new Error('This recipe has no steps')) + } + + React.useEffect(() => { + if ( + state.current === state.steps.length - 1 && + state.steps[state.current]?.status === Status.Committed + ) { + exit() + } + }) + + return ( + <DispatchContext.Provider value={dispatch}> + {userInput ? ( + <RecipeRendererWithInput + cliArgs={cliArgs} + cliFlags={cliFlags} + state={state} + recipeMeta={recipeMeta} + /> + ) : ( + <RecipeRendererWithoutInput + cliArgs={cliArgs} + cliFlags={cliFlags} + state={state} + recipeMeta={recipeMeta} + /> + )} + </DispatchContext.Provider> + ) +} + +function RecipeRendererWithInput({ + cliArgs, + cliFlags, + recipeMeta, + state, +}: Omit<RecipeProps, 'steps'> & { state: State }) { + const { exit } = useApp() + const dispatch = React.useContext(DispatchContext) + + useInput((input, key) => { + if (input === 'c' && key.ctrl) { + exit(new Error('You aborted installation')) + return + } + }) + + useEnterToContinue( + () => dispatch({ type: Action.SkipStep }), + state.current === -1 + ) + + return ( + <> + <StepMessages state={state} /> + {state.current === -1 ? ( + <WelcomeMessage recipeMeta={recipeMeta} /> + ) : ( + <StepExecutor + cliArgs={cliArgs} + cliFlags={cliFlags} + proposalData={state.steps[state.current]?.proposalData} + step={state.steps[state.current]?.executor} + status={state.steps[state.current]?.status} + /> + )} + </> + ) +} + +function RecipeRendererWithoutInput({ + cliArgs, + cliFlags, + recipeMeta, + state, +}: Omit<RecipeProps, 'steps'> & { state: State }) { + return ( + <> + <WelcomeMessage recipeMeta={recipeMeta} enterToContinue={false} /> + <StepMessages state={state} /> + <StepExecutor + cliArgs={cliArgs} + cliFlags={cliFlags} + proposalData={state.steps[state.current]?.proposalData} + step={state.steps[state.current]?.executor} + status={state.steps[state.current]?.status} + /> + </> + ) +} diff --git a/nextjs/packages/installer/src/transforms/add-blitz-middleware.ts b/nextjs/packages/installer/src/transforms/add-blitz-middleware.ts new file mode 100644 index 0000000000..7b1aed0044 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/add-blitz-middleware.ts @@ -0,0 +1,35 @@ +import type { ExpressionKind } from 'ast-types/gen/kinds' +import j from 'jscodeshift' +import { Program } from '../types' +import { transformBlitzConfig } from '.' + +export const addBlitzMiddleware = ( + program: Program, + middleware: ExpressionKind +): Program => + transformBlitzConfig(program, (config) => { + // Locate the middleware property + const middlewareProp = config.properties.find( + (value) => + value.type === 'ObjectProperty' && + value.key.type === 'Identifier' && + value.key.name === 'middleware' + ) as j.ObjectProperty | undefined + + if (middlewareProp && middlewareProp.value.type === 'ArrayExpression') { + // We found it, pop on our middleware. + middlewareProp.value.elements.push(middleware) + } else { + // No middleware prop, add our own. + config.properties.push( + j.property('init', j.identifier('middleware'), { + type: 'ArrayExpression', + elements: [middleware], + loc: null, + comments: null, + }) + ) + } + + return config + }) diff --git a/nextjs/packages/installer/src/transforms/add-import.ts b/nextjs/packages/installer/src/transforms/add-import.ts new file mode 100644 index 0000000000..c1a60d74cd --- /dev/null +++ b/nextjs/packages/installer/src/transforms/add-import.ts @@ -0,0 +1,19 @@ +import j from 'jscodeshift' +import { Program } from '../types' + +export function addImport( + program: Program, + importToAdd: j.ImportDeclaration +): Program { + const importStatementCount = program.find(j.ImportDeclaration).length + if (importStatementCount === 0) { + program.find(j.Statement).at(0).insertBefore(importToAdd) + return program + } + program.find(j.ImportDeclaration).forEach((stmt, idx) => { + if (idx === importStatementCount - 1) { + stmt.replace(stmt.node, importToAdd) + } + }) + return program +} diff --git a/nextjs/packages/installer/src/transforms/find-module-exports-expressions.ts b/nextjs/packages/installer/src/transforms/find-module-exports-expressions.ts new file mode 100644 index 0000000000..0da29c9f04 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/find-module-exports-expressions.ts @@ -0,0 +1,14 @@ +import j from 'jscodeshift' +import { Program } from '../types' + +export const findModuleExportsExpressions = (program: Program) => + program.find(j.AssignmentExpression).filter((path) => { + const { left, right } = path.value + return ( + left.type === 'MemberExpression' && + left.object.type === 'Identifier' && + left.property.type === 'Identifier' && + left.property.name === 'exports' && + right.type === 'ObjectExpression' + ) + }) diff --git a/nextjs/packages/installer/src/transforms/index.ts b/nextjs/packages/installer/src/transforms/index.ts new file mode 100644 index 0000000000..73906b0bbd --- /dev/null +++ b/nextjs/packages/installer/src/transforms/index.ts @@ -0,0 +1,8 @@ +export * from './add-import' +export * from './add-blitz-middleware' +export * from './find-module-exports-expressions' +export * from './prisma' +export * from './transform-blitz-config' +export * from './update-babel-config' +export * from './with-utilities' +export * from './wrap-blitz-config' diff --git a/nextjs/packages/installer/src/transforms/prisma/add-prisma-enum.ts b/nextjs/packages/installer/src/transforms/prisma/add-prisma-enum.ts new file mode 100644 index 0000000000..a4b9425a89 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/prisma/add-prisma-enum.ts @@ -0,0 +1,32 @@ +import { Enum } from '@mrleebo/prisma-ast' +import { produceSchema } from './produce-schema' + +/** + * Adds an enum to your schema.prisma data model. + * + * @param source - schema.prisma source file contents + * @param enumProps - the enum to add + * @returns The modified schema.prisma source + * @example Usage + * ``` + * addPrismaEnum(source, { + type: "enum", + name: "Role", + enumerators: [ + {type: "enumerator", name: "USER"}, + {type: "enumerator", name: "ADMIN"}, + ], + }) + * ``` + */ +export function addPrismaEnum( + source: string, + enumProps: Enum +): Promise<string> { + return produceSchema(source, (schema) => { + const existing = schema.list.find( + (x) => x.type === 'enum' && x.name === enumProps.name + ) + existing ? Object.assign(existing, enumProps) : schema.list.push(enumProps) + }) +} diff --git a/nextjs/packages/installer/src/transforms/prisma/add-prisma-field.ts b/nextjs/packages/installer/src/transforms/prisma/add-prisma-field.ts new file mode 100644 index 0000000000..a13e36ad28 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/prisma/add-prisma-field.ts @@ -0,0 +1,40 @@ +import { Field, Model } from '@mrleebo/prisma-ast' +import { produceSchema } from './produce-schema' + +/** + * Adds a field to a model in your schema.prisma data model. + * + * @param source - schema.prisma source file contents + * @param modelName - name of the model to add a field to + * @param fieldProps - the field to add + * @returns The modified schema.prisma source + * @example Usage + * ``` + * addPrismaField(source, "Project", { + type: "field", + name: "name", + fieldType: "String", + optional: false, + attributes: [{type: "attribute", kind: "field", name: "unique"}], + }) + * ``` + */ +export function addPrismaField( + source: string, + modelName: string, + fieldProps: Field +): Promise<string> { + return produceSchema(source, (schema) => { + const model = schema.list.find( + (x) => x.type === 'model' && x.name === modelName + ) as Model + if (!model) return + + const existing = model.properties.find( + (x) => x.type === 'field' && x.name === fieldProps.name + ) + existing + ? Object.assign(existing, fieldProps) + : model.properties.push(fieldProps) + }) +} diff --git a/nextjs/packages/installer/src/transforms/prisma/add-prisma-generator.ts b/nextjs/packages/installer/src/transforms/prisma/add-prisma-generator.ts new file mode 100644 index 0000000000..a5e269150d --- /dev/null +++ b/nextjs/packages/installer/src/transforms/prisma/add-prisma-generator.ts @@ -0,0 +1,31 @@ +import { Generator } from '@mrleebo/prisma-ast' +import { produceSchema } from './produce-schema' + +/** + * Adds a generator to your schema.prisma data model. + * + * @param source - schema.prisma source file contents + * @param generatorProps - the generator to add + * @returns The modified schema.prisma source + * @example Usage + * ``` + * addPrismaGenerator(source, { + type: "generator", + name: "nexusPrisma", + assignments: [{type: "assignment", key: "provider", value: '"nexus-prisma"'}], + }) + * ``` + */ +export function addPrismaGenerator( + source: string, + generatorProps: Generator +): Promise<string> { + return produceSchema(source, (schema) => { + const existing = schema.list.find( + (x) => x.type === 'generator' && x.name === generatorProps.name + ) as Generator + existing + ? Object.assign(existing, generatorProps) + : schema.list.push(generatorProps) + }) +} diff --git a/nextjs/packages/installer/src/transforms/prisma/add-prisma-model-attribute.ts b/nextjs/packages/installer/src/transforms/prisma/add-prisma-model-attribute.ts new file mode 100644 index 0000000000..d0824e2d6e --- /dev/null +++ b/nextjs/packages/installer/src/transforms/prisma/add-prisma-model-attribute.ts @@ -0,0 +1,41 @@ +import { Model, ModelAttribute } from '@mrleebo/prisma-ast' +import { produceSchema } from './produce-schema' + +/** + * Adds a field to a model in your schema.prisma data model. + * + * @remarks Not ready for actual use + * @param source - schema.prisma source file contents + * @param modelName - name of the model to add a field to + * @param attributeProps - the model attribute (such as an index) to add + * @returns The modified schema.prisma source + * @example Usage + * ``` + * addPrismaModelAttribute(source, "Project", { + * type: "attribute", + * kind: "model", + * name: "index", + * args: [{ type: "attributeArgument", value: { type: "array", args: ["name"] } }] + * }); + * ``` + */ +export function addPrismaModelAttribute( + source: string, + modelName: string, + attributeProps: ModelAttribute +): Promise<string> { + return produceSchema(source, (schema) => { + const model = schema.list.find( + (x) => x.type === 'model' && x.name === modelName + ) as Model + if (!model) return + + const existing = model.properties.find( + (x) => x.type === 'attribute' && x.name === attributeProps.name + ) + + existing + ? Object.assign(existing, attributeProps) + : model.properties.push(attributeProps) + }) +} diff --git a/nextjs/packages/installer/src/transforms/prisma/add-prisma-model.ts b/nextjs/packages/installer/src/transforms/prisma/add-prisma-model.ts new file mode 100644 index 0000000000..e749a6db56 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/prisma/add-prisma-model.ts @@ -0,0 +1,31 @@ +import { Model } from '@mrleebo/prisma-ast' +import { produceSchema } from './produce-schema' + +/** + * Adds an enum to your schema.prisma data model. + * + * @param source - schema.prisma source file contents + * @param modelProps - the model to add + * @returns The modified schema.prisma source + * @example Usage + * ``` + * addPrismaModel(source, { + type: "model", + name: "Project", + properties: [{type: "field", name: "id", fieldType: "String"}], + }) + * ``` + */ +export function addPrismaModel( + source: string, + modelProps: Model +): Promise<string> { + return produceSchema(source, (schema) => { + const existing = schema.list.find( + (x) => x.type === 'model' && x.name === modelProps.name + ) + existing + ? Object.assign(existing, modelProps) + : schema.list.push(modelProps) + }) +} diff --git a/nextjs/packages/installer/src/transforms/prisma/index.ts b/nextjs/packages/installer/src/transforms/prisma/index.ts new file mode 100644 index 0000000000..7ef6589681 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/prisma/index.ts @@ -0,0 +1,7 @@ +export * from './add-prisma-enum' +export * from './add-prisma-field' +export * from './add-prisma-generator' +export * from './add-prisma-model-attribute' +export * from './add-prisma-model' +export * from './produce-schema' +export * from './set-prisma-data-source' diff --git a/nextjs/packages/installer/src/transforms/prisma/print-schema.ts b/nextjs/packages/installer/src/transforms/prisma/print-schema.ts new file mode 100644 index 0000000000..b5048eaf16 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/prisma/print-schema.ts @@ -0,0 +1,14 @@ +import { printSchema as printer, Schema } from '@mrleebo/prisma-ast' + +/** + * Takes the schema.prisma document parsed from @mrleebo/prisma-ast and + * serializes it back to a schema.prisma source string. To ensure consistent + * formatting and prettify the document, we also execute the + * IntrospectionEngine from @prisma/sdk. + * + * @param schema - the parsed prisma schema + * @returns the schema.prisma source string + */ +export function printSchema(schema: Schema): string { + return printer(schema) +} diff --git a/nextjs/packages/installer/src/transforms/prisma/produce-schema.ts b/nextjs/packages/installer/src/transforms/prisma/produce-schema.ts new file mode 100644 index 0000000000..43cc8b5324 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/prisma/produce-schema.ts @@ -0,0 +1,19 @@ +import { getSchema, printSchema, Schema } from '@mrleebo/prisma-ast' + +/** + * A file transformer that parses a schema.prisma string, offers you a callback + * of the parsed document object, then takes your changes to the document and + * writes out a new schema.prisma string with the changes applied. + * + * @param source - schema.prisma source file contents + * @param producer - a callback function that can mutate the parsed data model + * @returns The modified schema.prisma source + */ +export async function produceSchema( + source: string, + producer: (schema: Schema) => void +): Promise<string> { + const schema = await getSchema(source) + producer(schema) + return printSchema(schema) +} diff --git a/nextjs/packages/installer/src/transforms/prisma/set-prisma-data-source.ts b/nextjs/packages/installer/src/transforms/prisma/set-prisma-data-source.ts new file mode 100644 index 0000000000..e63ffec0bd --- /dev/null +++ b/nextjs/packages/installer/src/transforms/prisma/set-prisma-data-source.ts @@ -0,0 +1,36 @@ +import { Datasource } from '@mrleebo/prisma-ast' +import { produceSchema } from './produce-schema' + +/** + * Modify the prisma datasource metadata to use the provider and url specified. + * + * @param source - schema.prisma source file contents + * @param datasourceProps - datasource object to assign to the schema + * @returns The modified schema.prisma source + * @example Usage + * ``` + * setPrismaDataSource(source, { + type: "datasource", + name: "db", + assignments: [ + {type: "assignment", key: "provider", value: '"postgresql"'}, + { + type: "assignment", + key: "url", + value: {type: "function", name: "env", params: ['"DATABASE_URL"']}, + }, + ], + }) + * ``` + */ +export function setPrismaDataSource( + source: string, + datasourceProps: Datasource +): Promise<string> { + return produceSchema(source, (schema) => { + const existing = schema.list.find((x) => x.type === 'datasource') + existing + ? Object.assign(existing, datasourceProps) + : schema.list.push(datasourceProps) + }) +} diff --git a/nextjs/packages/installer/src/transforms/transform-blitz-config.ts b/nextjs/packages/installer/src/transforms/transform-blitz-config.ts new file mode 100644 index 0000000000..ac3551d042 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/transform-blitz-config.ts @@ -0,0 +1,93 @@ +import type { ExpressionKind } from 'ast-types/gen/kinds' +import j from 'jscodeshift' +import { Program } from '../types' + +function recursiveConfigSearch( + program: Program, + obj: ExpressionKind +): j.ObjectExpression | undefined { + // Identifier being a variable name + if (obj.type === 'Identifier') { + const { node } = j(obj).get() + + // Get the definition of the variable + const identifier: j.ASTPath<j.VariableDeclarator> = program + .find(j.VariableDeclarator, { + id: { name: node.name }, + }) + .get() + + // Return what is after the `=` + return identifier.value.init + ? recursiveConfigSearch(program, identifier.value.init) + : undefined + } else if (obj.type === 'CallExpression') { + // If it's an function call (like `withBundleAnalyzer`), get the first argument + if (obj.arguments.length === 0) { + // If it has no arguments, create an empty object: `{}` + let config = j.objectExpression([]) + obj.arguments.push(config) + return config + } else { + const arg = obj.arguments[0] + if (arg.type === 'SpreadElement') return undefined + else return recursiveConfigSearch(program, arg) + } + } else if (obj.type === 'ObjectExpression') { + // If it's an object, return it + return obj + } else { + return undefined + } +} + +export type TransformBlitzConfigCallback = ( + config: j.ObjectExpression +) => j.ObjectExpression + +export function transformBlitzConfig( + program: Program, + transform: TransformBlitzConfigCallback +): Program { + let moduleExportsExpressions = program.find(j.AssignmentExpression, { + operator: '=', + left: { object: { name: 'module' }, property: { name: 'exports' } }, + right: {}, + }) + + // If there isn't any `module.exports = ...`, create one + if (moduleExportsExpressions.length === 0) { + let config = j.objectExpression([]) + + config = transform(config) + + let moduleExportExpression = j.expressionStatement( + j.assignmentExpression( + '=', + j.memberExpression(j.identifier('module'), j.identifier('exports')), + config + ) + ) + + program.get().node.program.body.push(moduleExportExpression) + } else if (moduleExportsExpressions.length === 1) { + let moduleExportsExpression: j.ASTPath<j.AssignmentExpression> = moduleExportsExpressions.get() + + let config: j.ObjectExpression | undefined = recursiveConfigSearch( + program, + moduleExportsExpression.value.right + ) + + if (config) { + config = transform(config) + } else { + console.warn( + "The configuration couldn't be found, but there is a 'module.exports' inside `blitz.config.js`" + ) + } + } else { + console.warn("There are multiple 'module.exports' inside 'blitz.config.js'") + } + + return program +} diff --git a/nextjs/packages/installer/src/transforms/update-babel-config.ts b/nextjs/packages/installer/src/transforms/update-babel-config.ts new file mode 100644 index 0000000000..d34a1d710c --- /dev/null +++ b/nextjs/packages/installer/src/transforms/update-babel-config.ts @@ -0,0 +1,133 @@ +import type { ExpressionKind } from 'ast-types/gen/kinds' +import j from 'jscodeshift' +import { JsonObject, JsonValue } from '../types' +import { Program } from '../types' +import { findModuleExportsExpressions } from './find-module-exports-expressions' + +type AddBabelItemDefinition = string | [name: string, options: JsonObject] + +const jsonValueToExpression = (value: JsonValue): ExpressionKind => + typeof value === 'string' + ? j.stringLiteral(value) + : typeof value === 'number' + ? j.numericLiteral(value) + : typeof value === 'boolean' + ? j.booleanLiteral(value) + : value === null + ? j.nullLiteral() + : Array.isArray(value) + ? j.arrayExpression(value.map(jsonValueToExpression)) + : j.objectExpression( + Object.entries(value) + .filter( + (entry): entry is [string, JsonValue] => entry[1] !== undefined + ) + .map(([key, value]) => + j.objectProperty(j.stringLiteral(key), jsonValueToExpression(value)) + ) + ) + +function updateBabelConfig( + program: Program, + item: AddBabelItemDefinition, + key: string +): Program { + findModuleExportsExpressions(program).forEach((moduleExportsExpression) => { + j(moduleExportsExpression) + .find(j.ObjectProperty, { key: { name: key } }) + .forEach((items) => { + // Don't add it again if it already exists, + // that what this code does. For simplicity, + // all the examples will be with key = 'presets' + + const itemName = Array.isArray(item) ? item[0] : item + + if ( + items.node.value.type === 'Literal' || + items.node.value.type === 'StringLiteral' + ) { + // { + // presets: "this-preset" + // } + if (itemName !== items.node.value.value) { + items.node.value = j.arrayExpression([ + items.node.value, + jsonValueToExpression(item), + ]) + } + } else if (items.node.value.type === 'ArrayExpression') { + // { + // presets: ["this-preset", "maybe-another", ...] + // } + // Here, it will return if it find the preset inside the + // array, so the last line doesn't push a duplicated preset + for (const [i, element] of items.node.value.elements.entries()) { + if (!element) continue + + if ( + element.type === 'Literal' || + element.type === 'StringLiteral' + ) { + // { + // presets: [..., "this-preset", ...] + // } + if (element.value === itemName) return + } else if (element.type === 'ArrayExpression') { + // { + // presets: [..., ["this-preset"], ...] + // } + if ( + (element.elements[0]?.type === 'Literal' || + element.elements[0]?.type === 'StringLiteral') && + element.elements[0].value === itemName + ) { + if ( + element.elements[1]?.type === 'ObjectExpression' && + element.elements[1].properties.length > 0 + ) { + // The preset has a config. + // ["this-preset", {...}] + if (Array.isArray(item)) { + // If it has an adittional config, add the new keys + // (don't matter if they already exists, let the user handle it later by themself) + let obj = element.elements[1] + + for (const key in item[1]) { + const value = item[1][key] + if (value === undefined) continue + obj.properties.push( + j.objectProperty( + j.stringLiteral(key), + jsonValueToExpression(value) + ) + ) + } + + items.node.value.elements[i] = obj + } + } else { + // The preset has no config. + // Its ["this-preset"] + items.node.value.elements[i] = jsonValueToExpression(item) + } + + return + } + } + } + items.node.value.elements.push(jsonValueToExpression(item)) + } + }) + }) + + return program +} + +export const addBabelPreset = ( + program: Program, + preset: AddBabelItemDefinition +): Program => updateBabelConfig(program, preset, 'presets') +export const addBabelPlugin = ( + program: Program, + plugin: AddBabelItemDefinition +): Program => updateBabelConfig(program, plugin, 'plugins') diff --git a/nextjs/packages/installer/src/transforms/with-utilities.ts b/nextjs/packages/installer/src/transforms/with-utilities.ts new file mode 100644 index 0000000000..2460ef734d --- /dev/null +++ b/nextjs/packages/installer/src/transforms/with-utilities.ts @@ -0,0 +1,24 @@ +import { + CommentKind, + TypeAnnotationKind, + TSTypeAnnotationKind, +} from 'ast-types/gen/kinds' +import j from 'jscodeshift' + +export function withComments< + Node extends { + comments?: CommentKind[] | null + } +>(node: Node, comments: CommentKind[]): Node { + node.comments = comments + return node +} + +export function withTypeAnnotation< + Node extends { + typeAnnotation?: TypeAnnotationKind | TSTypeAnnotationKind | null + } +>(node: Node, type: Parameters<typeof j.tsTypeAnnotation>[0]): Node { + node.typeAnnotation = j.tsTypeAnnotation(type) + return node +} diff --git a/nextjs/packages/installer/src/transforms/wrap-blitz-config.ts b/nextjs/packages/installer/src/transforms/wrap-blitz-config.ts new file mode 100644 index 0000000000..11b563bca8 --- /dev/null +++ b/nextjs/packages/installer/src/transforms/wrap-blitz-config.ts @@ -0,0 +1,37 @@ +import j from 'jscodeshift' +import { Program } from '../types' + +export function wrapBlitzConfig( + program: Program, + functionName: string +): Program { + let moduleExportsExpressions = program.find(j.AssignmentExpression, { + operator: '=', + left: { object: { name: 'module' }, property: { name: 'exports' } }, + right: {}, + }) + + // If there isn't any `module.exports = ...`, create one + if (moduleExportsExpressions.length === 0) { + let moduleExportExpression = j.expressionStatement( + j.assignmentExpression( + '=', + j.memberExpression(j.identifier('module'), j.identifier('exports')), + j.callExpression(j.identifier(functionName), [j.objectExpression([])]) + ) + ) + + program.get().node.program.body.push(moduleExportExpression) + } else if (moduleExportsExpressions.length === 1) { + let moduleExportsExpression: j.ASTPath<j.AssignmentExpression> = moduleExportsExpressions.get() + + moduleExportsExpression.value.right = j.callExpression( + j.identifier(functionName), + [moduleExportsExpression.value.right] + ) + } else { + console.warn("There are multiple 'module.exports' inside 'blitz.config.js'") + } + + return program +} diff --git a/nextjs/packages/installer/src/types.ts b/nextjs/packages/installer/src/types.ts new file mode 100644 index 0000000000..afcd035c44 --- /dev/null +++ b/nextjs/packages/installer/src/types.ts @@ -0,0 +1,41 @@ +import type * as j from 'jscodeshift' + +export interface RecipeMeta { + name: string + description: string + owner: string + repoLink: string +} + +export type RecipeCLIArgs = { [Key in string]?: string | true } + +export interface RecipeCLIFlags { + yesToAll: boolean +} + +export type Program = j.Collection<j.Program> + +/** +Matches a JSON object. +This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. Don't use this as a direct return type as the user would have to double-cast it: `jsonObject as unknown as CustomResponse`. Instead, you could extend your CustomResponse type from it to ensure your type only uses JSON-compatible types: `interface CustomResponse extends JsonObject { … }`. +@see https://github.com/sindresorhus/type-fest +*/ +export type JsonObject = { [Key in string]?: JsonValue } + +/** +Matches a JSON array. +@see https://github.com/sindresorhus/type-fest +*/ +export type JsonArray = JsonValue[] + +/** +Matches any valid JSON primitive value. +@see https://github.com/sindresorhus/type-fest +*/ +export type JsonPrimitive = string | number | boolean | null + +/** +Matches any valid JSON value. +@see https://github.com/sindresorhus/type-fest +*/ +export type JsonValue = JsonPrimitive | JsonObject | JsonArray diff --git a/nextjs/packages/installer/src/utils/paths.ts b/nextjs/packages/installer/src/utils/paths.ts new file mode 100644 index 0000000000..e14bd13263 --- /dev/null +++ b/nextjs/packages/installer/src/utils/paths.ts @@ -0,0 +1,34 @@ +import * as fs from 'fs-extra' +import * as path from 'path' + +function ext(jsx = false) { + return fs.existsSync(path.resolve('tsconfig.json')) + ? jsx + ? '.tsx' + : '.ts' + : '.js' +} + +export const paths = { + document() { + return `app/pages/_document${ext(true)}` + }, + app() { + return `app/pages/_app${ext(true)}` + }, + entry() { + return `app/pages/index${ext(true)}` + }, + babelConfig() { + return 'babel.config.js' + }, + blitzConfig() { + return `blitz.config${ext()}` + }, + packageJson() { + return 'package.json' + }, + prismaSchema() { + return 'db/schema.prisma' + }, +} diff --git a/nextjs/packages/installer/src/utils/transform.ts b/nextjs/packages/installer/src/utils/transform.ts new file mode 100644 index 0000000000..88998a0e3c --- /dev/null +++ b/nextjs/packages/installer/src/utils/transform.ts @@ -0,0 +1,75 @@ +import * as fs from 'fs-extra' +import j from 'jscodeshift' +import getBabelOptions, { Overrides } from 'recast/parsers/_babel_options' +import * as babel from 'recast/parsers/babel' +import { Program } from '../types' + +export const customTsParser = { + parse(source: string, options?: Overrides) { + const babelOptions = getBabelOptions(options) + babelOptions.plugins.push('typescript') + babelOptions.plugins.push('jsx') + return babel.parser.parse(source, babelOptions) + }, +} + +export enum TransformStatus { + Success = 'success', + Failure = 'failure', +} +export interface TransformResult { + status: TransformStatus + filename: string + error?: Error +} + +export type StringTransformer = (program: string) => string | Promise<string> +export type Transformer = (program: Program) => Program | Promise<Program> + +export function stringProcessFile( + original: string, + transformerFn: StringTransformer +): string | Promise<string> { + return transformerFn(original) +} + +export async function processFile( + original: string, + transformerFn: Transformer +): Promise<string> { + const program = j(original, { parser: customTsParser }) + return (await transformerFn(program)).toSource() +} + +export async function transform( + processFile: (original: string) => Promise<string>, + targetFilePaths: string[] +): Promise<TransformResult[]> { + const results: TransformResult[] = [] + for (const filePath of targetFilePaths) { + if (!fs.existsSync(filePath)) { + results.push({ + status: TransformStatus.Failure, + filename: filePath, + error: new Error(`Error: ${filePath} not found`), + }) + } + try { + const fileBuffer = fs.readFileSync(filePath) + const fileSource = fileBuffer.toString('utf-8') + const transformedCode = await processFile(fileSource) + fs.writeFileSync(filePath, transformedCode) + results.push({ + status: TransformStatus.Success, + filename: filePath, + }) + } catch (err) { + results.push({ + status: TransformStatus.Failure, + filename: filePath, + error: err as any, + }) + } + } + return results +} diff --git a/nextjs/packages/installer/src/utils/use-enter-to-continue.ts b/nextjs/packages/installer/src/utils/use-enter-to-continue.ts new file mode 100644 index 0000000000..6f4e43c80e --- /dev/null +++ b/nextjs/packages/installer/src/utils/use-enter-to-continue.ts @@ -0,0 +1,12 @@ +import { useInput } from 'ink' + +export function useEnterToContinue( + cb: Function, + additionalCondition: boolean = true +) { + useInput((_input, key) => { + if (additionalCondition && key.return) { + cb() + } + }) +} diff --git a/nextjs/packages/installer/src/utils/use-user-input.ts b/nextjs/packages/installer/src/utils/use-user-input.ts new file mode 100644 index 0000000000..89dc7d2419 --- /dev/null +++ b/nextjs/packages/installer/src/utils/use-user-input.ts @@ -0,0 +1,7 @@ +import { useStdin } from 'ink' +import { RecipeCLIFlags } from '../types' + +export function useUserInput(cliFlags: RecipeCLIFlags) { + const { isRawModeSupported } = useStdin() + return isRawModeSupported && !cliFlags.yesToAll +} diff --git a/nextjs/packages/installer/test/executors/__snapshots__/executor.test.tsx.snap b/nextjs/packages/installer/test/executors/__snapshots__/executor.test.tsx.snap new file mode 100644 index 0000000000..69e06ed4f8 --- /dev/null +++ b/nextjs/packages/installer/test/executors/__snapshots__/executor.test.tsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Executor should render Frontmatter 1`] = ` +" ++––––––––––––––+ +⎪   New File   ⎪ ++––––––––––––––+ +Testing text for a new file +" +`; diff --git a/nextjs/packages/installer/test/executors/__snapshots__/print-message-executor.test.tsx.snap b/nextjs/packages/installer/test/executors/__snapshots__/print-message-executor.test.tsx.snap new file mode 100644 index 0000000000..f9236f42b5 --- /dev/null +++ b/nextjs/packages/installer/test/executors/__snapshots__/print-message-executor.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Executor should render PrintMessageExecutor 1`] = ` +"My message + +Press ENTER to continue" +`; diff --git a/nextjs/packages/installer/test/executors/add-dependency-executor.test.ts b/nextjs/packages/installer/test/executors/add-dependency-executor.test.ts new file mode 100644 index 0000000000..49d3750499 --- /dev/null +++ b/nextjs/packages/installer/test/executors/add-dependency-executor.test.ts @@ -0,0 +1,91 @@ +import { spawn } from 'cross-spawn' +import { existsSync } from 'fs-extra' +import { mocked } from 'ts-jest/utils' +import * as AddDependencyExecutor from '../../src/executors/add-dependency-executor' + +jest.mock('fs-extra') +jest.mock('cross-spawn') + +describe('add dependency executor', () => { + const testConfiguration = { + stepId: 'addDependencies', + stepName: 'Add dependencies', + stepType: 'add-dependency', + explanation: 'This step will add some dependencies for testing purposes', + packages: [{ name: 'typescript', version: '4' }, { name: 'ts-node' }], + } + + it('should properly identify executor', () => { + const wrongConfiguration = { + stepId: 'wrongStep', + stepName: 'Wrong Step', + stepType: 'wrong-type', + explanation: 'This step is wrong', + } + expect( + AddDependencyExecutor.isAddDependencyExecutor(wrongConfiguration) + ).toBeFalsy() + expect( + AddDependencyExecutor.isAddDependencyExecutor(testConfiguration) + ).toBeTruthy() + }) + + it('should choose proper package manager according to lock file', () => { + mocked(existsSync).mockReturnValueOnce(true) + expect(AddDependencyExecutor.getPackageManager()).toEqual('yarn') + expect(AddDependencyExecutor.getPackageManager()).toEqual('npm') + }) + + it('should issue proper commands according to the specified packages', async () => { + const mockedSpawn = mockSpawn() + mocked(spawn).mockImplementation(mockedSpawn.spawn as any) + + // NPM + mocked(existsSync).mockReturnValue(false) + await AddDependencyExecutor.installPackages( + testConfiguration.packages, + true + ) + await AddDependencyExecutor.installPackages( + testConfiguration.packages, + false + ) + + // Yarn + mocked(existsSync).mockReturnValue(true) + await AddDependencyExecutor.installPackages( + testConfiguration.packages, + true + ) + await AddDependencyExecutor.installPackages( + testConfiguration.packages, + false + ) + + expect(mockedSpawn.calls.length).toEqual(4) + expect(mockedSpawn.calls[0]).toEqual( + 'npm install --save-dev typescript@4 ts-node' + ) + expect(mockedSpawn.calls[1]).toEqual('npm install typescript@4 ts-node') + expect(mockedSpawn.calls[2]).toEqual('yarn add -D typescript@4 ts-node') + expect(mockedSpawn.calls[3]).toEqual('yarn add typescript@4 ts-node') + }) +}) + +/** + * Primitive mock of spawn function + */ +const mockSpawn = () => { + let calls: string[] = [] + + return { + spawn: (command: string, args: string[], _: unknown = {}) => { + calls.push(`${command} ${args.join(' ')}`) + + return { + on: (_: string, resolve: () => void) => resolve(), + } + }, + calls, + } +} diff --git a/nextjs/packages/installer/test/executors/executor.test.tsx b/nextjs/packages/installer/test/executors/executor.test.tsx new file mode 100644 index 0000000000..156b9f0667 --- /dev/null +++ b/nextjs/packages/installer/test/executors/executor.test.tsx @@ -0,0 +1,25 @@ +import { render } from 'ink-testing-library' +import React from 'react' +import stripAnsi from 'strip-ansi' +import { Frontmatter } from '../../src/executors/executor' + +describe('Executor', () => { + const executorConfig = { + stepId: 'newFile', + stepName: 'New File', + stepType: 'new-file', + explanation: 'Testing text for a new file', + } + it('should render Frontmatter', () => { + const { lastFrame } = render(<Frontmatter executor={executorConfig} />) + + expect(stripAnsi(lastFrame())).toMatchSnapshot() + }) + + it('should contain a step name and explanation', () => { + const { frames } = render(<Frontmatter executor={executorConfig} />) + + expect(frames[0].includes('New File')).toBeTruthy() + expect(frames[0].includes('Testing text for a new file')).toBeTruthy() + }) +}) diff --git a/nextjs/packages/installer/test/executors/print-message-executor.test.tsx b/nextjs/packages/installer/test/executors/print-message-executor.test.tsx new file mode 100644 index 0000000000..4d6ec99c9c --- /dev/null +++ b/nextjs/packages/installer/test/executors/print-message-executor.test.tsx @@ -0,0 +1,39 @@ +import { render } from 'ink-testing-library' +import React from 'react' +import stripAnsi from 'strip-ansi' +import { Commit as PrintMessageExecutor } from '../../src/executors/print-message-executor' + +describe('Executor', () => { + const executorConfig = { + stepId: 'printMessage', + stepName: 'Print message', + stepType: 'print-message', + explanation: 'Testing text for a print message', + message: 'My message', + } + it('should render PrintMessageExecutor', () => { + const { lastFrame } = render( + <PrintMessageExecutor + cliArgs={null} + cliFlags={{ yesToAll: false }} + onChangeCommitted={() => {}} + step={executorConfig} + /> + ) + + expect(stripAnsi(lastFrame())).toMatchSnapshot() + }) + + it('should contain a step name and explanation', () => { + const { frames } = render( + <PrintMessageExecutor + cliArgs={null} + cliFlags={{ yesToAll: false }} + onChangeCommitted={() => {}} + step={executorConfig} + /> + ) + + expect(frames[0].includes('My message')).toBeTruthy() + }) +}) diff --git a/nextjs/packages/installer/test/transforms/__snapshots__/add-import.test.ts.snap b/nextjs/packages/installer/test/transforms/__snapshots__/add-import.test.ts.snap new file mode 100644 index 0000000000..62f176abda --- /dev/null +++ b/nextjs/packages/installer/test/transforms/__snapshots__/add-import.test.ts.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addImport transform adds import at start of file with no imports present 1`] = ` +"import React from \\"react\\"; +export const truth = () => 42" +`; + +exports[`addImport transform adds import at the end of all imports if imports are present 1`] = ` +"import React from 'react' + +import \\"app/styles/app.css\\"; + +export default function Comp() { + return <div>hello world!</div> +}" +`; diff --git a/nextjs/packages/installer/test/transforms/__snapshots__/transform-blitz-config.test.ts.snap b/nextjs/packages/installer/test/transforms/__snapshots__/transform-blitz-config.test.ts.snap new file mode 100644 index 0000000000..eb83247193 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/__snapshots__/transform-blitz-config.test.ts.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`transformBlitzConfig transform empty file 1`] = `"module.exports = {};"`; + +exports[`transformBlitzConfig transform module.exports 1`] = ` +"module.exports = { + test: true +}" +`; + +exports[`transformBlitzConfig transform the config file from examples/auth 1`] = ` +"import {sessionMiddleware, simpleRolesIsAuthorized} from \\"blitz\\" +import db from \\"db\\" +const withBundleAnalyzer = require(\\"@next/bundle-analyzer\\")({ + enabled: process.env.ANALYZE === \\"true\\", +}) + +module.exports = withBundleAnalyzer({ + middleware: [ + sessionMiddleware({ + cookiePrefix: \\"blitz-auth-example\\", + isAuthorized: simpleRolesIsAuthorized, + // sessionExpiryMinutes: 4, + getSession: (handle) => db.session.findFirst({where: {handle}}), + }), + ], + cli: { + clearConsoleOnBlitzDev: true + }, + codegen: { + templateDir: \\"my-templates\\", + }, + log: { + // level: \\"trace\\", + }, + experimental: { + initServer() { + console.log(\\"Hello world from initServer\\") + }, + }, + /* + webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => { + // Note: we provide webpack above so you should not \`require\` it + // Perform customizations to webpack config + // Important: return the modified config + return config + }, + webpackDevMiddleware: (config) => { + // Perform customizations to webpack dev middleware config + // Important: return the modified config + return config + }, + */ +})" +`; + +exports[`transformBlitzConfig transform with empty wrapper 1`] = `"module.exports = withBundleAnalyzer({})"`; + +exports[`transformBlitzConfig transform with wrapper 1`] = `"module.exports = withBundleAnalyzer({})"`; diff --git a/nextjs/packages/installer/test/transforms/__snapshots__/update-babel-config.test.ts.snap b/nextjs/packages/installer/test/transforms/__snapshots__/update-babel-config.test.ts.snap new file mode 100644 index 0000000000..f88625313f --- /dev/null +++ b/nextjs/packages/installer/test/transforms/__snapshots__/update-babel-config.test.ts.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addBabelPlugin transform adds babel plugin array 1`] = ` +"module.exports = { + presets: [\\"@babel/preset-typescript\\"], + plugins: [[\\"@babel/plugin-proposal-decorators\\", { + \\"legacy\\": true + }]], + }" +`; + +exports[`addBabelPlugin transform adds babel plugin literal 1`] = ` +"module.exports = { + presets: [\\"@babel/preset-typescript\\"], + plugins: [\\"@emotion\\"], + }" +`; + +exports[`addBabelPlugin transform avoid duplicated 1`] = ` +"module.exports = { + presets: [\\"@babel/preset-typescript\\"], + plugins: [\\"@babel/plugin-proposal-decorators\\"], + }" +`; + +exports[`addBabelPreset transform adds babel preset array 1`] = ` +"module.exports = { + presets: [\\"@babel/preset-typescript\\", [\\"blitz/babel\\", { + \\"legacy\\": true + }]], + plugins: [], + }" +`; + +exports[`addBabelPreset transform adds babel preset literal 1`] = ` +"module.exports = { + presets: [\\"@babel/preset-typescript\\", \\"blitz/babel\\"], + plugins: [], + }" +`; + +exports[`addBabelPreset transform avoid duplicated 1`] = ` +"module.exports = { + presets: [[\\"blitz/babel\\", {legacy: true}]], + plugins: [], + }" +`; diff --git a/nextjs/packages/installer/test/transforms/add-import.test.ts b/nextjs/packages/installer/test/transforms/add-import.test.ts new file mode 100644 index 0000000000..e211e29f92 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/add-import.test.ts @@ -0,0 +1,36 @@ +import { addImport, customTsParser } from '@blitzjs/installer' +import j from 'jscodeshift' + +function executeImport( + fileStr: string, + importStatement: j.ImportDeclaration +): string { + return addImport( + j(fileStr, { parser: customTsParser }), + importStatement + ).toSource({ tabWidth: 60 }) +} + +describe('addImport transform', () => { + it('adds import at start of file with no imports present', () => { + const file = `export const truth = () => 42` + const importStatement = j.importDeclaration( + [j.importDefaultSpecifier(j.identifier('React'))], + j.literal('react') + ) + expect(executeImport(file, importStatement)).toMatchSnapshot() + }) + + it('adds import at the end of all imports if imports are present', () => { + const file = `import React from 'react' + +export default function Comp() { + return <div>hello world!</div> +}` + const importStatement = j.importDeclaration( + [], + j.literal('app/styles/app.css') + ) + expect(executeImport(file, importStatement)).toMatchSnapshot() + }) +}) diff --git a/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-enum.test.ts.snap b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-enum.test.ts.snap new file mode 100644 index 0000000000..2769ceda81 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-enum.test.ts.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addPrismaEnum creates enum 1`] = ` +" +datasource db { + provider = \\"sqlite\\" + url = \\"file:./db.sqlite\\" +} + +enum Role { + USER + ADMIN +} +" +`; diff --git a/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-field.test.ts.snap b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-field.test.ts.snap new file mode 100644 index 0000000000..379fdc0f49 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-field.test.ts.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addPrismaField creates field 1`] = ` +" +datasource db { + provider = \\"sqlite\\" + url = \\"file:./db.sqlite\\" +} + +model Project { + id Int @id @default(autoincrement()) + name String @unique +} +" +`; + +exports[`addPrismaField skips if model is missing 1`] = ` +" +datasource db { + provider = \\"sqlite\\" + url = \\"file:./db.sqlite\\" +} +" +`; diff --git a/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-generator.test.ts.snap b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-generator.test.ts.snap new file mode 100644 index 0000000000..12f46582be --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-generator.test.ts.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addPrismaGenerator adds generator and keeps existing generator 1`] = ` +" +datasource db { + provider = \\"sqlite\\" + url = \\"file:./db.sqlite\\" +} + +generator client { + provider = \\"prisma-client-js\\" +} + +generator nexusPrisma { + provider = \\"nexus-prisma\\" +} +" +`; + +exports[`addPrismaGenerator adds generator to file 1`] = ` +" +datasource db { + provider = \\"sqlite\\" + url = \\"file:./db.sqlite\\" +} + +generator nexusPrisma { + provider = \\"nexus-prisma\\" +} +" +`; + +exports[`addPrismaGenerator overwrites same generator 1`] = ` +" +datasource db { + provider = \\"sqlite\\" + url = \\"file:./db.sqlite\\" +} + +generator nexusPrisma { + provider = \\"nexus-prisma\\" +} +" +`; diff --git a/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-model-attribute.test.ts.snap b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-model-attribute.test.ts.snap new file mode 100644 index 0000000000..66462f7c1b --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-model-attribute.test.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addPrismaModelAttribute creates index 1`] = ` +" +datasource db { + provider = \\"sqlite\\" + url = \\"file:./db.sqlite\\" +} + +model Project { + id Int @id @default(autoincrement()) + name String @unique + @@index([name]) +} +" +`; + +exports[`addPrismaModelAttribute skips if model is missing 1`] = ` +" +datasource db { + provider = \\"sqlite\\" + url = \\"file:./db.sqlite\\" +} +" +`; diff --git a/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-model.test.ts.snap b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-model.test.ts.snap new file mode 100644 index 0000000000..4e061de550 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/add-prisma-model.test.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addPrismaModel creates model 1`] = ` +" +datasource db { + provider = \\"sqlite\\" + url = \\"file:./db.sqlite\\" +} + +model Project { + id String +} +" +`; diff --git a/nextjs/packages/installer/test/transforms/prisma/__snapshots__/produce-schema.test.ts.snap b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/produce-schema.test.ts.snap new file mode 100644 index 0000000000..59da1be2af --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/produce-schema.test.ts.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`produceSchema cleanly parses and serializes schema: [redwood.prisma] 1`] = ` +" +datasource DS { + provider = \\"sqlite\\" + url = env(\\"DATABASE_URL\\") +} + +generator client { + provider = \\"prisma-client-js\\" + binaryTargets = \\"native\\" +} + +/// Define your own datamodels here and run \`yarn redwood db save\` to create +/// migrations for them. + +model Post { + /// this is the post id + id Int @id @default(autoincrement()) + title String + slug String @unique + author String + body String + image String? + tags Tag[] + postedAt DateTime? +} + +model Tag { + id Int @id @default(autoincrement()) + name String @unique + posts Post[] +} + +model User { + id Int @id @default(autoincrement()) + name String? + // also known as: electronic mail + email String @unique + + hasAdministrativeAccess Boolean @default(false) +} +" +`; diff --git a/nextjs/packages/installer/test/transforms/prisma/__snapshots__/set-prisma-data-source.test.ts.snap b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/set-prisma-data-source.test.ts.snap new file mode 100644 index 0000000000..02c115249a --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/__snapshots__/set-prisma-data-source.test.ts.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`setPrismaDataSource adds datasource if missing 1`] = ` +"// wow there is no datasource here + +datasource db { + provider = \\"postgresql\\" + url = env(\\"DATABASE_URL\\") +} +" +`; + +exports[`setPrismaDataSource sets datasource 1`] = ` +"// comment up here + +datasource db { + provider = \\"postgresql\\" + url = env(\\"DATABASE_URL\\") +} + +// comment down here +" +`; diff --git a/nextjs/packages/installer/test/transforms/prisma/add-prisma-enum.test.ts b/nextjs/packages/installer/test/transforms/prisma/add-prisma-enum.test.ts new file mode 100644 index 0000000000..4c7cfd428d --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/add-prisma-enum.test.ts @@ -0,0 +1,23 @@ +import { addPrismaEnum } from '@blitzjs/installer' + +describe('addPrismaEnum', () => { + const subject = (source: string) => + addPrismaEnum(source, { + type: 'enum', + name: 'Role', + enumerators: [ + { type: 'enumerator', name: 'USER' }, + { type: 'enumerator', name: 'ADMIN' }, + ], + }) + + it('creates enum', async () => { + const source = ` +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +}`.trim() + + expect(await subject(source)).toMatchSnapshot() + }) +}) diff --git a/nextjs/packages/installer/test/transforms/prisma/add-prisma-field.test.ts b/nextjs/packages/installer/test/transforms/prisma/add-prisma-field.test.ts new file mode 100644 index 0000000000..c9739fd617 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/add-prisma-field.test.ts @@ -0,0 +1,35 @@ +import { addPrismaField } from '@blitzjs/installer' + +describe('addPrismaField', () => { + const subject = (source: string) => + addPrismaField(source, 'Project', { + type: 'field', + name: 'name', + fieldType: 'String', + optional: false, + attributes: [{ type: 'attribute', kind: 'field', name: 'unique' }], + }) + + it('creates field', async () => { + const source = ` +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +} + +model Project { + id Int @id @default(autoincrement()) +}`.trim() + + expect(await subject(source)).toMatchSnapshot() + }) + + it('skips if model is missing', async () => { + const source = ` +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +}`.trim() + expect(await subject(source)).toMatchSnapshot() + }) +}) diff --git a/nextjs/packages/installer/test/transforms/prisma/add-prisma-generator.test.ts b/nextjs/packages/installer/test/transforms/prisma/add-prisma-generator.test.ts new file mode 100644 index 0000000000..60db39db3a --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/add-prisma-generator.test.ts @@ -0,0 +1,49 @@ +import { addPrismaGenerator } from '@blitzjs/installer' + +describe('addPrismaGenerator', () => { + const subject = (source: string) => + addPrismaGenerator(source, { + type: 'generator', + name: 'nexusPrisma', + assignments: [ + { type: 'assignment', key: 'provider', value: '"nexus-prisma"' }, + ], + }) + + it('adds generator and keeps existing generator', async () => { + const source = ` +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +} + +generator client { + provider = "prisma-client-js" +}`.trim() + + expect(await subject(source)).toMatchSnapshot() + }) + it('adds generator to file', async () => { + const source = ` +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +}`.trim() + + expect(await subject(source)).toMatchSnapshot() + }) + + it('overwrites same generator', async () => { + const source = ` +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +} + +generator nexusPrisma { + provider = "this-should-not-be-in-the-snapshot" +}`.trim() + + expect(await subject(source)).toMatchSnapshot() + }) +}) diff --git a/nextjs/packages/installer/test/transforms/prisma/add-prisma-model-attribute.test.ts b/nextjs/packages/installer/test/transforms/prisma/add-prisma-model-attribute.test.ts new file mode 100644 index 0000000000..56f5aabf6a --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/add-prisma-model-attribute.test.ts @@ -0,0 +1,43 @@ +import { addPrismaModelAttribute } from '@blitzjs/installer' + +describe('addPrismaModelAttribute', () => { + const subject = (source: string) => + addPrismaModelAttribute(source, 'Project', { + type: 'attribute', + kind: 'model', + name: 'index', + args: [ + { + type: 'attributeArgument', + value: { + type: 'array', + args: ['name'], + }, + }, + ], + }) + + it('creates index', async () => { + const source = ` +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +} + +model Project { + id Int @id @default(autoincrement()) + name String @unique +}`.trim() + + expect(await subject(source)).toMatchSnapshot() + }) + + it('skips if model is missing', async () => { + const source = ` +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +}`.trim() + expect(await subject(source)).toMatchSnapshot() + }) +}) diff --git a/nextjs/packages/installer/test/transforms/prisma/add-prisma-model.test.ts b/nextjs/packages/installer/test/transforms/prisma/add-prisma-model.test.ts new file mode 100644 index 0000000000..0e099fcaf6 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/add-prisma-model.test.ts @@ -0,0 +1,20 @@ +import { addPrismaModel } from '@blitzjs/installer' + +describe('addPrismaModel', () => { + const subject = (source: string) => + addPrismaModel(source, { + type: 'model', + name: 'Project', + properties: [{ type: 'field', name: 'id', fieldType: 'String' }], + }) + + it('creates model', async () => { + const source = ` +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +}`.trim() + + expect(await subject(source)).toMatchSnapshot() + }) +}) diff --git a/nextjs/packages/installer/test/transforms/prisma/fixtures/redwood.prisma b/nextjs/packages/installer/test/transforms/prisma/fixtures/redwood.prisma new file mode 100644 index 0000000000..ff359ed974 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/fixtures/redwood.prisma @@ -0,0 +1,38 @@ +datasource DS { + provider = "sqlite" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" + binaryTargets = "native" +} + +/// Define your own datamodels here and run `yarn redwood db save` to create +/// migrations for them. +model Post { + /// this is the post id + id Int @id @default(autoincrement()) + title String + slug String @unique + author String + body String + image String? + tags Tag[] + postedAt DateTime? +} + +model Tag { + id Int @id @default(autoincrement()) + name String @unique + posts Post[] +} + +model User { + id Int @id @default(autoincrement()) + name String? + // also known as: electronic mail + email String @unique + + hasAdministrativeAccess Boolean @default(false) +} diff --git a/nextjs/packages/installer/test/transforms/prisma/produce-schema.test.ts b/nextjs/packages/installer/test/transforms/prisma/produce-schema.test.ts new file mode 100644 index 0000000000..768ea313ca --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/produce-schema.test.ts @@ -0,0 +1,29 @@ +import { produceSchema } from '@blitzjs/installer' +import fs from 'fs' +import path from 'path' +import { promisify } from 'util' +const readFile = promisify(fs.readFile) + +describe('produceSchema', () => { + const subject = (source: string) => produceSchema(source, () => {}) + + let originalDatabaseUrl + beforeAll(() => { + originalDatabaseUrl = process.env.DATABASE_URL + process.env.DATABASE_URL ||= 'file:./db.sqlite' + }) + + afterAll(() => { + process.env.DATABASE_URL = originalDatabaseUrl + }) + + const fixturesDir = path.resolve(__dirname, './fixtures') + fs.readdirSync(fixturesDir).forEach((file) => { + it(`cleanly parses and serializes schema: [${file}]`, async () => { + const source = await readFile(path.resolve(fixturesDir, file), { + encoding: 'utf-8', + }) + expect(await subject(source)).toMatchSnapshot() + }) + }) +}) diff --git a/nextjs/packages/installer/test/transforms/prisma/set-prisma-data-source.test.ts b/nextjs/packages/installer/test/transforms/prisma/set-prisma-data-source.test.ts new file mode 100644 index 0000000000..8441493ba1 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/prisma/set-prisma-data-source.test.ts @@ -0,0 +1,38 @@ +import { setPrismaDataSource } from '@blitzjs/installer' + +describe('setPrismaDataSource', () => { + const subject = (source: string) => + setPrismaDataSource(source, { + type: 'datasource', + name: 'db', + assignments: [ + { type: 'assignment', key: 'provider', value: '"postgresql"' }, + { + type: 'assignment', + key: 'url', + value: { type: 'function', name: 'env', params: ['"DATABASE_URL"'] }, + }, + ], + }) + + it('sets datasource', async () => { + const source = ` +// comment up here + +datasource db { + provider = "sqlite" + url = "file:./db.sqlite" +} + +// comment down here`.trim() + + expect(await subject(source)).toMatchSnapshot() + }) + + it('adds datasource if missing', async () => { + const source = ` +// wow there is no datasource here + `.trim() + expect(await subject(source)).toMatchSnapshot() + }) +}) diff --git a/nextjs/packages/installer/test/transforms/transform-blitz-config.test.ts b/nextjs/packages/installer/test/transforms/transform-blitz-config.test.ts new file mode 100644 index 0000000000..2e038435f5 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/transform-blitz-config.test.ts @@ -0,0 +1,239 @@ +import { + customTsParser, + transformBlitzConfig, + TransformBlitzConfigCallback, +} from '@blitzjs/installer' +import j from 'jscodeshift' +import type { Options as RecastOptions } from 'recast' + +const recastOptions: RecastOptions = { + tabWidth: 2, + arrayBracketSpacing: false, + objectCurlySpacing: false, + quote: 'single', +} + +const executeTransform = ( + fileStr: string, + transform: TransformBlitzConfigCallback +) => transformBlitzConfig(j(fileStr, { parser: customTsParser }), transform) + +describe('transformBlitzConfig finds config', () => { + const CONFIG = `{testProp: 'found'}` + + function findConfig(fileStr: string, equalTo = CONFIG): boolean { + let config: j.ObjectExpression | undefined = undefined + + executeTransform(fileStr, (configObj) => { + config = configObj + return configObj + }) + + const configStr = config ? j(config).toSource(recastOptions) : null + + return configStr === equalTo + } + + it('simple module.exports', () => { + const file = ` + module.exports = ${CONFIG} + ` + expect(findConfig(file)).toBe(true) + }) + + it('different config object as a control', () => { + const file = ` + module.exports = {other: false} + ` + expect(findConfig(file)).toBe(false) + }) + + it('simple module.exports', () => { + const file = ` + module.exports = ${CONFIG} + ` + expect(findConfig(file)).toBe(true) + }) + + it('inside a variable', () => { + const file = ` + const config = ${CONFIG} + + module.exports = config + ` + expect(findConfig(file)).toBe(true) + }) + + it('with a wrapper', () => { + const file = ` + module.exports = withBundleAnalyzer(${CONFIG}) + ` + expect(findConfig(file)).toBe(true) + }) + + it('with an empty wrapper', () => { + const file = ` + module.exports = withBundleAnalyzer() + ` + expect(findConfig(file)).toBe(false) + }) + + it('as a variable inside a wrapper', () => { + const file = ` + const config = ${CONFIG} + module.exports = withBundleAnalyzer(config) + ` + expect(findConfig(file)).toBe(true) + }) + + it('nested wrapper', () => { + const file = ` + module.exports = wrapper(wrapper(wrapper(${CONFIG}))) + ` + expect(findConfig(file)).toBe(true) + }) + + it('wrapper inside a variable', () => { + const file = ` + const config = wrapper(${CONFIG}) + module.exports = config + ` + expect(findConfig(file)).toBe(true) + }) + + it('the very worst case', () => { + const file = ` + const config1 = wrapper( + wrapper(${CONFIG}), + {otherData: true} + ) + const config2 = wrapper(${CONFIG}) + const config3 = wrapper(wrapper(config2)) + module.exports = wrapper(config3) + ` + expect(findConfig(file)).toBe(true) + }) + + it('create empty object on empty function', () => { + const file = ` + module.exports = withBundleAnalyzer() + ` + expect(findConfig(file, '{}')).toBe(true) + }) +}) + +describe('transformBlitzConfig transform', () => { + it('module.exports', () => { + const file = `module.exports = {}` + + expect( + executeTransform(file, (config) => { + config.properties.push( + j.objectProperty(j.identifier('test'), j.booleanLiteral(true)) + ) + return config + }).toSource(recastOptions) + ).toMatchSnapshot() + }) + + it('empty file', () => { + const file = '' + + expect( + executeTransform(file, (config) => config).toSource(recastOptions) + ).toMatchSnapshot() + }) + + it('with wrapper', () => { + const file = `module.exports = withBundleAnalyzer({})` + + expect( + executeTransform(file, (config) => config).toSource(recastOptions) + ).toMatchSnapshot() + }) + + it('with empty wrapper', () => { + const file = `module.exports = withBundleAnalyzer()` + + expect( + executeTransform(file, (config) => config).toSource(recastOptions) + ).toMatchSnapshot() + }) + + it('the config file from examples/auth', () => { + const file = [ + 'import {sessionMiddleware, simpleRolesIsAuthorized} from "blitz"', + 'import db from "db"', + 'const withBundleAnalyzer = require("@next/bundle-analyzer")({', + ' enabled: process.env.ANALYZE === "true",', + '})', + '', + 'module.exports = withBundleAnalyzer({', + ' middleware: [', + ' sessionMiddleware({', + ' cookiePrefix: "blitz-auth-example",', + ' isAuthorized: simpleRolesIsAuthorized,', + ' // sessionExpiryMinutes: 4,', + ' getSession: (handle) => db.session.findFirst({where: {handle}}),', + ' }),', + ' ],', + ' cli: {', + ' clearConsoleOnBlitzDev: false,', + ' },', + ' codegen: {', + ' templateDir: "my-templates",', + ' },', + ' log: {', + ' // level: "trace",', + ' },', + ' experimental: {', + ' initServer() {', + ' console.log("Hello world from initServer")', + ' },', + ' },', + ' /*', + ' webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => {', + ' // Note: we provide webpack above so you should not `require` it', + ' // Perform customizations to webpack config', + ' // Important: return the modified config', + ' return config', + ' },', + ' webpackDevMiddleware: (config) => {', + ' // Perform customizations to webpack dev middleware config', + ' // Important: return the modified config', + ' return config', + ' },', + ' */', + '})', + ].join('\n') + + expect( + executeTransform(file, (config) => { + const cliValue = j.objectExpression([ + j.objectProperty( + j.identifier('clearConsoleOnBlitzDev'), + j.booleanLiteral(true) + ), + ]) + + const cliProp = config.properties.find( + (value) => + value.type === 'ObjectProperty' && + value.key.type === 'Identifier' && + value.key.name === 'cli' + ) as j.ObjectProperty | undefined + + if (!cliProp) { + config.properties.push( + j.objectProperty(j.identifier('cli'), cliValue) + ) + return config + } + + cliProp.value = cliValue + + return config + }).toSource(recastOptions) + ).toMatchSnapshot() + }) +}) diff --git a/nextjs/packages/installer/test/transforms/update-babel-config.test.ts b/nextjs/packages/installer/test/transforms/update-babel-config.test.ts new file mode 100644 index 0000000000..cd98548604 --- /dev/null +++ b/nextjs/packages/installer/test/transforms/update-babel-config.test.ts @@ -0,0 +1,96 @@ +import { + addBabelPlugin, + addBabelPreset, + customTsParser, +} from '@blitzjs/installer' +import j from 'jscodeshift' + +function executeBabelPlugin( + fileStr: string, + plugin: string | [string, Object] +): string { + return addBabelPlugin( + j(fileStr, { parser: customTsParser }), + plugin + ).toSource() +} + +function executeBabelPreset( + fileStr: string, + plugin: string | [string, Object] +): string { + return addBabelPreset( + j(fileStr, { parser: customTsParser }), + plugin + ).toSource() +} + +describe('addBabelPlugin transform', () => { + it('adds babel plugin literal', () => { + const source = `module.exports = { + presets: ["@babel/preset-typescript"], + plugins: [], + }` + + expect(executeBabelPlugin(source, '@emotion')).toMatchSnapshot() + }) + + it('adds babel plugin array', () => { + const source = `module.exports = { + presets: ["@babel/preset-typescript"], + plugins: [], + }` + + expect( + executeBabelPlugin(source, [ + '@babel/plugin-proposal-decorators', + { legacy: true }, + ]) + ).toMatchSnapshot() + }) + + it('avoid duplicated', () => { + const source = `module.exports = { + presets: ["@babel/preset-typescript"], + plugins: ["@babel/plugin-proposal-decorators"], + }` + + expect( + executeBabelPlugin(source, [ + '@babel/plugin-proposal-decorators', + { legacy: true }, + ]) + ).toMatchSnapshot() + }) +}) + +describe('addBabelPreset transform', () => { + it('adds babel preset literal', () => { + const source = `module.exports = { + presets: ["@babel/preset-typescript"], + plugins: [], + }` + + expect(executeBabelPreset(source, 'blitz/babel')).toMatchSnapshot() + }) + + it('adds babel preset array', () => { + const source = `module.exports = { + presets: ["@babel/preset-typescript"], + plugins: [], + }` + + expect( + executeBabelPreset(source, ['blitz/babel', { legacy: true }]) + ).toMatchSnapshot() + }) + + it('avoid duplicated', () => { + const source = `module.exports = { + presets: [["blitz/babel", {legacy: true}]], + plugins: [], + }` + + expect(executeBabelPreset(source, 'blitz/babel')).toMatchSnapshot() + }) +}) diff --git a/nextjs/packages/installer/test/utils/paths.test.ts b/nextjs/packages/installer/test/utils/paths.test.ts new file mode 100644 index 0000000000..d913e24391 --- /dev/null +++ b/nextjs/packages/installer/test/utils/paths.test.ts @@ -0,0 +1,28 @@ +import { paths } from '@blitzjs/installer' +import * as fs from 'fs-extra' + +jest.mock('fs-extra') + +const testIfNotWindows = process.platform === 'win32' ? test.skip : test + +describe('path utils', () => { + it('returns proper file paths in a TS project', () => { + fs.existsSync.mockReturnValue(true) + expect(paths.document()).toBe('app/pages/_document.tsx') + expect(paths.app()).toBe('app/pages/_app.tsx') + expect(paths.entry()).toBe('app/pages/index.tsx') + // Blitz and Babel configs are always JS, we shouldn't transform this extension + expect(paths.blitzConfig()).toBe('blitz.config.ts') + expect(paths.babelConfig()).toBe('babel.config.js') + }) + + // SKIP test because the fs mock is failing on windows + testIfNotWindows('returns proper file paths in a JS project', () => { + fs.existsSync.mockReturnValue(false) + expect(paths.document()).toBe('app/pages/_document.js') + expect(paths.app()).toBe('app/pages/_app.js') + expect(paths.entry()).toBe('app/pages/index.js') + expect(paths.blitzConfig()).toBe('blitz.config.js') + expect(paths.babelConfig()).toBe('babel.config.js') + }) +}) diff --git a/nextjs/packages/installer/types/index.d.ts b/nextjs/packages/installer/types/index.d.ts new file mode 100644 index 0000000000..336ce12bb9 --- /dev/null +++ b/nextjs/packages/installer/types/index.d.ts @@ -0,0 +1 @@ +export {} diff --git a/nextjs/packages/next-bundle-analyzer/index.js b/nextjs/packages/next-bundle-analyzer/index.js new file mode 100644 index 0000000000..1431ee2d54 --- /dev/null +++ b/nextjs/packages/next-bundle-analyzer/index.js @@ -0,0 +1,22 @@ +module.exports = ({ enabled = true } = {}) => (nextConfig = {}) => { + return Object.assign({}, nextConfig, { + webpack(config, options) { + if (enabled) { + const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') + config.plugins.push( + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + reportFilename: options.isServer + ? '../analyze/server.html' + : './analyze/client.html', + }) + ) + } + + if (typeof nextConfig.webpack === 'function') { + return nextConfig.webpack(config, options) + } + return config + }, + }) +} diff --git a/nextjs/packages/next-bundle-analyzer/package.json b/nextjs/packages/next-bundle-analyzer/package.json new file mode 100644 index 0000000000..0eb1049983 --- /dev/null +++ b/nextjs/packages/next-bundle-analyzer/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "name": "@next/bundle-analyzer", + "version": "11.1.0", + "main": "index.js", + "license": "MIT", + "repository": { + "url": "vercel/next.js", + "directory": "packages/next-bundle-analyzer" + }, + "dependencies": { + "webpack-bundle-analyzer": "4.3.0" + } +} diff --git a/nextjs/packages/next-bundle-analyzer/readme.md b/nextjs/packages/next-bundle-analyzer/readme.md new file mode 100644 index 0000000000..066b0b8e59 --- /dev/null +++ b/nextjs/packages/next-bundle-analyzer/readme.md @@ -0,0 +1,61 @@ +# Next.js + Webpack Bundle Analyzer + +Use `webpack-bundle-analyzer` in your Next.js project + +## Installation + +``` +npm install @next/bundle-analyzer +``` + +or + +``` +yarn add @next/bundle-analyzer +``` + +Note: if installing as a `devDependency` make sure to wrap the require in a `process.env` check as `next.config.js` is loaded during `next start` as well. + +### Usage with environment variables + +Create a next.config.js (and make sure you have next-bundle-analyzer set up) + +```js +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}) +module.exports = withBundleAnalyzer({}) +``` + +Or configuration as a function: + +```js +module.exports = (phase, defaultConfig) => { + return withBundleAnalyzer(defaultConfig) +} +``` + +Then you can run the command below: + +```bash +# Analyze is done on build when env var is set +ANALYZE=true yarn build +``` + +When enabled two HTML files (client.html and server.html) will be outputted to `<distDir>/analyze/`. One will be for the server bundle, one for the browser bundle. + +### Usage with next-compose-plugins + +From version 2.0.0 of next-compose-plugins you need to call bundle-analyzer in this way to work + +```js +const withPlugins = require('next-compose-plugins') +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}) + +module.exports = withPlugins([ + [withBundleAnalyzer], + // your other plugins here +]) +``` diff --git a/nextjs/packages/next-codemod/.gitignore b/nextjs/packages/next-codemod/.gitignore new file mode 100644 index 0000000000..5eadbe5734 --- /dev/null +++ b/nextjs/packages/next-codemod/.gitignore @@ -0,0 +1,5 @@ +*.d.ts +*.js +*.js.map +!transforms/__tests__/**/*.js +!transforms/__testfixtures__/**/*.js \ No newline at end of file diff --git a/nextjs/packages/next-codemod/README.md b/nextjs/packages/next-codemod/README.md new file mode 100644 index 0000000000..ce458da7e9 --- /dev/null +++ b/nextjs/packages/next-codemod/README.md @@ -0,0 +1,9 @@ +# Next.js Codemods + +Next.js provides Codemod transformations to help upgrade your Next.js codebase when a feature is deprecated. + +Codemods are transformations that run on your codebase programmatically. This allows for a large amount of changes to be applied without having to manually go through every file. + +## Documentation + +Visit [nextjs.org/docs/advanced-features/codemods](https://nextjs.org/docs/advanced-features/codemods) to view the documentation for this package. diff --git a/nextjs/packages/next-codemod/bin/cli.ts b/nextjs/packages/next-codemod/bin/cli.ts new file mode 100644 index 0000000000..82a53c768f --- /dev/null +++ b/nextjs/packages/next-codemod/bin/cli.ts @@ -0,0 +1,216 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +// Based on https://github.com/reactjs/react-codemod/blob/dd8671c9a470a2c342b221ec903c574cf31e9f57/bin/cli.js +// @next/codemod optional-name-of-transform optional/path/to/src [...options] + +import globby from 'globby' +import inquirer from 'inquirer' +import meow from 'meow' +import path from 'path' +import execa from 'execa' +import chalk from 'chalk' +import isGitClean from 'is-git-clean' + +export const jscodeshiftExecutable = require.resolve('.bin/jscodeshift') +export const transformerDirectory = path.join(__dirname, '../', 'transforms') + +export function checkGitStatus(force) { + let clean = false + let errorMessage = 'Unable to determine if git directory is clean' + try { + clean = isGitClean.sync(process.cwd()) + errorMessage = 'Git directory is not clean' + } catch (err) { + if (err && err.stderr && err.stderr.indexOf('Not a git repository') >= 0) { + clean = true + } + } + + if (!clean) { + if (force) { + console.log(`WARNING: ${errorMessage}. Forcibly continuing.`) + } else { + console.log('Thank you for using @next/codemod!') + console.log( + chalk.yellow( + '\nBut before we continue, please stash or commit your git changes.' + ) + ) + console.log( + '\nYou may use the --force flag to override this safety check.' + ) + process.exit(1) + } + } +} + +export function runTransform({ files, flags, transformer }) { + const transformerPath = path.join(transformerDirectory, `${transformer}.js`) + + if (transformer === 'cra-to-next') { + // cra-to-next transform doesn't use jscodeshift directly + return require(transformerPath).default(files, flags) + } + + let args = [] + + const { dry, print, runInBand } = flags + + if (dry) { + args.push('--dry') + } + if (print) { + args.push('--print') + } + if (runInBand) { + args.push('--run-in-band') + } + + args.push('--verbose=2') + + args.push('--ignore-pattern=**/node_modules/**') + args.push('--ignore-pattern=**/.next/**') + + args.push('--extensions=tsx,ts,jsx,js') + args.push('--parser=tsx') + + args = args.concat(['--transform', transformerPath]) + + if (flags.jscodeshift) { + args = args.concat(flags.jscodeshift) + } + + args = args.concat(files) + + console.log(`Executing command: jscodeshift ${args.join(' ')}`) + + const result = execa.sync(jscodeshiftExecutable, args, { + stdio: 'inherit', + stripFinalNewline: false, + }) + + if (result.failed) { + throw new Error(`jscodeshift exited with code ${result.exitCode}`) + } +} + +const TRANSFORMER_INQUIRER_CHOICES = [ + { + name: + 'name-default-component: Transforms anonymous components into named components to make sure they work with Fast Refresh', + value: 'name-default-component', + }, + { + name: + 'add-missing-react-import: Transforms files that do not import `React` to include the import in order for the new React JSX transform', + value: 'add-missing-react-import', + }, + { + name: + 'withamp-to-config: Transforms the withAmp HOC into Next.js 9 page configuration', + value: 'withamp-to-config', + }, + { + name: + 'url-to-withrouter: Transforms the deprecated automatically injected url property on top level pages to using withRouter', + value: 'url-to-withrouter', + }, + { + name: + 'cra-to-next (experimental): automatically migrates a Create React App project to Next.js', + value: 'cra-to-next', + }, +] + +function expandFilePathsIfNeeded(filesBeforeExpansion) { + const shouldExpandFiles = filesBeforeExpansion.some((file) => + file.includes('*') + ) + return shouldExpandFiles + ? globby.sync(filesBeforeExpansion) + : filesBeforeExpansion +} + +export function run() { + const cli = meow({ + description: 'Codemods for updating Next.js apps.', + help: ` + Usage + $ npx @next/codemod <transform> <path> <...options> + transform One of the choices from https://github.com/vercel/next.js/tree/canary/packages/next-codemod + path Files or directory to transform. Can be a glob like pages/**.js + Options + --force Bypass Git safety checks and forcibly run codemods + --dry Dry run (no changes are made to files) + --print Print transformed files to your terminal + --jscodeshift (Advanced) Pass options directly to jscodeshift + `, + flags: { + boolean: ['force', 'dry', 'print', 'help'], + string: ['_'], + alias: { + h: 'help', + }, + }, + } as meow.Options<meow.AnyFlags>) + + if (!cli.flags.dry) { + checkGitStatus(cli.flags.force) + } + + if ( + cli.input[0] && + !TRANSFORMER_INQUIRER_CHOICES.find((x) => x.value === cli.input[0]) + ) { + console.error('Invalid transform choice, pick one of:') + console.error( + TRANSFORMER_INQUIRER_CHOICES.map((x) => '- ' + x.value).join('\n') + ) + process.exit(1) + } + + inquirer + .prompt([ + { + type: 'input', + name: 'files', + message: 'On which files or directory should the codemods be applied?', + when: !cli.input[1], + default: '.', + // validate: () => + filter: (files) => files.trim(), + }, + { + type: 'list', + name: 'transformer', + message: 'Which transform would you like to apply?', + when: !cli.input[0], + pageSize: TRANSFORMER_INQUIRER_CHOICES.length, + choices: TRANSFORMER_INQUIRER_CHOICES, + }, + ]) + .then((answers) => { + const { files, transformer } = answers + + const filesBeforeExpansion = cli.input[1] || files + const filesExpanded = expandFilePathsIfNeeded([filesBeforeExpansion]) + + const selectedTransformer = cli.input[0] || transformer + + if (!filesExpanded.length) { + console.log(`No files found matching ${filesBeforeExpansion.join(' ')}`) + return null + } + + return runTransform({ + files: filesExpanded, + flags: cli.flags, + transformer: selectedTransformer, + }) + }) +} diff --git a/nextjs/packages/next-codemod/bin/next-codemod.ts b/nextjs/packages/next-codemod/bin/next-codemod.ts new file mode 100644 index 0000000000..8268a4de76 --- /dev/null +++ b/nextjs/packages/next-codemod/bin/next-codemod.ts @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +// Based on https://github.com/reactjs/react-codemod/blob/dd8671c9a470a2c342b221ec903c574cf31e9f57/bin/react-codemod.js +// next-codemod optional-name-of-transform optional/path/to/src [...options] + +require('./cli').run() diff --git a/nextjs/packages/next-codemod/jest.config.js b/nextjs/packages/next-codemod/jest.config.js new file mode 100644 index 0000000000..a1d97949ab --- /dev/null +++ b/nextjs/packages/next-codemod/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: '../../../jest-unit.config.js', +} diff --git a/nextjs/packages/next-codemod/jest.setup.js b/nextjs/packages/next-codemod/jest.setup.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/packages/next-codemod/lib/cra-to-next/gitignore b/nextjs/packages/next-codemod/lib/cra-to-next/gitignore new file mode 100644 index 0000000000..1437c53f70 --- /dev/null +++ b/nextjs/packages/next-codemod/lib/cra-to-next/gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/nextjs/packages/next-codemod/lib/cra-to-next/global-css-transform.ts b/nextjs/packages/next-codemod/lib/cra-to-next/global-css-transform.ts new file mode 100644 index 0000000000..2aadefbd5d --- /dev/null +++ b/nextjs/packages/next-codemod/lib/cra-to-next/global-css-transform.ts @@ -0,0 +1,65 @@ +import nodePath from 'path' +import { API, FileInfo, Options } from 'jscodeshift' + +export const globalCssContext = { + cssImports: new Set<string>(), + reactSvgImports: new Set<string>(), +} +const globalStylesRegex = /(?<!\.module)\.(css|scss|sass)$/i + +export default function transformer( + file: FileInfo, + api: API, + options: Options +) { + const j = api.jscodeshift + const root = j(file.source) + let hasModifications = false + + root + .find(j.ImportDeclaration) + .filter((path) => { + const { + node: { + source: { value }, + }, + } = path + + if (typeof value === 'string') { + if (globalStylesRegex.test(value)) { + let resolvedPath = value + + if (value.startsWith('.')) { + resolvedPath = nodePath.resolve(nodePath.dirname(file.path), value) + } + globalCssContext.cssImports.add(resolvedPath) + + const { start, end } = path.node as any + + if (!path.parentPath.node.comments) { + path.parentPath.node.comments = [] + } + + path.parentPath.node.comments = [ + j.commentLine(' ' + file.source.substring(start, end)), + ] + hasModifications = true + return true + } else if (value.endsWith('.svg')) { + const isComponentImport = path.node.specifiers.some((specifier) => { + return (specifier as any).imported?.name === 'ReactComponent' + }) + + if (isComponentImport) { + globalCssContext.reactSvgImports.add(file.path) + } + } + } + return false + }) + .remove() + + return hasModifications && globalCssContext.reactSvgImports.size === 0 + ? root.toSource(options) + : null +} diff --git a/nextjs/packages/next-codemod/lib/cra-to-next/index-to-component.ts b/nextjs/packages/next-codemod/lib/cra-to-next/index-to-component.ts new file mode 100644 index 0000000000..24bd5fd4e5 --- /dev/null +++ b/nextjs/packages/next-codemod/lib/cra-to-next/index-to-component.ts @@ -0,0 +1,101 @@ +import { API, FileInfo, JSXElement, Options } from 'jscodeshift' + +export const indexContext = { + multipleRenderRoots: false, + nestedRender: false, +} + +export default function transformer( + file: FileInfo, + api: API, + options: Options +) { + const j = api.jscodeshift + const root = j(file.source) + let hasModifications = false + let foundReactRender = 0 + let hasRenderImport = false + let defaultReactDomImport: string | undefined + + root.find(j.ImportDeclaration).forEach((path) => { + if (path.node.source.value === 'react-dom') { + return path.node.specifiers.forEach((specifier) => { + if (specifier.local.name === 'render') { + hasRenderImport = true + } + if (specifier.type === 'ImportDefaultSpecifier') { + defaultReactDomImport = specifier.local.name + } + }) + } + return false + }) + + root + .find(j.CallExpression) + .filter((path) => { + const { node } = path + let found = false + + if ( + defaultReactDomImport && + node.callee.type === 'MemberExpression' && + (node.callee.object as any).name === defaultReactDomImport && + (node.callee.property as any).name === 'render' + ) { + found = true + } + + if (hasRenderImport && (node.callee as any).name === 'render') { + found = true + } + + if (found) { + foundReactRender++ + hasModifications = true + + if (!Array.isArray(path.parentPath?.parentPath?.value)) { + indexContext.nestedRender = true + return false + } + + const newNode = j.exportDefaultDeclaration( + j.functionDeclaration( + j.identifier('NextIndexWrapper'), + [], + j.blockStatement([ + j.returnStatement( + // TODO: remove React.StrictMode wrapper and use + // next.config.js option instead? + path.node.arguments.find( + (a) => a.type === 'JSXElement' + ) as JSXElement + ), + ]) + ) + ) + + path.parentPath.insertBefore(newNode) + return true + } + return false + }) + .remove() + + indexContext.multipleRenderRoots = foundReactRender > 1 + hasModifications = + hasModifications && + !indexContext.nestedRender && + !indexContext.multipleRenderRoots + + // TODO: move function passed to reportWebVitals if present to + // _app reportWebVitals and massage values to expected shape + + // root.find(j.CallExpression, { + // callee: { + // name: 'reportWebVitals' + // } + // }).remove() + + return hasModifications ? root.toSource(options) : null +} diff --git a/nextjs/packages/next-codemod/lib/html-to-react-attributes.ts b/nextjs/packages/next-codemod/lib/html-to-react-attributes.ts new file mode 100644 index 0000000000..08560f28a8 --- /dev/null +++ b/nextjs/packages/next-codemod/lib/html-to-react-attributes.ts @@ -0,0 +1,502 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// When adding attributes to the HTML or SVG allowed attribute list, be sure to +// also add them to this module to ensure casing and incorrect name +// warnings. + +// Pulled from https://github.com/facebook/react/blob/master/packages/react-dom/src/shared/possibleStandardNames.js +const possibleStandardNames = { + // HTML + accept: 'accept', + acceptcharset: 'acceptCharset', + 'accept-charset': 'acceptCharset', + accesskey: 'accessKey', + action: 'action', + allowfullscreen: 'allowFullScreen', + alt: 'alt', + as: 'as', + async: 'async', + autocapitalize: 'autoCapitalize', + autocomplete: 'autoComplete', + autocorrect: 'autoCorrect', + autofocus: 'autoFocus', + autoplay: 'autoPlay', + autosave: 'autoSave', + capture: 'capture', + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing', + challenge: 'challenge', + charset: 'charSet', + checked: 'checked', + children: 'children', + cite: 'cite', + class: 'className', + classid: 'classID', + classname: 'className', + cols: 'cols', + colspan: 'colSpan', + content: 'content', + contenteditable: 'contentEditable', + contextmenu: 'contextMenu', + controls: 'controls', + controlslist: 'controlsList', + coords: 'coords', + crossorigin: 'crossOrigin', + dangerouslysetinnerhtml: 'dangerouslySetInnerHTML', + data: 'data', + datetime: 'dateTime', + default: 'default', + defaultchecked: 'defaultChecked', + defaultvalue: 'defaultValue', + defer: 'defer', + dir: 'dir', + disabled: 'disabled', + disablepictureinpicture: 'disablePictureInPicture', + disableremoteplayback: 'disableRemotePlayback', + download: 'download', + draggable: 'draggable', + enctype: 'encType', + enterkeyhint: 'enterKeyHint', + for: 'htmlFor', + form: 'form', + formmethod: 'formMethod', + formaction: 'formAction', + formenctype: 'formEncType', + formnovalidate: 'formNoValidate', + formtarget: 'formTarget', + frameborder: 'frameBorder', + headers: 'headers', + height: 'height', + hidden: 'hidden', + high: 'high', + href: 'href', + hreflang: 'hrefLang', + htmlfor: 'htmlFor', + httpequiv: 'httpEquiv', + 'http-equiv': 'httpEquiv', + icon: 'icon', + id: 'id', + innerhtml: 'innerHTML', + inputmode: 'inputMode', + integrity: 'integrity', + is: 'is', + itemid: 'itemID', + itemprop: 'itemProp', + itemref: 'itemRef', + itemscope: 'itemScope', + itemtype: 'itemType', + keyparams: 'keyParams', + keytype: 'keyType', + kind: 'kind', + label: 'label', + lang: 'lang', + list: 'list', + loop: 'loop', + low: 'low', + manifest: 'manifest', + marginwidth: 'marginWidth', + marginheight: 'marginHeight', + max: 'max', + maxlength: 'maxLength', + media: 'media', + mediagroup: 'mediaGroup', + method: 'method', + min: 'min', + minlength: 'minLength', + multiple: 'multiple', + muted: 'muted', + name: 'name', + nomodule: 'noModule', + nonce: 'nonce', + novalidate: 'noValidate', + open: 'open', + optimum: 'optimum', + pattern: 'pattern', + placeholder: 'placeholder', + playsinline: 'playsInline', + poster: 'poster', + preload: 'preload', + profile: 'profile', + radiogroup: 'radioGroup', + readonly: 'readOnly', + referrerpolicy: 'referrerPolicy', + rel: 'rel', + required: 'required', + reversed: 'reversed', + role: 'role', + rows: 'rows', + rowspan: 'rowSpan', + sandbox: 'sandbox', + scope: 'scope', + scoped: 'scoped', + scrolling: 'scrolling', + seamless: 'seamless', + selected: 'selected', + shape: 'shape', + size: 'size', + sizes: 'sizes', + span: 'span', + spellcheck: 'spellCheck', + src: 'src', + srcdoc: 'srcDoc', + srclang: 'srcLang', + srcset: 'srcSet', + start: 'start', + step: 'step', + style: 'style', + summary: 'summary', + tabindex: 'tabIndex', + target: 'target', + title: 'title', + type: 'type', + usemap: 'useMap', + value: 'value', + width: 'width', + wmode: 'wmode', + wrap: 'wrap', + + // SVG + about: 'about', + accentheight: 'accentHeight', + 'accent-height': 'accentHeight', + accumulate: 'accumulate', + additive: 'additive', + alignmentbaseline: 'alignmentBaseline', + 'alignment-baseline': 'alignmentBaseline', + allowreorder: 'allowReorder', + alphabetic: 'alphabetic', + amplitude: 'amplitude', + arabicform: 'arabicForm', + 'arabic-form': 'arabicForm', + ascent: 'ascent', + attributename: 'attributeName', + attributetype: 'attributeType', + autoreverse: 'autoReverse', + azimuth: 'azimuth', + basefrequency: 'baseFrequency', + baselineshift: 'baselineShift', + 'baseline-shift': 'baselineShift', + baseprofile: 'baseProfile', + bbox: 'bbox', + begin: 'begin', + bias: 'bias', + by: 'by', + calcmode: 'calcMode', + capheight: 'capHeight', + 'cap-height': 'capHeight', + clip: 'clip', + clippath: 'clipPath', + 'clip-path': 'clipPath', + clippathunits: 'clipPathUnits', + cliprule: 'clipRule', + 'clip-rule': 'clipRule', + color: 'color', + colorinterpolation: 'colorInterpolation', + 'color-interpolation': 'colorInterpolation', + colorinterpolationfilters: 'colorInterpolationFilters', + 'color-interpolation-filters': 'colorInterpolationFilters', + colorprofile: 'colorProfile', + 'color-profile': 'colorProfile', + colorrendering: 'colorRendering', + 'color-rendering': 'colorRendering', + contentscripttype: 'contentScriptType', + contentstyletype: 'contentStyleType', + cursor: 'cursor', + cx: 'cx', + cy: 'cy', + d: 'd', + datatype: 'datatype', + decelerate: 'decelerate', + descent: 'descent', + diffuseconstant: 'diffuseConstant', + direction: 'direction', + display: 'display', + divisor: 'divisor', + dominantbaseline: 'dominantBaseline', + 'dominant-baseline': 'dominantBaseline', + dur: 'dur', + dx: 'dx', + dy: 'dy', + edgemode: 'edgeMode', + elevation: 'elevation', + enablebackground: 'enableBackground', + 'enable-background': 'enableBackground', + end: 'end', + exponent: 'exponent', + externalresourcesrequired: 'externalResourcesRequired', + fill: 'fill', + fillopacity: 'fillOpacity', + 'fill-opacity': 'fillOpacity', + fillrule: 'fillRule', + 'fill-rule': 'fillRule', + filter: 'filter', + filterres: 'filterRes', + filterunits: 'filterUnits', + floodopacity: 'floodOpacity', + 'flood-opacity': 'floodOpacity', + floodcolor: 'floodColor', + 'flood-color': 'floodColor', + focusable: 'focusable', + fontfamily: 'fontFamily', + 'font-family': 'fontFamily', + fontsize: 'fontSize', + 'font-size': 'fontSize', + fontsizeadjust: 'fontSizeAdjust', + 'font-size-adjust': 'fontSizeAdjust', + fontstretch: 'fontStretch', + 'font-stretch': 'fontStretch', + fontstyle: 'fontStyle', + 'font-style': 'fontStyle', + fontvariant: 'fontVariant', + 'font-variant': 'fontVariant', + fontweight: 'fontWeight', + 'font-weight': 'fontWeight', + format: 'format', + from: 'from', + fx: 'fx', + fy: 'fy', + g1: 'g1', + g2: 'g2', + glyphname: 'glyphName', + 'glyph-name': 'glyphName', + glyphorientationhorizontal: 'glyphOrientationHorizontal', + 'glyph-orientation-horizontal': 'glyphOrientationHorizontal', + glyphorientationvertical: 'glyphOrientationVertical', + 'glyph-orientation-vertical': 'glyphOrientationVertical', + glyphref: 'glyphRef', + gradienttransform: 'gradientTransform', + gradientunits: 'gradientUnits', + hanging: 'hanging', + horizadvx: 'horizAdvX', + 'horiz-adv-x': 'horizAdvX', + horizoriginx: 'horizOriginX', + 'horiz-origin-x': 'horizOriginX', + ideographic: 'ideographic', + imagerendering: 'imageRendering', + 'image-rendering': 'imageRendering', + in2: 'in2', + in: 'in', + inlist: 'inlist', + intercept: 'intercept', + k1: 'k1', + k2: 'k2', + k3: 'k3', + k4: 'k4', + k: 'k', + kernelmatrix: 'kernelMatrix', + kernelunitlength: 'kernelUnitLength', + kerning: 'kerning', + keypoints: 'keyPoints', + keysplines: 'keySplines', + keytimes: 'keyTimes', + lengthadjust: 'lengthAdjust', + letterspacing: 'letterSpacing', + 'letter-spacing': 'letterSpacing', + lightingcolor: 'lightingColor', + 'lighting-color': 'lightingColor', + limitingconeangle: 'limitingConeAngle', + local: 'local', + markerend: 'markerEnd', + 'marker-end': 'markerEnd', + markerheight: 'markerHeight', + markermid: 'markerMid', + 'marker-mid': 'markerMid', + markerstart: 'markerStart', + 'marker-start': 'markerStart', + markerunits: 'markerUnits', + markerwidth: 'markerWidth', + mask: 'mask', + maskcontentunits: 'maskContentUnits', + maskunits: 'maskUnits', + mathematical: 'mathematical', + mode: 'mode', + numoctaves: 'numOctaves', + offset: 'offset', + opacity: 'opacity', + operator: 'operator', + order: 'order', + orient: 'orient', + orientation: 'orientation', + origin: 'origin', + overflow: 'overflow', + overlineposition: 'overlinePosition', + 'overline-position': 'overlinePosition', + overlinethickness: 'overlineThickness', + 'overline-thickness': 'overlineThickness', + paintorder: 'paintOrder', + 'paint-order': 'paintOrder', + panose1: 'panose1', + 'panose-1': 'panose1', + pathlength: 'pathLength', + patterncontentunits: 'patternContentUnits', + patterntransform: 'patternTransform', + patternunits: 'patternUnits', + pointerevents: 'pointerEvents', + 'pointer-events': 'pointerEvents', + points: 'points', + pointsatx: 'pointsAtX', + pointsaty: 'pointsAtY', + pointsatz: 'pointsAtZ', + prefix: 'prefix', + preservealpha: 'preserveAlpha', + preserveaspectratio: 'preserveAspectRatio', + primitiveunits: 'primitiveUnits', + property: 'property', + r: 'r', + radius: 'radius', + refx: 'refX', + refy: 'refY', + renderingintent: 'renderingIntent', + 'rendering-intent': 'renderingIntent', + repeatcount: 'repeatCount', + repeatdur: 'repeatDur', + requiredextensions: 'requiredExtensions', + requiredfeatures: 'requiredFeatures', + resource: 'resource', + restart: 'restart', + result: 'result', + results: 'results', + rotate: 'rotate', + rx: 'rx', + ry: 'ry', + scale: 'scale', + security: 'security', + seed: 'seed', + shaperendering: 'shapeRendering', + 'shape-rendering': 'shapeRendering', + slope: 'slope', + spacing: 'spacing', + specularconstant: 'specularConstant', + specularexponent: 'specularExponent', + speed: 'speed', + spreadmethod: 'spreadMethod', + startoffset: 'startOffset', + stddeviation: 'stdDeviation', + stemh: 'stemh', + stemv: 'stemv', + stitchtiles: 'stitchTiles', + stopcolor: 'stopColor', + 'stop-color': 'stopColor', + stopopacity: 'stopOpacity', + 'stop-opacity': 'stopOpacity', + strikethroughposition: 'strikethroughPosition', + 'strikethrough-position': 'strikethroughPosition', + strikethroughthickness: 'strikethroughThickness', + 'strikethrough-thickness': 'strikethroughThickness', + string: 'string', + stroke: 'stroke', + strokedasharray: 'strokeDasharray', + 'stroke-dasharray': 'strokeDasharray', + strokedashoffset: 'strokeDashoffset', + 'stroke-dashoffset': 'strokeDashoffset', + strokelinecap: 'strokeLinecap', + 'stroke-linecap': 'strokeLinecap', + strokelinejoin: 'strokeLinejoin', + 'stroke-linejoin': 'strokeLinejoin', + strokemiterlimit: 'strokeMiterlimit', + 'stroke-miterlimit': 'strokeMiterlimit', + strokewidth: 'strokeWidth', + 'stroke-width': 'strokeWidth', + strokeopacity: 'strokeOpacity', + 'stroke-opacity': 'strokeOpacity', + suppresscontenteditablewarning: 'suppressContentEditableWarning', + suppresshydrationwarning: 'suppressHydrationWarning', + surfacescale: 'surfaceScale', + systemlanguage: 'systemLanguage', + tablevalues: 'tableValues', + targetx: 'targetX', + targety: 'targetY', + textanchor: 'textAnchor', + 'text-anchor': 'textAnchor', + textdecoration: 'textDecoration', + 'text-decoration': 'textDecoration', + textlength: 'textLength', + textrendering: 'textRendering', + 'text-rendering': 'textRendering', + to: 'to', + transform: 'transform', + typeof: 'typeof', + u1: 'u1', + u2: 'u2', + underlineposition: 'underlinePosition', + 'underline-position': 'underlinePosition', + underlinethickness: 'underlineThickness', + 'underline-thickness': 'underlineThickness', + unicode: 'unicode', + unicodebidi: 'unicodeBidi', + 'unicode-bidi': 'unicodeBidi', + unicoderange: 'unicodeRange', + 'unicode-range': 'unicodeRange', + unitsperem: 'unitsPerEm', + 'units-per-em': 'unitsPerEm', + unselectable: 'unselectable', + valphabetic: 'vAlphabetic', + 'v-alphabetic': 'vAlphabetic', + values: 'values', + vectoreffect: 'vectorEffect', + 'vector-effect': 'vectorEffect', + version: 'version', + vertadvy: 'vertAdvY', + 'vert-adv-y': 'vertAdvY', + vertoriginx: 'vertOriginX', + 'vert-origin-x': 'vertOriginX', + vertoriginy: 'vertOriginY', + 'vert-origin-y': 'vertOriginY', + vhanging: 'vHanging', + 'v-hanging': 'vHanging', + videographic: 'vIdeographic', + 'v-ideographic': 'vIdeographic', + viewbox: 'viewBox', + viewtarget: 'viewTarget', + visibility: 'visibility', + vmathematical: 'vMathematical', + 'v-mathematical': 'vMathematical', + vocab: 'vocab', + widths: 'widths', + wordspacing: 'wordSpacing', + 'word-spacing': 'wordSpacing', + writingmode: 'writingMode', + 'writing-mode': 'writingMode', + x1: 'x1', + x2: 'x2', + x: 'x', + xchannelselector: 'xChannelSelector', + xheight: 'xHeight', + 'x-height': 'xHeight', + xlinkactuate: 'xlinkActuate', + 'xlink:actuate': 'xlinkActuate', + xlinkarcrole: 'xlinkArcrole', + 'xlink:arcrole': 'xlinkArcrole', + xlinkhref: 'xlinkHref', + 'xlink:href': 'xlinkHref', + xlinkrole: 'xlinkRole', + 'xlink:role': 'xlinkRole', + xlinkshow: 'xlinkShow', + 'xlink:show': 'xlinkShow', + xlinktitle: 'xlinkTitle', + 'xlink:title': 'xlinkTitle', + xlinktype: 'xlinkType', + 'xlink:type': 'xlinkType', + xmlbase: 'xmlBase', + 'xml:base': 'xmlBase', + xmllang: 'xmlLang', + 'xml:lang': 'xmlLang', + xmlns: 'xmlns', + 'xml:space': 'xmlSpace', + xmlnsxlink: 'xmlnsXlink', + 'xmlns:xlink': 'xmlnsXlink', + xmlspace: 'xmlSpace', + y1: 'y1', + y2: 'y2', + y: 'y', + ychannelselector: 'yChannelSelector', + z: 'z', + zoomandpan: 'zoomAndPan', +} + +export default possibleStandardNames diff --git a/nextjs/packages/next-codemod/lib/install.ts b/nextjs/packages/next-codemod/lib/install.ts new file mode 100644 index 0000000000..f1c814f58d --- /dev/null +++ b/nextjs/packages/next-codemod/lib/install.ts @@ -0,0 +1,109 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import chalk from 'chalk' +import spawn from 'cross-spawn' + +interface InstallArgs { + /** + * Indicate whether to install packages using Yarn. + */ + useYarn: boolean + /** + * Indicate whether there is an active Internet connection. + */ + isOnline: boolean + /** + * Indicate whether the given dependencies are devDependencies. + */ + devDependencies?: boolean +} + +/** + * Spawn a package manager installation with either Yarn or NPM. + * + * @returns A Promise that resolves once the installation is finished. + */ +export function install( + root: string, + dependencies: string[] | null, + { useYarn, isOnline, devDependencies }: InstallArgs +): Promise<void> { + /** + * NPM-specific command-line flags. + */ + const npmFlags: string[] = ['--logLevel', 'error'] + /** + * Yarn-specific command-line flags. + */ + const yarnFlags: string[] = [] + /** + * Return a Promise that resolves once the installation is finished. + */ + return new Promise((resolve, reject) => { + let args: string[] + let command: string = useYarn ? 'yarnpkg' : 'npm' + + if (dependencies && dependencies.length) { + /** + * If there are dependencies, run a variation of `{displayCommand} add`. + */ + if (useYarn) { + /** + * Call `yarn add --exact (--offline)? (-D)? ...`. + */ + args = ['add', '--exact'] + if (!isOnline) args.push('--offline') + args.push('--cwd', root) + if (devDependencies) args.push('--dev') + args.push(...dependencies) + } else { + /** + * Call `npm install [--save|--save-dev] ...`. + */ + args = ['install', '--save-exact'] + args.push(devDependencies ? '--save-dev' : '--save') + args.push(...dependencies) + } + } else { + /** + * If there are no dependencies, run a variation of `{displayCommand} + * install`. + */ + args = ['install'] + if (useYarn) { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log(chalk.yellow('Falling back to the local Yarn cache.')) + console.log() + args.push('--offline') + } + } else { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log() + } + } + } + /** + * Add any package manager-specific flags. + */ + if (useYarn) { + args.push(...yarnFlags) + } else { + args.push(...npmFlags) + } + /** + * Spawn the installation process. + */ + const child = spawn(command, args, { + stdio: 'inherit', + env: { ...process.env, ADBLOCK: '1', DISABLE_OPENCOLLECTIVE: '1' }, + }) + child.on('close', (code) => { + if (code !== 0) { + reject({ command: `${command} ${args.join(' ')}` }) + return + } + resolve() + }) + }) +} diff --git a/nextjs/packages/next-codemod/lib/run-jscodeshift.ts b/nextjs/packages/next-codemod/lib/run-jscodeshift.ts new file mode 100644 index 0000000000..c7a9a29285 --- /dev/null +++ b/nextjs/packages/next-codemod/lib/run-jscodeshift.ts @@ -0,0 +1,19 @@ +// @ts-ignore internal module +import Runner from 'jscodeshift/src/Runner' + +export default function runJscodeshift( + transformerPath: string, + flags: { [key: string]: any }, + files: string[] +) { + // we run jscodeshift in the same process to be able to + // share state between the main CRA transform and sub-transforms + return Runner.run(transformerPath, files, { + ignorePattern: ['**/node_modules/**', '**/.next/**', '**/build/**'], + extensions: 'tsx,ts,jsx,js', + parser: 'tsx', + verbose: 2, + runInBand: true, + ...flags, + }) +} diff --git a/nextjs/packages/next-codemod/license.md b/nextjs/packages/next-codemod/license.md new file mode 100644 index 0000000000..b708f872cb --- /dev/null +++ b/nextjs/packages/next-codemod/license.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Vercel, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nextjs/packages/next-codemod/package.json b/nextjs/packages/next-codemod/package.json new file mode 100644 index 0000000000..500b6a1753 --- /dev/null +++ b/nextjs/packages/next-codemod/package.json @@ -0,0 +1,31 @@ +{ + "private": true, + "name": "@next/codemod", + "version": "11.1.0", + "license": "MIT", + "dependencies": { + "chalk": "4.1.0", + "cheerio": "1.0.0-rc.9", + "execa": "4.0.3", + "globby": "11.0.1", + "inquirer": "7.3.3", + "is-git-clean": "1.1.0", + "jscodeshift": "^0.6.4", + "meow": "7.0.1" + }, + "files": [ + "transforms/*.js", + "bin/*.js", + "lib/**/*.js", + "lib/cra-to-next/gitignore" + ], + "scripts": { + "prepublish": "yarn tsc -d -p tsconfig.json", + "dev": "yarn tsc -d -w -p tsconfig.json", + "test": "jest" + }, + "bin": "./bin/next-codemod.js", + "devDependencies": { + "@types/jscodeshift": "0.11.0" + } +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.input.js new file mode 100644 index 0000000000..f78adb630b --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.input.js @@ -0,0 +1,5 @@ +export default class Home extends React.Component { + render() { + return <div>Hello World</div> + } +} \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.output.js new file mode 100644 index 0000000000..06c84a885d --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/class-component.output.js @@ -0,0 +1,6 @@ +import React from 'react' +export default class Home extends React.Component { + render() { + return <div>Hello World</div> + } +} \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.input.js new file mode 100644 index 0000000000..5ba85aa610 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.input.js @@ -0,0 +1,16 @@ +import { Children, isValidElement } from 'react'; + +function Heading(props) { + const { component, className, children, ...rest } = props; + return React.cloneElement( + component, + { + className: [className, component.props.className || ''].join(' '), + ...rest + }, + children + ); +} + + +export default Heading; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.output.js new file mode 100644 index 0000000000..7b15a0ad4b --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/add-missing-react-import/missing-react-import-in-component.output.js @@ -0,0 +1,16 @@ +import React, { Children, isValidElement } from 'react'; + +function Heading(props) { + const { component, className, children, ...rest } = props; + return React.cloneElement( + component, + { + className: [className, component.props.className || ''].join(' '), + ...rest + }, + children + ); +} + + +export default Heading; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/1-starts-with-number.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/1-starts-with-number.input.js new file mode 100644 index 0000000000..026e0bc4e2 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/1-starts-with-number.input.js @@ -0,0 +1 @@ +export default () => <div>Anonymous function</div>; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/1-starts-with-number.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/1-starts-with-number.output.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-2.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-2.input.js new file mode 100644 index 0000000000..c09b1109d0 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-2.input.js @@ -0,0 +1,11 @@ +class ExistingName2Input { + render() {} +} + +class nested { + render() { + const ExistingName2InputComponent = null; + } +} + +export default () => <div>Anonymous function</div>; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-2.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-2.output.js new file mode 100644 index 0000000000..84ddb2d3ca --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-2.output.js @@ -0,0 +1,13 @@ +class ExistingName2Input { + render() {} +} + +class nested { + render() { + const ExistingName2InputComponent = null; + } +} + +const ExistingName2InputComponent = () => <div>Anonymous function</div>; + +export default ExistingName2InputComponent; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-3.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-3.input.js new file mode 100644 index 0000000000..4c4f34eff6 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-3.input.js @@ -0,0 +1,7 @@ +function ExistingName3Input() {} + +function nested() { + const ExistingName3InputComponent = null; +} + +export default () => <div>Anonymous function</div>; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-3.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-3.output.js new file mode 100644 index 0000000000..4fb06fa35d --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-3.output.js @@ -0,0 +1,9 @@ +function ExistingName3Input() {} + +function nested() { + const ExistingName3InputComponent = null; +} + +const ExistingName3InputComponent = () => <div>Anonymous function</div>; + +export default ExistingName3InputComponent; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-ignore.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-ignore.input.js new file mode 100644 index 0000000000..6a3ad64095 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-ignore.input.js @@ -0,0 +1,4 @@ +const ExistingNameIgnoreInput = null; +const ExistingNameIgnoreInputComponent = null; + +export default () => <div>Anonymous function</div>; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-ignore.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name-ignore.output.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name.input.js new file mode 100644 index 0000000000..53c4840edb --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name.input.js @@ -0,0 +1,7 @@ +const ExistingNameInput = null; + +function nested() { + const ExistingNameInputComponent = null; +} + +export default () => <div>Anonymous function</div>; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name.output.js new file mode 100644 index 0000000000..3725f53ad2 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/existing-name.output.js @@ -0,0 +1,9 @@ +const ExistingNameInput = null; + +function nested() { + const ExistingNameInputComponent = null; +} + +const ExistingNameInputComponent = () => <div>Anonymous function</div>; + +export default ExistingNameInputComponent; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-2.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-2.input.js new file mode 100644 index 0000000000..026e0bc4e2 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-2.input.js @@ -0,0 +1 @@ +export default () => <div>Anonymous function</div>; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-2.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-2.output.js new file mode 100644 index 0000000000..b5ba56fdce --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-2.output.js @@ -0,0 +1,2 @@ +const FunctionComponent2Input = () => <div>Anonymous function</div>; +export default FunctionComponent2Input; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-ignore.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-ignore.input.js new file mode 100644 index 0000000000..32ea5ec533 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-ignore.input.js @@ -0,0 +1,7 @@ +export default () => { + const x = 'y'; + if (true) { + return ''; + } + return null; +}; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-ignore.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component-ignore.output.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component.input.js new file mode 100644 index 0000000000..35898a0fab --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component.input.js @@ -0,0 +1,7 @@ +export default () => { + const x = 'y'; + if (true) { + return <div>Anonymous function</div>; + } + return null; +}; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component.output.js new file mode 100644 index 0000000000..58fc251967 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-component.output.js @@ -0,0 +1,9 @@ +const FunctionComponentInput = () => { + const x = 'y'; + if (true) { + return <div>Anonymous function</div>; + } + return null; +}; + +export default FunctionComponentInput; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression-ignore.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression-ignore.input.js new file mode 100644 index 0000000000..abad3a2b3d --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression-ignore.input.js @@ -0,0 +1,7 @@ +export default function Name() { + const x = 'y'; + if (true) { + return <div>Anonymous function</div>; + } + return null; +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression-ignore.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression-ignore.output.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression.input.js new file mode 100644 index 0000000000..dfccf3900c --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression.input.js @@ -0,0 +1,7 @@ +export default function () { + const x = 'y'; + if (true) { + return <div>Anonymous function</div>; + } + return null; +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression.output.js new file mode 100644 index 0000000000..6a6e7381e7 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/function-expression.output.js @@ -0,0 +1,7 @@ +export default function FunctionExpressionInput() { + const x = 'y'; + if (true) { + return <div>Anonymous function</div>; + } + return null; +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/special-ch@racter.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/special-ch@racter.input.js new file mode 100644 index 0000000000..026e0bc4e2 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/special-ch@racter.input.js @@ -0,0 +1 @@ +export default () => <div>Anonymous function</div>; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/special-ch@racter.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/name-default-component/special-ch@racter.output.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/already-using-withrouter.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/already-using-withrouter.input.js new file mode 100644 index 0000000000..7f50651bec --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/already-using-withrouter.input.js @@ -0,0 +1,7 @@ +import {withRouter} from 'next/router' + +export default withRouter(class extends React.Component { + render() { + const test = this.props.url + } +}) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/already-using-withrouter.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/already-using-withrouter.output.js new file mode 100644 index 0000000000..9d40a584db --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/already-using-withrouter.output.js @@ -0,0 +1,7 @@ +import {withRouter} from 'next/router' + +export default withRouter(class extends React.Component { + render() { + const test = this.props.router + } +}) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/arrow-function-component.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/arrow-function-component.input.js new file mode 100644 index 0000000000..271218c108 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/arrow-function-component.input.js @@ -0,0 +1,3 @@ +export default withAppContainer(withAuth(props => { + const test = props.url +})) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/arrow-function-component.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/arrow-function-component.output.js new file mode 100644 index 0000000000..4fb38acd7c --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/arrow-function-component.output.js @@ -0,0 +1,5 @@ +import { withRouter } from "next/router"; + +export default withRouter(withAppContainer(withAuth(props => { + const test = props.router +}))); \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentdidupdate.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentdidupdate.input.js new file mode 100644 index 0000000000..16ec4c102d --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentdidupdate.input.js @@ -0,0 +1,7 @@ +export default class extends React.Component { + componentDidUpdate(prevProps) { + if (prevProps.url.query.f !== this.props.router.query.f) { + const test = this.props.url + } + } +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentdidupdate.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentdidupdate.output.js new file mode 100644 index 0000000000..44ab887c0c --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentdidupdate.output.js @@ -0,0 +1,9 @@ +import { withRouter } from "next/router"; + +export default withRouter(class extends React.Component { + componentDidUpdate(prevProps) { + if (prevProps.router.query.f !== this.props.router.query.f) { + const test = this.props.router + } + } +}); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentwillreceiveprops.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentwillreceiveprops.input.js new file mode 100644 index 0000000000..f54c76f76d --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentwillreceiveprops.input.js @@ -0,0 +1,7 @@ +export default class extends React.Component { + componentWillReceiveProps(nextProps) { + if (this.props.url.query !== nextProps.url.query) { + const test = this.props.url + } + } +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentwillreceiveprops.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentwillreceiveprops.output.js new file mode 100644 index 0000000000..d01741d953 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/componentwillreceiveprops.output.js @@ -0,0 +1,9 @@ +import { withRouter } from "next/router"; + +export default withRouter(class extends React.Component { + componentWillReceiveProps(nextProps) { + if (this.props.router.query !== nextProps.router.query) { + const test = this.props.router + } + } +}); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-class.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-class.input.js new file mode 100644 index 0000000000..bf1827d95f --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-class.input.js @@ -0,0 +1,7 @@ +export default class Something extends React.Component { + render() { + const {props, stats} = this + + const test = props.url + } +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-class.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-class.output.js new file mode 100644 index 0000000000..c305ef33ce --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-class.output.js @@ -0,0 +1,9 @@ +import { withRouter } from "next/router"; + +export default withRouter(class Something extends React.Component { + render() { + const {props, stats} = this + + const test = props.router + } +}); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props-nested.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props-nested.input.js new file mode 100644 index 0000000000..bddad56b38 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props-nested.input.js @@ -0,0 +1,23 @@ +export default withAppContainer( + withAuth( + class BuyDomains extends React.Component { + render() { + const { url } = this.props + + return ( + <Page> + <Header + user={user} + pathname={url.pathname} + onLogout={() => { + onUser(null) + url.push('/login') + }} + onLogoRightClick={() => url.push('/logos')} + /> + </Page> + ) + } + } + ) +) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props-nested.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props-nested.output.js new file mode 100644 index 0000000000..8ffab0b1e6 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props-nested.output.js @@ -0,0 +1,23 @@ +import { withRouter } from "next/router"; + +export default withRouter(withAppContainer(withAuth( + class BuyDomains extends React.Component { + render() { + const { router } = this.props + + return ( + <Page> + <Header + user={user} + pathname={router.pathname} + onLogout={() => { + onUser(null) + router.push('/login') + }} + onLogoRightClick={() => router.push('/logos')} + /> + </Page> + ); + } + } +))); \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props.input.js new file mode 100644 index 0000000000..576a906512 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props.input.js @@ -0,0 +1,26 @@ +class AddonsPage extends React.Component { + render() { + const { + url + } = this.props + return ( + <Page> + <Header + user={user} + pathname={url.pathname} + onLogout={() => onUser(null)} + onLogoRightClick={() => Router.push('/logos')} + /> + <SubMenu + subscription={subscription} + teamsAndUser={teamsAndUser} + teams={teams} + user={user} + url={url} + /> + </Page> + ) + } +} + +export default withAppContainer(withAuthRequired(withError(AddonsPage))) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props.output.js new file mode 100644 index 0000000000..2bc886769b --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this-props.output.js @@ -0,0 +1,27 @@ +import { withRouter } from "next/router"; +class AddonsPage extends React.Component { + render() { + const { + router + } = this.props + return ( + <Page> + <Header + user={user} + pathname={router.pathname} + onLogout={() => onUser(null)} + onLogoRightClick={() => Router.push('/logos')} + /> + <SubMenu + subscription={subscription} + teamsAndUser={teamsAndUser} + teams={teams} + user={user} + url={router} + /> + </Page> + ); + } +} + +export default withRouter(withAppContainer(withAuthRequired(withError(AddonsPage)))); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this.input.js new file mode 100644 index 0000000000..08f635ac0c --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this.input.js @@ -0,0 +1,7 @@ +export default withApp(withAuth(class Something extends React.Component { + render() { + const {props, stats} = this + + const test = props.url + } +})) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this.output.js new file mode 100644 index 0000000000..85c4a02045 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/destructuring-this.output.js @@ -0,0 +1,9 @@ +import { withRouter } from "next/router"; + +export default withRouter(withApp(withAuth(class Something extends React.Component { + render() { + const {props, stats} = this + + const test = props.router + } +}))); \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable-wrapping.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable-wrapping.input.js new file mode 100644 index 0000000000..e424bc4bff --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable-wrapping.input.js @@ -0,0 +1,7 @@ +class Test extends React.Component { + render() { + const test = this.props.url + } +} + +export default wrappingFunction(Test) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable-wrapping.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable-wrapping.output.js new file mode 100644 index 0000000000..77922797d2 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable-wrapping.output.js @@ -0,0 +1,8 @@ +import { withRouter } from "next/router"; +class Test extends React.Component { + render() { + const test = this.props.router + } +} + +export default withRouter(wrappingFunction(Test)); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable.input.js new file mode 100644 index 0000000000..9d1dda768a --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable.input.js @@ -0,0 +1,7 @@ +class Test extends React.Component { + render() { + const test = this.props.url + } +} + +export default Test diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable.output.js new file mode 100644 index 0000000000..6a358340e6 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/export-default-variable.output.js @@ -0,0 +1,8 @@ +import { withRouter } from "next/router"; +class Test extends React.Component { + render() { + const test = this.props.router + } +} + +export default withRouter(Test); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/first-parameter-hoc.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/first-parameter-hoc.input.js new file mode 100644 index 0000000000..7dedddc889 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/first-parameter-hoc.input.js @@ -0,0 +1,26 @@ +class Plan extends React.Component { + render() { + const { url} = this.props + + return ( + <Page> + <Header + user={user} + pathname={url.pathname} + onLogout={() => onUser(null)} + onLogoRightClick={() => Router.push('/logos')} + /> + + <SubMenu + subscription={subscription} + teamsAndUser={teamsAndUser} + teams={teams} + user={user} + url={url} + /> + </Page> + ) + } +} + +export default withAppContainer(withAuthRequired(Plan, 'signup')) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/first-parameter-hoc.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/first-parameter-hoc.output.js new file mode 100644 index 0000000000..5ebb27eb27 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/first-parameter-hoc.output.js @@ -0,0 +1,27 @@ +import { withRouter } from "next/router"; +class Plan extends React.Component { + render() { + const { router} = this.props + + return ( + <Page> + <Header + user={user} + pathname={router.pathname} + onLogout={() => onUser(null)} + onLogoRightClick={() => Router.push('/logos')} + /> + + <SubMenu + subscription={subscription} + teamsAndUser={teamsAndUser} + teams={teams} + user={user} + url={router} + /> + </Page> + ); + } +} + +export default withRouter(withAppContainer(withAuthRequired(Plan, 'signup'))); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform-method.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform-method.input.js new file mode 100644 index 0000000000..f7fba3acef --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform-method.input.js @@ -0,0 +1,9 @@ +export default withAppContainer( + withAuth( + class BuyDomains extends React.Component { + something = ({url}) => { + + } + } + ) +) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform-method.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform-method.output.js new file mode 100644 index 0000000000..f7fba3acef --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform-method.output.js @@ -0,0 +1,9 @@ +export default withAppContainer( + withAuth( + class BuyDomains extends React.Component { + something = ({url}) => { + + } + } + ) +) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform.input.js new file mode 100644 index 0000000000..5bd292441d --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform.input.js @@ -0,0 +1 @@ +export default class extends React.Component {} \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform.output.js new file mode 100644 index 0000000000..5bd292441d --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/no-transform.output.js @@ -0,0 +1 @@ +export default class extends React.Component {} \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/url-property-not-part-of-this-props.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/url-property-not-part-of-this-props.input.js new file mode 100644 index 0000000000..96e7d77e79 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/url-property-not-part-of-this-props.input.js @@ -0,0 +1,11 @@ +const examples = [{ name: 'ex1', url: 'https://google.fr/' }] + +export default () => ( + <div> + {examples.map(example => ( + <div key={example.name}> + {example.name} - {example.url} + </div> + ))} + </div> +) \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/url-property-not-part-of-this-props.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/url-property-not-part-of-this-props.output.js new file mode 100644 index 0000000000..96e7d77e79 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/url-property-not-part-of-this-props.output.js @@ -0,0 +1,11 @@ +const examples = [{ name: 'ex1', url: 'https://google.fr/' }] + +export default () => ( + <div> + {examples.map(example => ( + <div key={example.name}> + {example.name} - {example.url} + </div> + ))} + </div> +) \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/using-inline-class.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/using-inline-class.input.js new file mode 100644 index 0000000000..2886da4127 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/using-inline-class.input.js @@ -0,0 +1,5 @@ +export default class extends React.Component { + render() { + const test = this.props.url + } +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/using-inline-class.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/using-inline-class.output.js new file mode 100644 index 0000000000..e5887145d0 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/using-inline-class.output.js @@ -0,0 +1,7 @@ +import { withRouter } from "next/router"; + +export default withRouter(class extends React.Component { + render() { + const test = this.props.router + } +}); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/variable-export.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/variable-export.input.js new file mode 100644 index 0000000000..7f3d171adb --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/variable-export.input.js @@ -0,0 +1,7 @@ +const Test = class extends React.Component { + render() { + const test = this.props.url + } +} + +export default abc(wrappingFunction(Test)) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/variable-export.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/variable-export.output.js new file mode 100644 index 0000000000..9d5f37a1ed --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/variable-export.output.js @@ -0,0 +1,8 @@ +import { withRouter } from "next/router"; +const Test = class extends React.Component { + render() { + const test = this.props.router + } +} + +export default withRouter(abc(wrappingFunction(Test))); \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-nested-arrow-function.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-nested-arrow-function.input.js new file mode 100644 index 0000000000..14b733830c --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-nested-arrow-function.input.js @@ -0,0 +1,22 @@ +export default withAppContainer( + withAuth( + class Blog extends React.Component { + render() { + const { props, state } = this + + return ( + <Header + inverse={true} + user={props.user} + pathname={props.url.pathname} + onLogout={() => { + props.onUser(null) + props.url.push('/login') + }} + onLogoRightClick={() => props.url.push('/logos')} + /> + ) + } + } + ) +) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-nested-arrow-function.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-nested-arrow-function.output.js new file mode 100644 index 0000000000..577c51ff7d --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-nested-arrow-function.output.js @@ -0,0 +1,22 @@ +import { withRouter } from "next/router"; + +export default withRouter(withAppContainer(withAuth( + class Blog extends React.Component { + render() { + const { props, state } = this + + return ( + <Header + inverse={true} + user={props.user} + pathname={props.router.pathname} + onLogout={() => { + props.onUser(null) + props.router.push('/login') + }} + onLogoRightClick={() => props.router.push('/logos')} + /> + ); + } + } +))); \ No newline at end of file diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-router-import.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-router-import.input.js new file mode 100644 index 0000000000..dca33d63c0 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-router-import.input.js @@ -0,0 +1,7 @@ +import Router from 'next/router' + +export default class extends React.Component { + render() { + const test = this.props.url + } +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-router-import.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-router-import.output.js new file mode 100644 index 0000000000..8dc44f19e8 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/with-router-import.output.js @@ -0,0 +1,7 @@ +import Router, { withRouter } from 'next/router'; + +export default withRouter(class extends React.Component { + render() { + const test = this.props.router + } +}); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/without-import.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/without-import.input.js new file mode 100644 index 0000000000..2886da4127 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/without-import.input.js @@ -0,0 +1,5 @@ +export default class extends React.Component { + render() { + const test = this.props.url + } +} diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/without-import.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/without-import.output.js new file mode 100644 index 0000000000..e5887145d0 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/without-import.output.js @@ -0,0 +1,7 @@ +import { withRouter } from "next/router"; + +export default withRouter(class extends React.Component { + render() { + const test = this.props.router + } +}); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/wrapping-export.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/wrapping-export.input.js new file mode 100644 index 0000000000..c4ee2580b4 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/wrapping-export.input.js @@ -0,0 +1,5 @@ +export default withSomethingElse(class extends React.Component { + render() { + const test = this.props.url + } +}) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/wrapping-export.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/wrapping-export.output.js new file mode 100644 index 0000000000..c20747a0dd --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/url-to-withrouter/wrapping-export.output.js @@ -0,0 +1,7 @@ +import { withRouter } from "next/router"; + +export default withRouter(withSomethingElse(class extends React.Component { + render() { + const test = this.props.router + } +})); diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-inline.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-inline.input.js new file mode 100644 index 0000000000..8ee8286d05 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-inline.input.js @@ -0,0 +1,5 @@ +import { withAmp, withAmp as alternative } from 'next/amp' + +export default alternative(function Home() { + return <h1>My AMP Page</h1> +}) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-inline.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-inline.output.js new file mode 100644 index 0000000000..1cabea8294 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-inline.output.js @@ -0,0 +1,7 @@ +export default function Home() { + return <h1>My AMP Page</h1> +}; + +export const config = { + amp: true +}; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-dupe.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-dupe.input.js new file mode 100644 index 0000000000..4c5eaf2ed3 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-dupe.input.js @@ -0,0 +1,12 @@ +import { withAmp } from 'next/amp' + +function Home() { + return <h1>My AMP Page</h1> +} + +export const config = { + foo: 'bar', + amp: false +} + +export default withAmp(Home) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-dupe.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-dupe.output.js new file mode 100644 index 0000000000..27b478ca8e --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-dupe.output.js @@ -0,0 +1,10 @@ +function Home() { + return <h1>My AMP Page</h1> +} + +export const config = { + foo: 'bar', + amp: true +} + +export default Home; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-var.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-var.input.js new file mode 100644 index 0000000000..a421ce85c1 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-var.input.js @@ -0,0 +1,14 @@ +import { withAmp } from 'next/amp' + +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +const config = { + foo: 'bar', +} + +export default withAmp(Home) + +export { config } diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-var.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-var.output.js new file mode 100644 index 0000000000..d80bfcfc7f --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config-var.output.js @@ -0,0 +1,13 @@ +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +const config = { + foo: 'bar', + amp: true +} + +export default Home; + +export { config } diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config.input.js new file mode 100644 index 0000000000..d98d63a4e2 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config.input.js @@ -0,0 +1,12 @@ +import { withAmp, useAmp } from 'next/amp' + +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +export const config = { + foo: 'bar', +} + +export default withAmp(Home) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config.output.js new file mode 100644 index 0000000000..1cf6895e61 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp-with-config.output.js @@ -0,0 +1,13 @@ +import { useAmp } from 'next/amp'; + +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +export const config = { + foo: 'bar', + amp: true +} + +export default Home; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp.input.js new file mode 100644 index 0000000000..a9b21f59ba --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp.input.js @@ -0,0 +1,8 @@ +import { withAmp } from 'next/amp' + +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +export default withAmp(Home) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp.output.js new file mode 100644 index 0000000000..cbd7c4690f --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/full-amp.output.js @@ -0,0 +1,10 @@ +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +export default Home; + +export const config = { + amp: true +}; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp-with-config.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp-with-config.input.js new file mode 100644 index 0000000000..e28aad5f86 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp-with-config.input.js @@ -0,0 +1,12 @@ +import { withAmp } from 'next/amp' + +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +export const config = { + foo: 'bar', +} + +export default withAmp(Home, { hybrid: true }) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp-with-config.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp-with-config.output.js new file mode 100644 index 0000000000..901ed4d09a --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp-with-config.output.js @@ -0,0 +1,11 @@ +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +export const config = { + foo: 'bar', + amp: "hybrid" +} + +export default Home; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp.input.js new file mode 100644 index 0000000000..6b70454c97 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp.input.js @@ -0,0 +1,8 @@ +import { withAmp } from 'next/amp' + +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +export default withAmp(Home, { hybrid: true }) diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp.output.js new file mode 100644 index 0000000000..85e2d7e53c --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/hybrid-amp.output.js @@ -0,0 +1,10 @@ +function Home() { + const config = {} + return <h1>My AMP Page</h1> +} + +export default Home; + +export const config = { + amp: "hybrid" +}; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-renamed.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-renamed.input.js new file mode 100644 index 0000000000..36df14997e --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-renamed.input.js @@ -0,0 +1 @@ +import { withAmp as apples } from 'next/amp' diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-renamed.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-renamed.output.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-single.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-single.input.js new file mode 100644 index 0000000000..99c6e3837b --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-single.input.js @@ -0,0 +1 @@ +import { withAmp, useAmp } from 'next/amp' diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-single.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-single.output.js new file mode 100644 index 0000000000..1a971e2d0f --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import-single.output.js @@ -0,0 +1 @@ +import { useAmp } from 'next/amp'; diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import.input.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import.input.js new file mode 100644 index 0000000000..687189ee59 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import.input.js @@ -0,0 +1 @@ +import { withAmp } from 'next/amp' diff --git a/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import.output.js b/nextjs/packages/next-codemod/transforms/__testfixtures__/withamp-to-config/remove-import.output.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nextjs/packages/next-codemod/transforms/__tests__/add-missing-react-import.js b/nextjs/packages/next-codemod/transforms/__tests__/add-missing-react-import.js new file mode 100644 index 0000000000..11342ad49e --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__tests__/add-missing-react-import.js @@ -0,0 +1,16 @@ +/* global jest */ +jest.autoMockOff() +const defineTest = require('jscodeshift/dist/testUtils').defineTest + +const fixtures = [ + 'missing-react-import-in-component' +] + +for (const fixture of fixtures) { + defineTest( + __dirname, + 'add-missing-react-import', + null, + `add-missing-react-import/${fixture}` + ) +} diff --git a/nextjs/packages/next-codemod/transforms/__tests__/name-default-component-test.js b/nextjs/packages/next-codemod/transforms/__tests__/name-default-component-test.js new file mode 100644 index 0000000000..07c32fc636 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__tests__/name-default-component-test.js @@ -0,0 +1,27 @@ +/* global jest */ +jest.autoMockOff() + +const defineTest = require('jscodeshift/dist/testUtils').defineTest + +const fixtures = [ + 'function-component', + 'function-component-2', + 'function-component-ignore', + 'function-expression', + 'function-expression-ignore', + 'existing-name', + 'existing-name-2', + 'existing-name-3', + 'existing-name-ignore', + '1-starts-with-number', + 'special-ch@racter', +] + +fixtures.forEach((test) => + defineTest( + __dirname, + 'name-default-component', + null, + `name-default-component/${test}` + ) +) diff --git a/nextjs/packages/next-codemod/transforms/__tests__/url-to-withrouter.test.js b/nextjs/packages/next-codemod/transforms/__tests__/url-to-withrouter.test.js new file mode 100644 index 0000000000..afd2cc8271 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__tests__/url-to-withrouter.test.js @@ -0,0 +1,35 @@ +/* global jest */ +jest.autoMockOff() +const defineTest = require('jscodeshift/dist/testUtils').defineTest + +const fixtures = [ + 'with-router-import', + 'without-import', + 'already-using-withrouter', + 'using-inline-class', + 'export-default-variable', + 'export-default-variable-wrapping', + 'no-transform', + 'no-transform-method', + 'wrapping-export', + 'variable-export', + 'arrow-function-component', + 'destructuring-this', + 'destructuring-this-class', + 'destructuring-this-props', + 'destructuring-this-props-nested', + 'with-nested-arrow-function', + 'componentdidupdate', + 'componentwillreceiveprops', + 'first-parameter-hoc', + 'url-property-not-part-of-this-props', +] + +for (const fixture of fixtures) { + defineTest( + __dirname, + 'url-to-withrouter', + null, + `url-to-withrouter/${fixture}` + ) +} diff --git a/nextjs/packages/next-codemod/transforms/__tests__/withamp-to-config.test.js b/nextjs/packages/next-codemod/transforms/__tests__/withamp-to-config.test.js new file mode 100644 index 0000000000..5229b56dfd --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/__tests__/withamp-to-config.test.js @@ -0,0 +1,25 @@ +/* global jest */ +jest.autoMockOff() +const defineTest = require('jscodeshift/dist/testUtils').defineTest + +const fixtures = [ + 'remove-import', + 'remove-import-renamed', + 'remove-import-single', + 'full-amp', + 'full-amp-inline', + 'full-amp-with-config', + 'full-amp-with-config-dupe', + 'full-amp-with-config-var', + 'hybrid-amp', + 'hybrid-amp-with-config', +] + +for (const fixture of fixtures) { + defineTest( + __dirname, + 'withamp-to-config', + null, + `withamp-to-config/${fixture}` + ) +} diff --git a/nextjs/packages/next-codemod/transforms/add-missing-react-import.ts b/nextjs/packages/next-codemod/transforms/add-missing-react-import.ts new file mode 100644 index 0000000000..5c43ffa270 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/add-missing-react-import.ts @@ -0,0 +1,83 @@ +import { API, Collection, FileInfo, JSCodeshift, Options } from 'jscodeshift' + +function addReactImport(j: JSCodeshift, root: Collection) { + // We create an import specifier, this is the value of an import, eg: + // import React from 'react' + // The specifier would be `React` + const ReactDefaultSpecifier = j.importDefaultSpecifier(j.identifier('React')) + + // Check if this file is already importing `react` + // so that we can attach `React` to the existing import instead of creating a new `import` node + const originalReactImport = root.find(j.ImportDeclaration, { + source: { + value: 'react', + }, + }) + if (originalReactImport.length > 0) { + // Check if `React` is already imported. In that case we don't have to do anything + if (originalReactImport.find(j.ImportDefaultSpecifier).length > 0) { + return + } + + // Attach `React` to the existing `react` import node + originalReactImport.forEach((node) => { + node.value.specifiers.unshift(ReactDefaultSpecifier) + }) + return + } + + // Create import node + // import React from 'react' + const ReactImport = j.importDeclaration( + [ReactDefaultSpecifier], + j.stringLiteral('react') + ) + + // Find the Program, this is the top level AST node + const Program = root.find(j.Program) + // Attach the import at the top of the body + Program.forEach((node) => { + node.value.body.unshift(ReactImport) + }) +} + +export default function transformer( + file: FileInfo, + api: API, + options: Options +) { + const j = api.jscodeshift + const root = j(file.source) + + const hasReactImport = (r) => { + return ( + r.find(j.ImportDefaultSpecifier, { + local: { + type: 'Identifier', + name: 'React', + }, + }).length > 0 + ) + } + + const hasReactVariableUsage = (r) => { + return ( + r.find(j.MemberExpression, { + object: { + type: 'Identifier', + name: 'React', + }, + }).length > 0 + ) + } + + if (hasReactImport(root)) { + return + } + + if (hasReactVariableUsage(root)) { + addReactImport(j, root) + } + + return root.toSource(options) +} diff --git a/nextjs/packages/next-codemod/transforms/cra-to-next.ts b/nextjs/packages/next-codemod/transforms/cra-to-next.ts new file mode 100644 index 0000000000..7f218fb8d0 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/cra-to-next.ts @@ -0,0 +1,611 @@ +import fs from 'fs' +import path from 'path' +import execa from 'execa' +import globby from 'globby' +import cheerio from 'cheerio' +import { install } from '../lib/install' +import runJscodeshift from '../lib/run-jscodeshift' +import htmlToReactAttributes from '../lib/html-to-react-attributes' +import { indexContext } from '../lib/cra-to-next/index-to-component' +import { globalCssContext } from '../lib/cra-to-next/global-css-transform' + +const feedbackMessage = `Please share any feedback on the migration here: https://github.com/vercel/next.js/discussions/25858` + +// log error and exit without new stacktrace +function fatalMessage(...logs) { + console.error(...logs, `\n${feedbackMessage}`) + process.exit(1) +} + +const craTransformsPath = path.join('../lib/cra-to-next') + +const globalCssTransformPath = require.resolve( + path.join(craTransformsPath, 'global-css-transform.js') +) +const indexTransformPath = require.resolve( + path.join(craTransformsPath, 'index-to-component.js') +) + +class CraTransform { + private appDir: string + private pagesDir: string + private isVite: boolean + private isCra: boolean + private isDryRun: boolean + private indexPage: string + private installClient: string + private shouldLogInfo: boolean + private packageJsonPath: string + private shouldUseTypeScript: boolean + private packageJsonData: { [key: string]: any } + private jscodeShiftFlags: { [key: string]: boolean } + + constructor(files: string[], flags: { [key: string]: boolean }) { + this.isDryRun = flags.dry + this.jscodeShiftFlags = flags + this.appDir = this.validateAppDir(files) + this.packageJsonPath = path.join(this.appDir, 'package.json') + this.packageJsonData = this.loadPackageJson() + this.shouldLogInfo = flags.print || flags.dry + this.pagesDir = this.getPagesDir() + this.installClient = this.checkForYarn() ? 'yarn' : 'npm' + + const { dependencies, devDependencies } = this.packageJsonData + const hasDep = (dep) => dependencies?.[dep] || devDependencies?.[dep] + + this.isCra = hasDep('react-scripts') + this.isVite = !this.isCra && hasDep('vite') + + if (!this.isCra && !this.isVite) { + fatalMessage( + `Error: react-scripts was not detected, is this a CRA project?` + ) + } + + this.shouldUseTypeScript = + fs.existsSync(path.join(this.appDir, 'tsconfig.json')) || + globby.sync('src/**/*.{ts,tsx}', { + cwd: path.join(this.appDir, 'src'), + }).length > 0 + + this.indexPage = globby.sync( + [`${this.isCra ? 'index' : 'main'}.{js,jsx,ts,tsx}`], + { + cwd: path.join(this.appDir, 'src'), + } + )[0] + + if (!this.indexPage) { + fatalMessage('Error: unable to find `src/index`') + } + } + + public async transform() { + console.log('Transforming CRA project at:', this.appDir) + + // convert src/index.js to a react component to render + // inside of Next.js instead of the custom render root + const indexTransformRes = await runJscodeshift( + indexTransformPath, + { ...this.jscodeShiftFlags, silent: true, verbose: 0 }, + [path.join(this.appDir, 'src', this.indexPage)] + ) + + if (indexTransformRes.error > 0) { + fatalMessage( + `Error: failed to apply transforms for src/${this.indexPage}, please check for syntax errors to continue` + ) + } + + if (indexContext.multipleRenderRoots) { + fatalMessage( + `Error: multiple ReactDOM.render roots in src/${this.indexPage}, migrate additional render roots to use portals instead to continue.\n` + + `See here for more info: https://reactjs.org/docs/portals.html` + ) + } + + if (indexContext.nestedRender) { + fatalMessage( + `Error: nested ReactDOM.render found in src/${this.indexPage}, please migrate this to a top-level render (no wrapping functions) to continue` + ) + } + + // comment out global style imports and collect them + // so that we can add them to _app + const globalCssRes = await runJscodeshift( + globalCssTransformPath, + { ...this.jscodeShiftFlags }, + [this.appDir] + ) + + if (globalCssRes.error > 0) { + fatalMessage( + `Error: failed to apply transforms for src/${this.indexPage}, please check for syntax errors to continue` + ) + } + + if (!this.isDryRun) { + await fs.promises.mkdir(path.join(this.appDir, this.pagesDir)) + } + this.logCreate(this.pagesDir) + + if (globalCssContext.reactSvgImports.size > 0) { + // This de-opts webpack 5 since svg/webpack doesn't support webpack 5 yet, + // so we don't support this automatically + fatalMessage( + `Error: import {ReactComponent} from './logo.svg' is not supported, please use normal SVG imports to continue.\n` + + `React SVG imports found in:\n${[ + ...globalCssContext.reactSvgImports, + ].join('\n')}` + ) + } + await this.updatePackageJson() + await this.createNextConfig() + await this.updateGitIgnore() + await this.createPages() + } + + private checkForYarn() { + try { + const userAgent = process.env.npm_config_user_agent + if (userAgent) { + return Boolean(userAgent && userAgent.startsWith('yarn')) + } + execa.sync('yarnpkg', ['--version'], { stdio: 'ignore' }) + return true + } catch (e) { + console.log('error', e) + return false + } + } + + private logCreate(...args: any[]) { + if (this.shouldLogInfo) { + console.log('Created:', ...args) + } + } + + private logModify(...args: any[]) { + if (this.shouldLogInfo) { + console.log('Modified:', ...args) + } + } + + private logInfo(...args: any[]) { + if (this.shouldLogInfo) { + console.log(...args) + } + } + + private async createPages() { + // load public/index.html and add tags to _document + const htmlContent = await fs.promises.readFile( + path.join(this.appDir, `${this.isCra ? 'public/' : ''}index.html`), + 'utf8' + ) + const $ = cheerio.load(htmlContent) + // note: title tag and meta[viewport] needs to be placed in _app + // not _document + const titleTag = $('title')[0] + const metaViewport = $('meta[name="viewport"]')[0] + const headTags = $('head').children() + const bodyTags = $('body').children() + + const pageExt = this.shouldUseTypeScript ? 'tsx' : 'js' + const appPage = path.join(this.pagesDir, `_app.${pageExt}`) + const documentPage = path.join(this.pagesDir, `_document.${pageExt}`) + const catchAllPage = path.join(this.pagesDir, `[[...slug]].${pageExt}`) + + const gatherTextChildren = (children: CheerioElement[]) => { + return children + .map((child) => { + if (child.type === 'text') { + return child.data + } + return '' + }) + .join('') + } + + const serializeAttrs = (attrs: CheerioElement['attribs']) => { + const attrStr = Object.keys(attrs || {}) + .map((name) => { + const reactName = htmlToReactAttributes[name] || name + const value = attrs[name] + + // allow process.env access to work dynamically still + if (value.match(/%([a-zA-Z0-9_]{0,})%/)) { + return `${reactName}={\`${value.replace( + /%([a-zA-Z0-9_]{0,})%/g, + (subStr) => { + return `\${process.env.${subStr.substr(1, subStr.length - 2)}}` + } + )}\`}` + } + return `${reactName}="${value}"` + }) + .join(' ') + + return attrStr.length > 0 ? ` ${attrStr}` : '' + } + const serializedHeadTags: string[] = [] + const serializedBodyTags: string[] = [] + + headTags.map((_index, element) => { + if ( + element.tagName === 'title' || + (element.tagName === 'meta' && element.attribs.name === 'viewport') + ) { + return element + } + let hasChildren = element.children.length > 0 + let serializedAttrs = serializeAttrs(element.attribs) + + if (element.tagName === 'script' || element.tagName === 'style') { + hasChildren = false + serializedAttrs += ` dangerouslySetInnerHTML={{ __html: \`${gatherTextChildren( + element.children + ).replace(/`/g, '\\`')}\` }}` + } + + serializedHeadTags.push( + hasChildren + ? `<${element.tagName}${serializedAttrs}>${gatherTextChildren( + element.children + )}</${element.tagName}>` + : `<${element.tagName}${serializedAttrs} />` + ) + + return element + }) + + bodyTags.map((_index, element) => { + if (element.tagName === 'div' && element.attribs.id === 'root') { + return element + } + let hasChildren = element.children.length > 0 + let serializedAttrs = serializeAttrs(element.attribs) + + if (element.tagName === 'script' || element.tagName === 'style') { + hasChildren = false + serializedAttrs += ` dangerouslySetInnerHTML={{ __html: \`${gatherTextChildren( + element.children + ).replace(/`/g, '\\`')}\` }}` + } + + serializedHeadTags.push( + hasChildren + ? `<${element.tagName}${serializedAttrs}>${gatherTextChildren( + element.children + )}</${element.tagName}>` + : `<${element.tagName}${serializedAttrs} />` + ) + + return element + }) + + if (!this.isDryRun) { + await fs.promises.writeFile( + path.join(this.appDir, appPage), + `${ + globalCssContext.cssImports.size === 0 + ? '' + : [...globalCssContext.cssImports] + .map((file) => { + if (!this.isCra) { + file = file.startsWith('/') ? file.substr(1) : file + } + + return `import '${ + file.startsWith('/') + ? path.relative( + path.join(this.appDir, this.pagesDir), + file + ) + : file + }'` + }) + .join('\n') + '\n' + }${titleTag ? `import Head from 'next/head'` : ''} + +export default function MyApp({ Component, pageProps}) { + ${ + titleTag || metaViewport + ? `return ( + <> + <Head> + ${ + titleTag + ? `<title${serializeAttrs(titleTag.attribs)}>${gatherTextChildren( + titleTag.children + )}` + : '' + } + ${metaViewport ? `` : ''} + + + + + )` + : 'return ' + } +} +` + ) + + await fs.promises.writeFile( + path.join(this.appDir, documentPage), + `import Document, { Html, Head, Main, NextScript } from 'next/document' + +class MyDocument extends Document { + render() { + return ( + + + ${serializedHeadTags.join('\n ')} + + + +
    + + ${serializedBodyTags.join('\n ')} + + + ) + } +} + +export default MyDocument +` + ) + + const relativeIndexPath = path.relative( + path.join(this.appDir, this.pagesDir), + path.join(this.appDir, 'src', this.isCra ? '' : 'main') + ) + + // TODO: should we default to ssr: true below and recommend they + // set it to false if they encounter errors or prefer the more safe + // option to prevent their first start from having any errors? + await fs.promises.writeFile( + path.join(this.appDir, catchAllPage), + `// import NextIndexWrapper from '${relativeIndexPath}' + +// next/dynamic is used to prevent breaking incompatibilities +// with SSR from window.SOME_VAR usage, if this is not used +// next/dynamic can be removed to take advantage of SSR/prerendering +import dynamic from 'next/dynamic' + +// try changing "ssr" to true below to test for incompatibilities, if +// no errors occur the above static import can be used instead and the +// below removed +const NextIndexWrapper = dynamic(() => import('${relativeIndexPath}'), { ssr: false }) + +export default function Page(props) { + return +} +` + ) + } + this.logCreate(appPage) + this.logCreate(documentPage) + this.logCreate(catchAllPage) + } + + private async updatePackageJson() { + // rename react-scripts -> next and react-scripts test -> jest + // add needed dependencies for webpack compatibility + const newDependencies: Array<{ + name: string + version: string + }> = [ + // TODO: do we want to install jest automatically? + { + name: 'next', + version: 'latest', + }, + ] + const packageName = this.isCra ? 'react-scripts' : 'vite' + const packagesToRemove = { + [packageName]: undefined, + } + const neededDependencies: string[] = [] + const { devDependencies, dependencies, scripts } = this.packageJsonData + + for (const dep of newDependencies) { + if (!devDependencies?.[dep.name] && !dependencies?.[dep.name]) { + neededDependencies.push(`${dep.name}@${dep.version}`) + } + } + + this.logInfo( + `Installing ${neededDependencies.join(' ')} with ${this.installClient}` + ) + + if (!this.isDryRun) { + await fs.promises.writeFile( + this.packageJsonPath, + JSON.stringify( + { + ...this.packageJsonData, + scripts: Object.keys(scripts).reduce((prev, cur) => { + const command = scripts[cur] + prev[cur] = command + + if (command === packageName) { + prev[cur] = 'next dev' + } + + if (command.includes(`${packageName} `)) { + prev[cur] = command.replace( + `${packageName} `, + command.includes(`${packageName} test`) ? 'jest ' : 'next ' + ) + } + if (cur === 'eject') { + prev[cur] = undefined + } + // TODO: do we want to map start -> next start instead of CRA's + // default of mapping starting to dev mode? + if (cur === 'start') { + prev[cur] = prev[cur].replace('next start', 'next dev') + prev['start-production'] = 'next start' + } + return prev + }, {} as { [key: string]: string }), + dependencies: { + ...dependencies, + ...packagesToRemove, + }, + devDependencies: { + ...devDependencies, + ...packagesToRemove, + }, + }, + null, + 2 + ) + ) + + await install(this.appDir, neededDependencies, { + useYarn: this.installClient === 'yarn', + // do we want to detect offline as well? they might not + // have next in the local cache already + isOnline: true, + }) + } + } + + private async updateGitIgnore() { + // add Next.js specific items to .gitignore e.g. '.next' + const gitignorePath = path.join(this.appDir, '.gitignore') + let ignoreContent = await fs.promises.readFile(gitignorePath, 'utf8') + const nextIgnores = ( + await fs.promises.readFile( + path.join(path.dirname(globalCssTransformPath), 'gitignore'), + 'utf8' + ) + ).split('\n') + + if (!this.isDryRun) { + for (const ignore of nextIgnores) { + if (!ignoreContent.includes(ignore)) { + ignoreContent += `\n${ignore}` + } + } + + await fs.promises.writeFile(gitignorePath, ignoreContent) + } + this.logModify('.gitignore') + } + + private async createNextConfig() { + if (!this.isDryRun) { + const { proxy, homepage } = this.packageJsonData + const homepagePath = new URL(homepage || '/', 'http://example.com') + .pathname + + await fs.promises.writeFile( + path.join(this.appDir, 'next.config.js'), + `module.exports = {${ + proxy + ? ` + async rewrites() { + return { + fallback: [ + { + source: '/:path*', + destination: '${proxy}' + } + ] + } + },` + : '' + } + env: { + PUBLIC_URL: '${homepagePath === '/' ? '' : homepagePath || ''}' + }, + experimental: { + craCompat: true, + }, + // Remove this to leverage Next.js' static image handling + // read more here: https://nextjs.org/docs/api-reference/next/image + images: { + disableStaticImages: true + } +} +` + ) + } + this.logCreate('next.config.js') + } + + private getPagesDir() { + // prefer src/pages as CRA uses the src dir by default + // and attempt falling back to top-level pages dir + let pagesDir = 'src/pages' + + if (fs.existsSync(path.join(this.appDir, pagesDir))) { + pagesDir = 'pages' + } + + if (fs.existsSync(path.join(this.appDir, pagesDir))) { + fatalMessage( + `Error: a "./pages" directory already exists, please rename to continue` + ) + } + return pagesDir + } + + private loadPackageJson() { + let packageJsonData + + try { + packageJsonData = JSON.parse( + fs.readFileSync(this.packageJsonPath, 'utf8') + ) + } catch (err) { + fatalMessage( + `Error: failed to load package.json from ${this.packageJsonPath}, ensure provided directory is root of CRA project` + ) + } + + return packageJsonData + } + + private validateAppDir(files: string[]) { + if (files.length > 1) { + fatalMessage( + `Error: only one directory should be provided for the cra-to-next transform, received ${files.join( + ', ' + )}` + ) + } + const appDir = path.join(process.cwd(), files[0]) + let isValidDirectory = false + + try { + isValidDirectory = fs.lstatSync(appDir).isDirectory() + } catch (err) { + // not a valid directory + } + + if (!isValidDirectory) { + fatalMessage( + `Error: invalid directory provided for the cra-to-next transform, received ${appDir}` + ) + } + return appDir + } +} + +export default async function transformer(files, flags) { + try { + const craTransform = new CraTransform(files, flags) + await craTransform.transform() + + console.log(`CRA to Next.js migration complete`, `\n${feedbackMessage}`) + } catch (err) { + fatalMessage(`Error: failed to complete transform`, err) + } +} diff --git a/nextjs/packages/next-codemod/transforms/name-default-component.ts b/nextjs/packages/next-codemod/transforms/name-default-component.ts new file mode 100644 index 0000000000..ef81c9a679 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/name-default-component.ts @@ -0,0 +1,103 @@ +import { + API, + ArrowFunctionExpression, + ASTPath, + ExportDefaultDeclaration, + FileInfo, + FunctionDeclaration, + Options, +} from 'jscodeshift' +import { basename, extname } from 'path' + +const camelCase = (value: string): string => { + const val = value.replace(/[-_\s.]+(.)?/g, (_match, chr) => + chr ? chr.toUpperCase() : '' + ) + return val.substr(0, 1).toUpperCase() + val.substr(1) +} + +const isValidIdentifier = (value: string): boolean => + /^[a-zA-ZÀ-ÿ][0-9a-zA-ZÀ-ÿ]+$/.test(value) + +export default function transformer( + file: FileInfo, + api: API, + options: Options +) { + const j = api.jscodeshift + const root = j(file.source) + + let hasModifications: boolean + + const returnsJSX = (node): boolean => + node.type === 'JSXElement' || + (node.type === 'BlockStatement' && + j(node) + .find(j.ReturnStatement) + .some((path) => path.value.argument?.type === 'JSXElement')) + + const hasRootAsParent = (path): boolean => { + const program = path.parentPath.parentPath.parentPath.parentPath.parentPath + return !program || program?.value?.type === 'Program' + } + + const nameFunctionComponent = ( + path: ASTPath + ): void => { + const node = path.value + + if (!node.declaration) { + return + } + + const isArrowFunction = + node.declaration.type === 'ArrowFunctionExpression' && + returnsJSX(node.declaration.body) + const isAnonymousFunction = + node.declaration.type === 'FunctionDeclaration' && !node.declaration.id + + if (!(isArrowFunction || isAnonymousFunction)) { + return + } + + const fileName = basename(file.path, extname(file.path)) + let name = camelCase(fileName) + + // If the generated name looks off, don't add a name + if (!isValidIdentifier(name)) { + return + } + + // Add `Component` to the end of the name if an identifier with the + // same name already exists + while (root.find(j.Identifier, { name }).some(hasRootAsParent)) { + // If the name is still duplicated then don't add a name + if (name.endsWith('Component')) { + return + } + name += 'Component' + } + + hasModifications = true + + if (isArrowFunction) { + path.insertBefore( + j.variableDeclaration('const', [ + j.variableDeclarator( + j.identifier(name), + node.declaration as ArrowFunctionExpression + ), + ]) + ) + + node.declaration = j.identifier(name) + } else { + // Anonymous Function + ;(node.declaration as FunctionDeclaration).id = j.identifier(name) + } + } + + root.find(j.ExportDefaultDeclaration).forEach(nameFunctionComponent) + + return hasModifications ? root.toSource(options) : null +} diff --git a/nextjs/packages/next-codemod/transforms/url-to-withrouter.ts b/nextjs/packages/next-codemod/transforms/url-to-withrouter.ts new file mode 100644 index 0000000000..27f6c702a0 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/url-to-withrouter.ts @@ -0,0 +1,393 @@ +// One-time usage file. You can delete me after running the codemod! + +function addWithRouterImport(j, root) { + // We create an import specifier, this is the value of an import, eg: + // import {withRouter} from 'next/router + // The specifier would be `withRouter` + const withRouterSpecifier = j.importSpecifier(j.identifier('withRouter')) + + // Check if this file is already import `next/router` + // so that we can just attach `withRouter` instead of creating a new `import` node + const originalRouterImport = root.find(j.ImportDeclaration, { + source: { + value: 'next/router', + }, + }) + if (originalRouterImport.length > 0) { + // Check if `withRouter` is already imported. In that case we don't have to do anything + if ( + originalRouterImport.find(j.ImportSpecifier, { + imported: { name: 'withRouter' }, + }).length > 0 + ) { + return + } + + // Attach `withRouter` to the existing `next/router` import node + originalRouterImport.forEach((node) => { + node.value.specifiers.push(withRouterSpecifier) + }) + return + } + + // Create import node + // import {withRouter} from 'next/router' + const withRouterImport = j.importDeclaration( + [withRouterSpecifier], + j.stringLiteral('next/router') + ) + + // Find the Program, this is the top level AST node + const Program = root.find(j.Program) + // Attach the import at the top of the body + Program.forEach((node) => { + node.value.body.unshift(withRouterImport) + }) +} + +function getThisPropsUrlNodes(j, tree) { + return tree.find(j.MemberExpression, { + object: { + type: 'MemberExpression', + object: { type: 'ThisExpression' }, + property: { name: 'props' }, + }, + property: { name: 'url' }, + }) +} + +function getPropsUrlNodes(j, tree, name) { + return tree.find(j.MemberExpression, { + object: { name }, + property: { name: 'url' }, + }) +} + +// Wraps the provided node in a function call +// For example if `functionName` is `withRouter` it will wrap the provided node in `withRouter(NODE_CONTENT)` +function wrapNodeInFunction(j, functionName, args) { + const mappedArgs = args.map((node) => { + // If the node is a ClassDeclaration we have to turn it into a ClassExpression + // since ClassDeclarations can't be wrapped in a function + if (node.type === 'ClassDeclaration') { + node.type = 'ClassExpression' + } + + return node + }) + return j.callExpression(j.identifier(functionName), mappedArgs) +} + +function turnUrlIntoRouter(j, tree) { + tree.find(j.Identifier, { name: 'url' }).replaceWith(j.identifier('router')) +} + +export default function transformer(file, api) { + // j is just a shorthand for the jscodeshift api + const j = api.jscodeshift + // this is the AST root on which we can call methods like `.find` + const root = j(file.source) + + // We search for `export default` + const defaultExports = root.find(j.ExportDefaultDeclaration) + + // We loop over the `export default` instances + // This is just how jscodeshift works, there can only be one export default instance + defaultExports.forEach((rule) => { + // rule.value is an AST node + const { value: node } = rule + // declaration holds the AST node for what comes after `export default` + const { declaration } = node + + function wrapDefaultExportInWithRouter() { + if ( + j(rule).find(j.CallExpression, { callee: { name: 'withRouter' } }) + .length > 0 + ) { + return + } + j(rule).replaceWith( + j.exportDefaultDeclaration( + wrapNodeInFunction(j, 'withRouter', [declaration]) + ) + ) + } + + // The `Identifier` type is given in this case: + // export default Test + // where `Test` is the identifier + if (declaration.type === 'Identifier') { + // the variable name + const { name } = declaration + + // find the implementation of the variable, can be a class, function, etc + let implementation = root.find(j.Declaration, { id: { name } }) + if (implementation.length === 0) { + implementation = root.find(j.VariableDeclarator, { id: { name } }) + } + + implementation + .find(j.Property, { key: { name: 'url' } }) + .forEach((propertyRule) => { + const isThisPropsDestructure = j(propertyRule).closest( + j.VariableDeclarator, + { + init: { + object: { + type: 'ThisExpression', + }, + property: { name: 'props' }, + }, + } + ) + if (isThisPropsDestructure.length === 0) { + return + } + const originalKeyValue = propertyRule.value.value.name + propertyRule.value.key.name = 'router' + wrapDefaultExportInWithRouter() + addWithRouterImport(j, root) + // If the property is reassigned to another variable we don't have to transform it + if (originalKeyValue !== 'url') { + return + } + + propertyRule.value.value.name = 'router' + j(propertyRule) + .closest(j.BlockStatement) + .find(j.Identifier, (identifierNode) => { + if (identifierNode.type === 'JSXIdentifier') { + return false + } + + if (identifierNode.name !== 'url') { + return false + } + + return true + }) + .replaceWith(j.identifier('router')) + }) + + // Find usage of `this.props.url` + const thisPropsUrlUsage = getThisPropsUrlNodes(j, implementation) + + if (thisPropsUrlUsage.length === 0) { + return + } + + // rename `url` to `router` + turnUrlIntoRouter(j, thisPropsUrlUsage) + wrapDefaultExportInWithRouter() + addWithRouterImport(j, root) + return + } + + const arrowFunctions = j(rule).find(j.ArrowFunctionExpression) + ;(() => { + if (arrowFunctions.length === 0) { + return + } + + arrowFunctions.forEach((r) => { + // This makes sure we don't match nested functions, only the top one + if (j(r).closest(j.Expression).length !== 0) { + return + } + + if (!r.value.params || !r.value.params[0]) { + return + } + + const name = r.value.params[0].name + const propsUrlUsage = getPropsUrlNodes(j, j(r), name) + if (propsUrlUsage.length === 0) { + return + } + + turnUrlIntoRouter(j, propsUrlUsage) + wrapDefaultExportInWithRouter() + addWithRouterImport(j, root) + }) + return + })() + + if (declaration.type === 'CallExpression') { + j(rule) + .find(j.CallExpression, (haystack) => { + const firstArgument = haystack.arguments[0] || {} + if (firstArgument.type === 'Identifier') { + return true + } + + return false + }) + .forEach((callRule) => { + const { name } = callRule.value.arguments[0] + + // find the implementation of the variable, can be a class, function, etc + let implementation = root.find(j.Declaration, { id: { name } }) + if (implementation.length === 0) { + implementation = root.find(j.VariableDeclarator, { id: { name } }) + } + // Find usage of `this.props.url` + const thisPropsUrlUsage = getThisPropsUrlNodes(j, implementation) + + implementation + .find(j.Property, { key: { name: 'url' } }) + .forEach((propertyRule) => { + const isThisPropsDestructure = j(propertyRule).closest( + j.VariableDeclarator, + { + init: { + object: { + type: 'ThisExpression', + }, + property: { name: 'props' }, + }, + } + ) + if (isThisPropsDestructure.length === 0) { + return + } + const originalKeyValue = propertyRule.value.value.name + propertyRule.value.key.name = 'router' + wrapDefaultExportInWithRouter() + addWithRouterImport(j, root) + // If the property is reassigned to another variable we don't have to transform it + if (originalKeyValue !== 'url') { + return + } + + propertyRule.value.value.name = 'router' + j(propertyRule) + .closest(j.BlockStatement) + .find(j.Identifier, (identifierNode) => { + if (identifierNode.type === 'JSXIdentifier') { + return false + } + + if (identifierNode.name !== 'url') { + return false + } + + return true + }) + .replaceWith(j.identifier('router')) + }) + + if (thisPropsUrlUsage.length === 0) { + return + } + + // rename `url` to `router` + turnUrlIntoRouter(j, thisPropsUrlUsage) + wrapDefaultExportInWithRouter() + addWithRouterImport(j, root) + return + }) + } + + j(rule) + .find(j.Property, { key: { name: 'url' } }) + .forEach((propertyRule) => { + const isThisPropsDestructure = j(propertyRule).closest( + j.VariableDeclarator, + { + init: { + object: { + type: 'ThisExpression', + }, + property: { name: 'props' }, + }, + } + ) + if (isThisPropsDestructure.length === 0) { + return + } + const originalKeyValue = propertyRule.value.value.name + propertyRule.value.key.name = 'router' + wrapDefaultExportInWithRouter() + addWithRouterImport(j, root) + // If the property is reassigned to another variable we don't have to transform it + if (originalKeyValue !== 'url') { + return + } + + propertyRule.value.value.name = 'router' + j(propertyRule) + .closest(j.BlockStatement) + .find(j.Identifier, (identifierNode) => { + if (identifierNode.type === 'JSXIdentifier') { + return false + } + + if (identifierNode.name !== 'url') { + return false + } + + return true + }) + .replaceWith(j.identifier('router')) + }) + + j(rule) + .find(j.MethodDefinition, { key: { name: 'componentWillReceiveProps' } }) + .forEach((methodRule) => { + const func = methodRule.value.value + if (!func.params[0]) { + return + } + const firstArgumentName = func.params[0].name + const propsUrlUsage = getPropsUrlNodes( + j, + j(methodRule), + firstArgumentName + ) + turnUrlIntoRouter(j, propsUrlUsage) + if (propsUrlUsage.length === 0) { + return + } + wrapDefaultExportInWithRouter() + addWithRouterImport(j, root) + }) + + j(rule) + .find(j.MethodDefinition, { key: { name: 'componentDidUpdate' } }) + .forEach((methodRule) => { + const func = methodRule.value.value + if (!func.params[0]) { + return + } + const firstArgumentName = func.params[0].name + const propsUrlUsage = getPropsUrlNodes( + j, + j(methodRule), + firstArgumentName + ) + turnUrlIntoRouter(j, propsUrlUsage) + if (propsUrlUsage.length === 0) { + return + } + wrapDefaultExportInWithRouter() + addWithRouterImport(j, root) + }) + + const thisPropsUrlUsage = getThisPropsUrlNodes(j, j(rule)) + const propsUrlUsage = getPropsUrlNodes(j, j(rule), 'props') + + // rename `url` to `router` + turnUrlIntoRouter(j, thisPropsUrlUsage) + turnUrlIntoRouter(j, propsUrlUsage) + + if (thisPropsUrlUsage.length === 0 && propsUrlUsage.length === 0) { + return + } + + wrapDefaultExportInWithRouter() + addWithRouterImport(j, root) + return + }) + + return root.toSource() +} diff --git a/nextjs/packages/next-codemod/transforms/withamp-to-config.ts b/nextjs/packages/next-codemod/transforms/withamp-to-config.ts new file mode 100644 index 0000000000..9bd6f3ea38 --- /dev/null +++ b/nextjs/packages/next-codemod/transforms/withamp-to-config.ts @@ -0,0 +1,175 @@ +// One-time usage file. You can delete me after running the codemod! + +function injectAmp(j, o, desiredAmpValue) { + const init = o.node.init + + switch (init.type) { + case 'ObjectExpression': { + const overwroteAmpKey = init.properties.some((prop) => { + switch (prop.type) { + case 'Property': + case 'ObjectProperty': + if (!(prop.key.type === 'Identifier' && prop.key.name === 'amp')) { + return false + } + + prop.value = desiredAmpValue + return true + default: + return false + } + }) + + if (!overwroteAmpKey) { + init.properties.push( + j.objectProperty(j.identifier('amp'), desiredAmpValue) + ) + } + + return true + } + default: { + return false + } + } +} + +export default function transformer(file, api) { + const j = api.jscodeshift + const root = j(file.source) + const done = () => root.toSource() + + const imports = root.find(j.ImportDeclaration, { + source: { value: 'next/amp' }, + }) + + if (imports.length < 1) { + return + } + + let hadWithAmp = false + const ampImportNames = [] + + imports.forEach((ampImport) => { + const ampImportShift = j(ampImport) + + const withAmpImport = ampImportShift.find(j.ImportSpecifier, { + imported: { name: 'withAmp' }, + }) + + if (withAmpImport.length < 1) { + return + } + + hadWithAmp = true + withAmpImport.forEach((element) => { + ampImportNames.push(element.value.local.name) + j(element).remove() + }) + + if (ampImport.value.specifiers.length === 0) { + ampImportShift.remove() + } + }) + + if (!hadWithAmp) { + return done() + } + + const defaultExportsShift = root.find(j.ExportDefaultDeclaration) + if (defaultExportsShift.length < 1) { + return done() + } + + let desiredAmpValue = j.booleanLiteral(true) + + const defaultExport = defaultExportsShift.nodes()[0] + const removedWrapper = ampImportNames.some((ampImportName) => { + const ampWrapping = j(defaultExport).find(j.CallExpression, { + callee: { name: ampImportName }, + }) + + if (ampWrapping.length < 1) { + return false + } + + ampWrapping.forEach((e) => { + if (e.value.arguments.length < 1) { + j(e).remove() + } else { + const withAmpOptions = e.value.arguments[1] + if (withAmpOptions && withAmpOptions.type === 'ObjectExpression') { + const isHybrid = withAmpOptions.properties.some((prop) => { + if (!(prop.type === 'Property' || prop.type === 'ObjectProperty')) { + return false + } + + if (!(prop.key && prop.key.name === 'hybrid')) { + return false + } + + return ( + (prop.value.type === 'Literal' || + prop.value.type === 'BooleanLiteral') && + prop.value.value === true + ) + }) + + if (isHybrid) { + desiredAmpValue = j.stringLiteral('hybrid') + } + } + + j(e).replaceWith(e.value.arguments[0]) + } + }) + return true + }) + + if (!removedWrapper) { + return done() + } + + const namedExportsShift = root.find(j.ExportNamedDeclaration) + const hadExistingConfig = namedExportsShift.some((namedExport) => { + const configExportedObject = j(namedExport).find(j.VariableDeclarator, { + id: { name: 'config' }, + }) + if (configExportedObject.length > 0) { + return configExportedObject.some((exportedObject) => + injectAmp(j, exportedObject, desiredAmpValue) + ) + } + + const configReexported = j(namedExport).find(j.ExportSpecifier, { + local: { name: 'config' }, + }) + if (configReexported.length > 0) { + const configObjects = root + .findVariableDeclarators('config') + .filter((el) => el.scope.isGlobal) + return configObjects.some((configObject) => + injectAmp(j, configObject, desiredAmpValue) + ) + } + + return false + }) + + if (!hadExistingConfig) { + defaultExportsShift.insertAfter( + j.exportNamedDeclaration( + j.variableDeclaration('const', [ + j.variableDeclarator( + j.identifier('config'), + j.objectExpression([ + j.objectProperty(j.identifier('amp'), desiredAmpValue), + ]) + ), + ]) + ) + ) + } + + return done() +} diff --git a/nextjs/packages/next-codemod/tsconfig.json b/nextjs/packages/next-codemod/tsconfig.json new file mode 100644 index 0000000000..e206315fc9 --- /dev/null +++ b/nextjs/packages/next-codemod/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "skipLibCheck": true, + "module": "commonjs", + "sourceMap": true, + "esModuleInterop": true, + "target": "es2015", + "downlevelIteration": true, + "preserveWatchOutput": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/nextjs/packages/next-env/.gitignore b/nextjs/packages/next-env/.gitignore new file mode 100644 index 0000000000..fbf06224c7 --- /dev/null +++ b/nextjs/packages/next-env/.gitignore @@ -0,0 +1 @@ +types \ No newline at end of file diff --git a/nextjs/packages/next-env/README.md b/nextjs/packages/next-env/README.md new file mode 100644 index 0000000000..af60128ea7 --- /dev/null +++ b/nextjs/packages/next-env/README.md @@ -0,0 +1,3 @@ +# `@blitzjs/env` + +Next.js' util for loading dotenv files in with the proper priorities diff --git a/nextjs/packages/next-env/index.ts b/nextjs/packages/next-env/index.ts new file mode 100644 index 0000000000..c2e0f9fd50 --- /dev/null +++ b/nextjs/packages/next-env/index.ts @@ -0,0 +1,119 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import * as fs from 'fs' +import * as path from 'path' +import * as dotenv from 'dotenv' +import dotenvExpand from 'dotenv-expand' + +export type Env = { [key: string]: string } +export type LoadedEnvFiles = Array<{ + path: string + contents: string +}> + +let combinedEnv: Env | undefined = undefined +let cachedLoadedEnvFiles: LoadedEnvFiles = [] + +type Log = { + info: (...args: any[]) => void + error: (...args: any[]) => void +} + +export function processEnv( + loadedEnvFiles: LoadedEnvFiles, + dir?: string, + log: Log = console +) { + // don't reload env if we already have since this breaks escaped + // environment values e.g. \$ENV_FILE_KEY + if (process.env.__NEXT_PROCESSED_ENV || loadedEnvFiles.length === 0) { + return process.env as Env + } + // flag that we processed the environment values in case a serverless + // function is re-used or we are running in `next start` mode + process.env.__NEXT_PROCESSED_ENV = 'true' + + const origEnv = Object.assign({}, process.env) + const parsed: dotenv.DotenvParseOutput = {} + + for (const envFile of loadedEnvFiles) { + try { + let result: dotenv.DotenvConfigOutput = {} + result.parsed = dotenv.parse(envFile.contents) + + result = dotenvExpand(result) + + if (result.parsed) { + log.info(`Loaded env from ${path.join(dir || '', envFile.path)}`) + } + + for (const key of Object.keys(result.parsed || {})) { + if ( + typeof parsed[key] === 'undefined' && + typeof origEnv[key] === 'undefined' + ) { + parsed[key] = result.parsed?.[key]! + } + } + } catch (err) { + log.error( + `Failed to load env from ${path.join(dir || '', envFile.path)}`, + err + ) + } + } + + return Object.assign(process.env, parsed) +} + +export function loadEnvConfig( + dir: string = process.cwd(), + _dev?: boolean, + log: Log = console, + { ignoreCache } = { ignoreCache: false } +): { + combinedEnv: Env + loadedEnvFiles: LoadedEnvFiles +} { + // don't reload env if we already have since this breaks escaped + // environment values e.g. \$ENV_FILE_KEY + if (combinedEnv && !ignoreCache) + return { combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles } + + const appEnv = process.env.APP_ENV ?? process.env.NODE_ENV ?? 'development' + + let dotenvFiles = [ + `.env.${appEnv}.local`, + `.env.${appEnv}`, + // Don't include `.env.local` for `test` environment + // since normally you expect tests to produce the same + // results for everyone + appEnv !== 'test' && `.env.local`, + '.env', + ].filter(Boolean) as string[] + + for (const envFile of dotenvFiles) { + // only load .env if the user provided has an env config file + const dotEnvPath = path.join(dir, envFile) + + try { + const stats = fs.statSync(dotEnvPath) + + // make sure to only attempt to read files + if (!stats.isFile()) { + continue + } + + const contents = fs.readFileSync(dotEnvPath, 'utf8') + cachedLoadedEnvFiles.push({ + path: envFile, + contents, + }) + } catch (err: any) { + if (err.code !== 'ENOENT') { + log.error(`Failed to load env from ${envFile}`, err) + } + } + } + combinedEnv = processEnv(cachedLoadedEnvFiles, dir, log) + return { combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles } +} diff --git a/nextjs/packages/next-env/package.json b/nextjs/packages/next-env/package.json new file mode 100644 index 0000000000..eebee56133 --- /dev/null +++ b/nextjs/packages/next-env/package.json @@ -0,0 +1,37 @@ +{ + "name": "@blitzjs/env", + "version": "0.45.4", + "keywords": [ + "react", + "next", + "next.js", + "dotenv" + ], + "description": "Next.js dotenv file loading", + "repository": { + "type": "git", + "url": "https://github.com/vercel/next.js", + "directory": "packages/next-env" + }, + "author": "Blitz.js", + "license": "MIT", + "main": "dist/index.js", + "types": "types/index.d.ts", + "files": [ + "dist", + "types" + ], + "scripts": { + "dev": "ncc build ./index.ts -w -o dist/", + "prerelease": "rimraf ./dist/", + "types": "tsc index.ts --declaration --emitDeclarationOnly --declarationDir types --esModuleInterop --skipLibCheck", + "release": "ncc build ./index.ts -o ./dist/ --minify --no-cache --no-source-map-register", + "prepublish": "yarn release && yarn types" + }, + "devDependencies": { + "@types/dotenv": "8.2.0", + "@zeit/ncc": "0.20.4", + "dotenv": "8.2.0", + "dotenv-expand": "^5.1.0" + } +} diff --git a/nextjs/packages/next-env/tsconfig.json b/nextjs/packages/next-env/tsconfig.json new file mode 100644 index 0000000000..d0e9e223e6 --- /dev/null +++ b/nextjs/packages/next-env/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2015", + "moduleResolution": "node", + "strict": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": false + } +} diff --git a/nextjs/packages/next-mdx/index.js b/nextjs/packages/next-mdx/index.js new file mode 100644 index 0000000000..27f9f66290 --- /dev/null +++ b/nextjs/packages/next-mdx/index.js @@ -0,0 +1,24 @@ +module.exports = (pluginOptions = {}) => (nextConfig = {}) => { + const extension = pluginOptions.extension || /\.mdx$/ + + return Object.assign({}, nextConfig, { + webpack(config, options) { + config.module.rules.push({ + test: extension, + use: [ + options.defaultLoaders.babel, + { + loader: require.resolve('@mdx-js/loader'), + options: pluginOptions.options, + }, + ], + }) + + if (typeof nextConfig.webpack === 'function') { + return nextConfig.webpack(config, options) + } + + return config + }, + }) +} diff --git a/nextjs/packages/next-mdx/license.md b/nextjs/packages/next-mdx/license.md new file mode 100644 index 0000000000..b708f872cb --- /dev/null +++ b/nextjs/packages/next-mdx/license.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Vercel, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nextjs/packages/next-mdx/package.json b/nextjs/packages/next-mdx/package.json new file mode 100644 index 0000000000..340c1aaf05 --- /dev/null +++ b/nextjs/packages/next-mdx/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "name": "@next/mdx", + "version": "11.1.0", + "main": "index.js", + "license": "MIT", + "repository": { + "url": "vercel/next.js", + "directory": "packages/next-mdx" + }, + "peerDependencies": { + "@mdx-js/loader": ">=0.15.0", + "@mdx-js/react": "*" + } +} diff --git a/nextjs/packages/next-mdx/readme.md b/nextjs/packages/next-mdx/readme.md new file mode 100644 index 0000000000..12f0020ca8 --- /dev/null +++ b/nextjs/packages/next-mdx/readme.md @@ -0,0 +1,78 @@ +# Next.js + MDX + +Use [MDX](https://github.com/mdx-js/mdx) with [Next.js](https://github.com/vercel/next.js) + +## Installation + +``` +npm install @next/mdx @mdx-js/loader +``` + +or + +``` +yarn add @next/mdx @mdx-js/loader +``` + +## Usage + +Create a `next.config.js` in your project + +```js +// next.config.js +const withMDX = require('@next/mdx')() +module.exports = withMDX() +``` + +Optionally you can provide [MDX plugins](https://mdxjs.com/advanced/plugins#plugins): + +```js +// next.config.js +const withMDX = require('@next/mdx')({ + options: { + remarkPlugins: [], + rehypePlugins: [], + }, +}) +module.exports = withMDX() +``` + +Optionally you can add your custom Next.js configuration as parameter + +```js +// next.config.js +const withMDX = require('@next/mdx')() +module.exports = withMDX({ + webpack(config, options) { + return config + }, +}) +``` + +Optionally you can match other file extensions for MDX compilation, by default only `.mdx` is supported + +```js +// next.config.js +const withMDX = require('@next/mdx')({ + extension: /\.(md|mdx)$/, +}) +module.exports = withMDX() +``` + +## Top level .mdx pages + +Define the `pageExtensions` option to have Next.js handle `.md` and `.mdx` files in the `pages` directory as pages: + +```js +// next.config.js +const withMDX = require('@next/mdx')({ + extension: /\.mdx?$/, +}) +module.exports = withMDX({ + pageExtensions: ['js', 'jsx', 'md', 'mdx'], +}) +``` + +## TypeScript + +Follow [this guide](https://mdxjs.com/advanced/typescript) from the MDX docs. diff --git a/nextjs/packages/next-plugin-storybook/package.json b/nextjs/packages/next-plugin-storybook/package.json new file mode 100644 index 0000000000..6062fd92f4 --- /dev/null +++ b/nextjs/packages/next-plugin-storybook/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "name": "@next/plugin-storybook", + "version": "11.1.0", + "repository": { + "url": "vercel/next.js", + "directory": "packages/next-plugin-storybook" + }, + "peerDependencies": { + "next": "*" + } +} diff --git a/nextjs/packages/next-plugin-storybook/preset.js b/nextjs/packages/next-plugin-storybook/preset.js new file mode 100644 index 0000000000..8a70445c8d --- /dev/null +++ b/nextjs/packages/next-plugin-storybook/preset.js @@ -0,0 +1,68 @@ +const { PHASE_PRODUCTION_BUILD } = require('next/constants') +const { findPagesDir } = require('next/dist/lib/find-pages-dir') +const loadConfig = require('next/dist/server/config').default +const getWebpackConfig = require('next/dist/build/webpack-config').default + +const CWD = process.cwd() + +async function webpackFinal(config) { + const pagesDir = findPagesDir(CWD) + const nextConfig = await loadConfig(PHASE_PRODUCTION_BUILD, CWD) + const nextWebpackConfig = await getWebpackConfig(CWD, { + pagesDir, + entrypoints: {}, + isServer: false, + target: 'server', + config: nextConfig, + buildId: 'storybook', + rewrites: { beforeFiles: [], afterFiles: [], fallback: [] }, + }) + + config.plugins = [...config.plugins, ...nextWebpackConfig.plugins] + + config.resolve = { + ...config.resolve, + ...nextWebpackConfig.resolve, + } + + config.module.rules = [ + ...filterModuleRules(config), + ...nextWebpackConfig.module.rules.map((rule) => { + // we need to resolve next-babel-loader since it's not available + // relative with storybook's config + if (rule.use && rule.use.loader === 'next-babel-loader') { + rule.use.loader = require.resolve( + 'next/dist/build/webpack/loaders/next-babel-loader' + ) + } + return rule + }), + ] + + return config +} + +function filterModuleRules(config) { + return config.module.rules.filter((rule) => { + // the rules we're filtering use RegExp for the test + if (!(rule.test instanceof RegExp)) return true + // use Next.js' built-in CSS + if (rule.test.test('hello.css')) { + return false + } + // use next-babel-loader instead of storybook's babel-loader + if ( + rule.test.test('hello.js') && + Array.isArray(rule.use) && + rule.use[0].loader === 'babel-loader' + ) { + return false + } + return true + }) +} + +module.exports = { + webpackFinal, + filterModuleRules, +} diff --git a/nextjs/packages/next-plugin-storybook/readme.md b/nextjs/packages/next-plugin-storybook/readme.md new file mode 100644 index 0000000000..cdf5aadbe6 --- /dev/null +++ b/nextjs/packages/next-plugin-storybook/readme.md @@ -0,0 +1,3 @@ +# Unstable @next/plugin-storybook + +This package is still very experimental and should not be used at this point diff --git a/nextjs/packages/next-polyfill-module/package.json b/nextjs/packages/next-polyfill-module/package.json new file mode 100644 index 0000000000..917524d642 --- /dev/null +++ b/nextjs/packages/next-polyfill-module/package.json @@ -0,0 +1,19 @@ +{ + "private": true, + "name": "@next/polyfill-module", + "version": "11.1.0", + "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", + "main": "dist/polyfill-module.js", + "license": "MIT", + "repository": { + "url": "vercel/next.js", + "directory": "packages/next-polyfill-module" + }, + "scripts": { + "prepublish": "microbundle -i src/index.js -o dist/polyfill-module.js -f iife --no-sourcemap --external none --no-pkg-main", + "build": "microbundle watch -i src/index.js -o dist/polyfill-module.js -f iife --no-sourcemap --external none --no-pkg-main" + }, + "devDependencies": { + "microbundle": "0.13.0" + } +} diff --git a/nextjs/packages/next-polyfill-module/src/index.js b/nextjs/packages/next-polyfill-module/src/index.js new file mode 100644 index 0000000000..73b6c052f0 --- /dev/null +++ b/nextjs/packages/next-polyfill-module/src/index.js @@ -0,0 +1,96 @@ +/* eslint-disable no-extend-native */ + +// Contains polyfills for methods missing after browser version(s): +// Edge 16, Firefox 60, Chrome 61, Safari 10.1 + +/** + * Available in: + * Edge: never + * Firefox: 61 + * Chrome: 66 + * Safari: 12 + * + * https://caniuse.com/mdn-javascript_builtins_string_trimstart + * https://caniuse.com/mdn-javascript_builtins_string_trimend + */ +if (!('trimStart' in String.prototype)) { + String.prototype.trimStart = String.prototype.trimLeft +} +if (!('trimEnd' in String.prototype)) { + String.prototype.trimEnd = String.prototype.trimRight +} + +/** + * Available in: + * Edge: never + * Firefox: 63 + * Chrome: 70 + * Safari: 12.1 + * + * https://caniuse.com/mdn-javascript_builtins_symbol_description + */ +if (!('description' in Symbol.prototype)) { + Object.defineProperty(Symbol.prototype, 'description', { + configurable: true, + get: function get() { + var m = /\((.*)\)/.exec(this.toString()) + return m ? m[1] : undefined + }, + }) +} + +/** + * Available in: + * Edge: never + * Firefox: 62 + * Chrome: 69 + * Safari: 12 + * + * https://caniuse.com/array-flat + */ +// Copied from https://gist.github.com/developit/50364079cf0390a73e745e513fa912d9 +// Licensed Apache-2.0 +if (!Array.prototype.flat) { + Array.prototype.flat = function flat(d, c) { + return ( + (c = this.concat.apply([], this)), + d > 1 && c.some(Array.isArray) ? c.flat(d - 1) : c + ) + } + Array.prototype.flatMap = function (c, a) { + return this.map(c, a).flat() + } +} + +/** + * Available in: + * Edge: 18 + * Firefox: 58 + * Chrome: 63 + * Safari: 11.1 + * + * https://caniuse.com/promise-finally + */ +// Modified from https://gist.github.com/developit/e96097d9b657f2a2f3e588ffde433437 +// Licensed Apache-2.0 +if (!Promise.prototype.finally) { + Promise.prototype.finally = function (callback) { + if (typeof callback !== 'function') { + return this.then(callback, callback) + } + + var P = this.constructor || Promise + return this.then( + function (value) { + return P.resolve(callback()).then(function () { + return value + }) + }, + function (err) { + return P.resolve(callback()).then(function () { + throw err + }) + } + ) + } +} diff --git a/nextjs/packages/next-polyfill-nomodule/package.json b/nextjs/packages/next-polyfill-nomodule/package.json new file mode 100644 index 0000000000..6bb0f5cee9 --- /dev/null +++ b/nextjs/packages/next-polyfill-nomodule/package.json @@ -0,0 +1,22 @@ +{ + "private": true, + "name": "@next/polyfill-nomodule", + "version": "11.1.0", + "description": "A polyfill for non-dead, nomodule browsers.", + "main": "dist/polyfill-nomodule.js", + "license": "MIT", + "repository": { + "url": "vercel/next.js", + "directory": "packages/next-polyfill-nomodule" + }, + "scripts": { + "prepublish": "microbundle -i src/index.js -o dist/polyfill-nomodule.js -f iife --no-sourcemap --external none --no-pkg-main", + "build": "microbundle watch -i src/index.js -o dist/polyfill-nomodule.js -f iife --no-sourcemap --external none --no-pkg-main" + }, + "devDependencies": { + "core-js": "3.6.5", + "microbundle": "0.13.0", + "object-assign": "4.1.1", + "whatwg-fetch": "3.0.0" + } +} diff --git a/nextjs/packages/next-polyfill-nomodule/src/index.js b/nextjs/packages/next-polyfill-nomodule/src/index.js new file mode 100644 index 0000000000..df37098a1f --- /dev/null +++ b/nextjs/packages/next-polyfill-nomodule/src/index.js @@ -0,0 +1,59 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import 'core-js/features/array/copy-within' +import 'core-js/features/array/fill' +import 'core-js/features/array/find' +import 'core-js/features/array/find-index' +import 'core-js/features/array/flat-map' +import 'core-js/features/array/flat' +import 'core-js/features/array/from' +import 'core-js/features/array/includes' +import 'core-js/features/array/iterator' +import 'core-js/features/array/of' +import 'core-js/features/function/has-instance' +import 'core-js/features/function/name' +import 'core-js/features/map' +import 'core-js/features/number/constructor' +import 'core-js/features/number/epsilon' +import 'core-js/features/number/is-finite' +import 'core-js/features/number/is-integer' +import 'core-js/features/number/is-nan' +import 'core-js/features/number/is-safe-integer' +import 'core-js/features/number/max-safe-integer' +import 'core-js/features/number/min-safe-integer' +import 'core-js/features/number/parse-float' +import 'core-js/features/number/parse-int' +import 'core-js/features/object/entries' +import 'core-js/features/object/get-own-property-descriptors' +import 'core-js/features/object/keys' +import 'core-js/features/object/is' +import 'core-js/features/object/values' +import 'core-js/features/reflect' +import 'core-js/features/regexp' +import 'core-js/features/set' +import 'core-js/features/symbol' +import 'core-js/features/symbol/async-iterator' +import 'core-js/features/string/code-point-at' +import 'core-js/features/string/ends-with' +import 'core-js/features/string/from-code-point' +import 'core-js/features/string/includes' +import 'core-js/features/string/iterator' +import 'core-js/features/string/pad-start' +import 'core-js/features/string/pad-end' +import 'core-js/features/string/raw' +import 'core-js/features/string/repeat' +import 'core-js/features/string/starts-with' +import 'core-js/features/string/trim-left' +import 'core-js/features/string/trim-right' +import 'core-js/features/url' +import 'core-js/features/url/to-json' +import 'core-js/features/url-search-params' +import 'core-js/features/weak-map' +import 'core-js/features/weak-set' +import 'core-js/features/promise' +import 'core-js/features/promise/all-settled' +import 'core-js/features/promise/finally' + +// Specialized Packages: +import 'whatwg-fetch' +import assign from 'object-assign' +Object.assign = assign diff --git a/nextjs/packages/next/README.md b/nextjs/packages/next/README.md new file mode 100644 index 0000000000..c91a22e839 --- /dev/null +++ b/nextjs/packages/next/README.md @@ -0,0 +1,58 @@ +

    + + +

    Next.js

    + +

    + +

    + + + + + + + + + + + + +

    + +## Getting Started + +Visit https://nextjs.org/learn to get started with Next.js. + +## Documentation + +Visit [https://nextjs.org/docs](https://nextjs.org/docs) to view the full documentation. + +## Who is using Next.js? + +Next.js is used by the world's leading companies. Check out the [Next.js Showcase](https://nextjs.org/showcase) to learn more. + +## Community + +The Next.js community can be found on [GitHub Discussions](https://github.com/vercel/next.js/discussions), where you can ask questions, voice ideas, and share your projects. + +To chat with other community members you can join the [Next.js Discord](https://nextjs.org/discord). + +Our [Code of Conduct](https://github.com/vercel/next.js/blob/canary/CODE_OF_CONDUCT.md) applies to all Next.js community channels. + +## Contributing + +Please see our [contributing.md](/contributing.md). + +### Good First Issues + +We have a list of [good first issues](https://github.com/vercel/next.js/labels/good%20first%20issue) that contain bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process. + +## Authors + +- Tim Neutkens ([@timneutkens](https://twitter.com/timneutkens)) – [Vercel](https://vercel.com/about/timneutkens) +- Naoyuki Kanezawa ([@nkzawa](https://twitter.com/nkzawa)) – [Vercel](https://vercel.com/about/nkzawa) +- Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) – [Vercel](https://vercel.com/about/rauchg) +- Arunoda Susiripala ([@arunoda](https://twitter.com/arunoda)) +- Tony Kovanen ([@tonykovanen](https://twitter.com/tonykovanen)) +- Dan Zajdband ([@impronunciable](https://twitter.com/impronunciable)) diff --git a/nextjs/packages/next/amp.d.ts b/nextjs/packages/next/amp.d.ts new file mode 100644 index 0000000000..bca40393c3 --- /dev/null +++ b/nextjs/packages/next/amp.d.ts @@ -0,0 +1 @@ +export * from './dist/shared/lib/amp' diff --git a/nextjs/packages/next/amp.js b/nextjs/packages/next/amp.js new file mode 100644 index 0000000000..9c3c257022 --- /dev/null +++ b/nextjs/packages/next/amp.js @@ -0,0 +1 @@ +module.exports = require('./dist/shared/lib/amp') diff --git a/nextjs/packages/next/app.d.ts b/nextjs/packages/next/app.d.ts new file mode 100644 index 0000000000..644150b3ba --- /dev/null +++ b/nextjs/packages/next/app.d.ts @@ -0,0 +1,2 @@ +export * from './dist/pages/_app' +export { default } from './dist/pages/_app' diff --git a/nextjs/packages/next/app.js b/nextjs/packages/next/app.js new file mode 100644 index 0000000000..5437bb67cc --- /dev/null +++ b/nextjs/packages/next/app.js @@ -0,0 +1 @@ +module.exports = require('./dist/pages/_app') diff --git a/nextjs/packages/next/babel.d.ts b/nextjs/packages/next/babel.d.ts new file mode 100644 index 0000000000..3addd068f1 --- /dev/null +++ b/nextjs/packages/next/babel.d.ts @@ -0,0 +1 @@ +export * from './dist/build/babel/preset' diff --git a/nextjs/packages/next/babel.js b/nextjs/packages/next/babel.js new file mode 100644 index 0000000000..1bbe4f360f --- /dev/null +++ b/nextjs/packages/next/babel.js @@ -0,0 +1 @@ +module.exports = require('./dist/build/babel/preset') diff --git a/nextjs/packages/next/bin/next.ts b/nextjs/packages/next/bin/next.ts new file mode 100755 index 0000000000..3bf38eb68c --- /dev/null +++ b/nextjs/packages/next/bin/next.ts @@ -0,0 +1,133 @@ +#!/usr/bin/env node +import * as log from '../build/output/log' +import arg from 'next/dist/compiled/arg/index.js' +import { NON_STANDARD_NODE_ENV } from '../lib/constants' +;['react', 'react-dom'].forEach((dependency) => { + try { + // When 'npm link' is used it checks the clone location. Not the project. + require.resolve(dependency) + } catch (err) { + console.warn( + `The module '${dependency}' was not found. Blitz.js requires that you include it in 'dependencies' of your 'package.json'. To add it, run 'npm install ${dependency}'` + ) + } +}) + +const defaultCommand = 'dev' +export type cliCommand = (argv?: string[]) => void +const commands: { [command: string]: () => Promise } = { + build: () => import('../cli/next-build').then((i) => i.nextBuild), + start: () => import('../cli/next-start').then((i) => i.nextStart), + export: () => import('../cli/next-export').then((i) => i.nextExport), + dev: () => import('../cli/next-dev').then((i) => i.nextDev), + lint: () => import('../cli/next-lint').then((i) => i.nextLint), + telemetry: () => import('../cli/next-telemetry').then((i) => i.nextTelemetry), +} + +const args = arg( + { + // Types + '--version': Boolean, + '--help': Boolean, + '--inspect': Boolean, + '--env': String, + + // Aliases + '-v': '--version', + '-h': '--help', + '-e': '--env' + }, + { + permissive: true, + } +) + +// Version is inlined into the file using taskr build pipeline +if (args['--version']) { + console.log(`Blitz.js v${process.env.__NEXT_VERSION}`) + process.exit(0) +} + +// Check if we are running `next ` or `next` +const foundCommand = Boolean(commands[args._[0]]) + +// Makes sure the `next --help` case is covered +// This help message is only showed for `next --help` +// `next --help` falls through to be handled later +if (!foundCommand && args['--help']) { + console.log(` + Usage + $ next + + Available commands + ${Object.keys(commands).join(', ')} + + Options + --env, -e App environment name + --version, -v Version number + --help, -h Displays this message + + For more information run a command with the --help flag + $ next build --help + `) + process.exit(0) +} + +const command = foundCommand ? args._[0] : defaultCommand +const forwardedArgs = foundCommand ? args._.slice(1) : args._ + +if (args['--inspect']) + throw new Error( + `--inspect flag is deprecated. Use env variable NODE_OPTIONS instead: NODE_OPTIONS='--inspect' next ${command}` + ) + +// Make sure the `next --help` case is covered +if (args['--help']) { + forwardedArgs.push('--help') +} + +if (args['--env']) { + process.env.APP_ENV = args['--env'] +} + +const defaultEnv = command === 'dev' ? 'development' : 'production' + +const standardEnv = ['production', 'development', 'test'] + +if (process.env.NODE_ENV && !standardEnv.includes(process.env.NODE_ENV)) { + log.warn(NON_STANDARD_NODE_ENV) +} + +;(process.env as any).NODE_ENV = process.env.NODE_ENV || defaultEnv + +// Make sure commands gracefully respect termination signals (e.g. from Docker) +process.on('SIGTERM', () => process.exit(0)) +process.on('SIGINT', () => process.exit(0)) + +commands[command]() + .then((exec) => exec(forwardedArgs)) + .then(() => { + if (command === 'build') { + // ensure process exits after build completes so open handles/connections + // don't cause process to hang + process.exit(0) + } + }) + +if (command === 'dev') { + const { watchFile } = require('fs') + watchFile(`${process.cwd()}/blitz.config.js`, (cur: any, prev: any) => { + if (cur.size > 0 || prev.size > 0) { + console.log( + `\n> Found a change in blitz.config.js. Restart the server to see the changes in effect.` + ) + } + }) + watchFile(`${process.cwd()}/blitz.config.ts`, (cur: any, prev: any) => { + if (cur.size > 0 || prev.size > 0) { + console.log( + `\n> Found a change in blitz.config.ts. Restart the server to see the changes in effect.` + ) + } + }) +} diff --git a/nextjs/packages/next/build/babel/loader/get-config.ts b/nextjs/packages/next/build/babel/loader/get-config.ts new file mode 100644 index 0000000000..528ec55bbb --- /dev/null +++ b/nextjs/packages/next/build/babel/loader/get-config.ts @@ -0,0 +1,398 @@ +import { readFileSync } from 'fs' +import JSON5 from 'next/dist/compiled/json5' + +import { createConfigItem, loadOptions } from 'next/dist/compiled/babel/core' +import loadConfig from 'next/dist/compiled/babel/core-lib-config' + +import { NextBabelLoaderOptions, NextJsLoaderContext } from './types' +import { consumeIterator } from './util' +import { getIsPageFile, getIsRpcFile } from '../../utils' +import * as Log from '../../output/log' + +const nextDistPath = /(next[\\/]dist[\\/]shared[\\/]lib)|(next[\\/]dist[\\/]client)|(next[\\/]dist[\\/]pages)/ + +/** + * The properties defined here are the conditions with which subsets of inputs + * can be identified that are able to share a common Babel config. For example, + * in dev mode, different transforms must be applied to a source file depending + * on whether you're compiling for the client or for the server - thus `isServer` + * is germane. + * + * However, these characteristics need not protect against circumstances that + * will not be encountered in Next.js. For example, a source file may be + * transformed differently depending on whether we're doing a production compile + * or for HMR in dev mode. However, those two circumstances will never be + * encountered within the context of a single V8 context (and, thus, shared + * cache). Therefore, hasReactRefresh is _not_ germane to caching. + * + * NOTE: This approach does not support multiple `.babelrc` files in a + * single project. A per-cache-key config will be generated once and, + * if `.babelrc` is present, that config will be used for any subsequent + * transformations. + */ +interface CharacteristicsGermaneToCaching { + isServer: boolean + isPageFile: boolean + isRpcFile: boolean + isNextDist: boolean + hasModuleExports: boolean + fileExt: string +} + +const fileExtensionRegex = /\.([a-z]+)$/ +function getCacheCharacteristics( + loaderOptions: NextBabelLoaderOptions, + source: string, + filename: string +): CharacteristicsGermaneToCaching { + const { isServer, pagesDir } = loaderOptions + const isNextDist = nextDistPath.test(filename) + const relativePathFromRoot = filename.replace(pagesDir, '') + const isPageFile = !isNextDist && getIsPageFile(relativePathFromRoot) + const isRpcFile = !isNextDist && getIsRpcFile(relativePathFromRoot) + const hasModuleExports = source.indexOf('module.exports') !== -1 + const fileExt = fileExtensionRegex.exec(filename)?.[1] || 'unknown' + + return { + isServer, + isPageFile, + isRpcFile, + isNextDist, + hasModuleExports, + fileExt, + } +} + +/** + * Return an array of Babel plugins, conditioned upon loader options and + * source file characteristics. + */ +function getPlugins( + loaderOptions: NextBabelLoaderOptions, + cacheCharacteristics: CharacteristicsGermaneToCaching +) { + const { + isServer, + isPageFile, + isRpcFile, + isNextDist, + hasModuleExports, + } = cacheCharacteristics + + const { hasReactRefresh, development } = loaderOptions + + const applyCommonJsItem = hasModuleExports + ? createConfigItem(require('../plugins/commonjs'), { type: 'plugin' }) + : null + const reactRefreshItem = hasReactRefresh + ? createConfigItem( + [require('react-refresh/babel'), { skipEnvCheck: true }], + { type: 'plugin' } + ) + : null + const noAnonymousDefaultExportItem = + hasReactRefresh && !isServer + ? createConfigItem( + [require('../plugins/no-anonymous-default-export'), {}], + { type: 'plugin' } + ) + : null + const pageConfigItem = + !isServer && isPageFile + ? createConfigItem([require('../plugins/next-page-config')], { + type: 'plugin', + }) + : null + const rpcClientConfigItem = + !isServer && isRpcFile + ? createConfigItem([require('../plugins/blitz-rpc-client')], { + type: 'plugin', + }) + : null + const rpcServerTransformConfigItem = + isServer && isRpcFile + ? createConfigItem([require('../plugins/blitz-rpc-server-transform')], { + type: 'plugin', + }) + : null + const disallowExportAllItem = + !isServer && isPageFile + ? createConfigItem( + [require('../plugins/next-page-disallow-re-export-all-exports')], + { type: 'plugin' } + ) + : null + const transformDefineItem = createConfigItem( + [ + require.resolve('next/dist/compiled/babel/plugin-transform-define'), + { + 'process.env.NODE_ENV': development ? 'development' : 'production', + 'typeof window': isServer ? 'undefined' : 'object', + 'process.browser': isServer ? false : true, + }, + 'next-js-transform-define-instance', + ], + { type: 'plugin' } + ) + const nextSsgItem = + !isServer && isPageFile + ? createConfigItem([require.resolve('../plugins/next-ssg-transform')], { + type: 'plugin', + }) + : null + const commonJsItem = isNextDist + ? createConfigItem( + require('next/dist/compiled/babel/plugin-transform-modules-commonjs'), + { type: 'plugin' } + ) + : null + const fixNodeFileTraceItem = + isServer && isPageFile + ? createConfigItem([require('../plugins/fix-node-file-trace')], { + type: 'plugin', + }) + : null + + return [ + noAnonymousDefaultExportItem, + reactRefreshItem, + pageConfigItem, + rpcClientConfigItem, + rpcServerTransformConfigItem, + disallowExportAllItem, + applyCommonJsItem, + transformDefineItem, + nextSsgItem, + commonJsItem, + fixNodeFileTraceItem, + ].filter(Boolean) +} + +const isJsonFile = /\.(json|babelrc)$/ +const isJsFile = /\.js$/ + +/** + * While this function does block execution while reading from disk, it + * should not introduce any issues. The function is only invoked when + * generating a fresh config, and only a small handful of configs should + * be generated during compilation. + */ +function getCustomBabelConfig(configFilePath: string) { + if (isJsonFile.exec(configFilePath)) { + const babelConfigRaw = readFileSync(configFilePath, 'utf8') + return JSON5.parse(babelConfigRaw) + } else if (isJsFile.exec(configFilePath)) { + return require(configFilePath) + } + throw new Error( + 'The Blitz.js Babel loader does not support .mjs or .cjs config files.' + ) +} + +/** + * Generate a new, flat Babel config, ready to be handed to Babel-traverse. + * This config should have no unresolved overrides, presets, etc. + */ +function getFreshConfig( + this: NextJsLoaderContext, + cacheCharacteristics: CharacteristicsGermaneToCaching, + loaderOptions: NextBabelLoaderOptions, + target: string, + filename: string, + inputSourceMap?: object | null +) { + let { + isServer, + pagesDir, + development, + hasJsxRuntime, + configFile, + } = loaderOptions + + let customConfig: any = configFile + ? getCustomBabelConfig(configFile) + : undefined + + let options = { + babelrc: false, + cloneInputAst: false, + filename, + inputSourceMap: inputSourceMap || undefined, + + // Set the default sourcemap behavior based on Webpack's mapping flag, + // but allow users to override if they want. + sourceMaps: + loaderOptions.sourceMaps === undefined + ? this.sourceMap + : loaderOptions.sourceMaps, + + // Ensure that Webpack will get a full absolute path in the sourcemap + // so that it can properly map the module back to its internal cached + // modules. + sourceFileName: filename, + + plugins: [ + ...getPlugins(loaderOptions, cacheCharacteristics), + ...(customConfig?.plugins || []), + ], + + // target can be provided in babelrc + target: isServer ? undefined : customConfig?.target, + // env can be provided in babelrc + env: customConfig?.env, + + presets: (() => { + // If presets is defined the user will have next/babel in their babelrc + if (customConfig?.presets) { + return customConfig.presets + } + + // If presets is not defined the user will likely have "env" in their babelrc + if (customConfig) { + return undefined + } + + // If no custom config is provided the default is to use next/babel + return ['next/babel'] + })(), + + overrides: loaderOptions.overrides, + + caller: { + name: 'next-babel-turbo-loader', + supportsStaticESM: true, + supportsDynamicImport: true, + + // Provide plugins with insight into webpack target. + // https://github.com/babel/babel-loader/issues/787 + target: target, + + // Webpack 5 supports TLA behind a flag. We enable it by default + // for Babel, and then webpack will throw an error if the experimental + // flag isn't enabled. + supportsTopLevelAwait: true, + + isServer, + pagesDir, + isDev: development, + hasJsxRuntime, + + ...loaderOptions.caller, + }, + } as any + + // Babel does strict checks on the config so undefined is not allowed + if (typeof options.target === 'undefined') { + delete options.target + } + + Object.defineProperty(options.caller, 'onWarning', { + enumerable: false, + writable: false, + value: (reason: any) => { + if (!(reason instanceof Error)) { + reason = new Error(reason) + } + this.emitWarning(reason) + }, + }) + + const loadedOptions = loadOptions(options) + const config = consumeIterator(loadConfig(loadedOptions)) + + return config +} + +/** + * Each key returned here corresponds with a Babel config that can be shared. + * The conditions of permissible sharing between files is dependent on specific + * file attributes and Next.js compiler states: `CharacteristicsGermaneToCaching`. + */ +function getCacheKey(cacheCharacteristics: CharacteristicsGermaneToCaching) { + const { + isServer, + isPageFile, + isRpcFile, + isNextDist, + hasModuleExports, + fileExt, + } = cacheCharacteristics + + const flags = + 0 | + (isServer ? 0b0001 : 0) | + (isPageFile ? 0b0010 : 0) | + (isNextDist ? 0b0100 : 0) | + (hasModuleExports ? 0b1000 : 0) | + (isRpcFile ? 0b10000 : 0) + + return fileExt + flags +} + +type BabelConfig = any +const configCache: Map = new Map() +const configFiles: Set = new Set() + +export default function getConfig( + this: NextJsLoaderContext, + { + source, + target, + loaderOptions, + filename, + inputSourceMap, + }: { + source: string + loaderOptions: NextBabelLoaderOptions + target: string + filename: string + inputSourceMap?: object | null + } +): BabelConfig { + const cacheCharacteristics = getCacheCharacteristics( + loaderOptions, + source, + filename + ) + + if (loaderOptions.configFile) { + // Ensures webpack invalidates the cache for this loader when the config file changes + this.addDependency(loaderOptions.configFile) + } + + const cacheKey = getCacheKey(cacheCharacteristics) + if (configCache.has(cacheKey)) { + const cachedConfig = configCache.get(cacheKey) + + return { + ...cachedConfig, + options: { + ...cachedConfig.options, + cwd: loaderOptions.cwd, + root: loaderOptions.cwd, + filename, + sourceFileName: filename, + }, + } + } + + if (loaderOptions.configFile && !configFiles.has(loaderOptions.configFile)) { + configFiles.add(loaderOptions.configFile) + Log.info( + `Using external babel configuration from ${loaderOptions.configFile}` + ) + } + + const freshConfig = getFreshConfig.call( + this, + cacheCharacteristics, + loaderOptions, + target, + filename, + inputSourceMap + ) + + configCache.set(cacheKey, freshConfig) + + return freshConfig +} diff --git a/nextjs/packages/next/build/babel/loader/index.ts b/nextjs/packages/next/build/babel/loader/index.ts new file mode 100644 index 0000000000..33f458b4e9 --- /dev/null +++ b/nextjs/packages/next/build/babel/loader/index.ts @@ -0,0 +1,59 @@ +import { getOptions } from 'next/dist/compiled/loader-utils' +import { trace } from '../../../telemetry/trace' +import { Span } from '../../../telemetry/trace' +import transform from './transform' +import { NextJsLoaderContext } from './types' + +async function nextBabelLoader( + this: NextJsLoaderContext, + parentTrace: Span, + inputSource: string, + inputSourceMap: object | null | undefined +) { + const filename = this.resourcePath + const target = this.target + const loaderOptions = parentTrace + .traceChild('get-options') + .traceFn(() => getOptions(this)) + + const loaderSpanInner = parentTrace.traceChild('next-babel-turbo-transform') + const { + code: transformedSource, + map: outputSourceMap, + } = loaderSpanInner.traceFn(() => + transform.call( + this, + inputSource, + inputSourceMap, + loaderOptions, + filename, + target, + loaderSpanInner + ) + ) + + return [transformedSource, outputSourceMap] +} + +const nextBabelLoaderOuter = function nextBabelLoaderOuter( + this: NextJsLoaderContext, + inputSource: string, + inputSourceMap: object | null | undefined +) { + const callback = this.async() + + const loaderSpan = trace('next-babel-turbo-loader', this.currentTraceSpan?.id) + loaderSpan + .traceAsyncFn(() => + nextBabelLoader.call(this, loaderSpan, inputSource, inputSourceMap) + ) + .then( + ([transformedSource, outputSourceMap]) => + callback?.(null, transformedSource, outputSourceMap || inputSourceMap), + (err) => { + callback?.(err) + } + ) +} + +export default nextBabelLoaderOuter diff --git a/nextjs/packages/next/build/babel/loader/transform.ts b/nextjs/packages/next/build/babel/loader/transform.ts new file mode 100644 index 0000000000..a702c8a8d0 --- /dev/null +++ b/nextjs/packages/next/build/babel/loader/transform.ts @@ -0,0 +1,106 @@ +/* + * Partially adapted from @babel/core (MIT license). + */ + +import traverse from 'next/dist/compiled/babel/traverse' +import generate from 'next/dist/compiled/babel/generator' +import normalizeFile from 'next/dist/compiled/babel/core-lib-normalize-file' +import normalizeOpts from 'next/dist/compiled/babel/core-lib-normalize-opts' +import loadBlockHoistPlugin from 'next/dist/compiled/babel/core-lib-block-hoist-plugin' +import PluginPass from 'next/dist/compiled/babel/core-lib-plugin-pass' + +import getConfig from './get-config' +import { consumeIterator } from './util' +import { Span } from '../../../telemetry/trace' +import { NextJsLoaderContext } from './types' + +function getTraversalParams(file: any, pluginPairs: any[]) { + const passPairs = [] + const passes = [] + const visitors = [] + + for (const plugin of pluginPairs.concat(loadBlockHoistPlugin())) { + const pass = new PluginPass(file, plugin.key, plugin.options) + passPairs.push([plugin, pass]) + passes.push(pass) + visitors.push(plugin.visitor) + } + + return { passPairs, passes, visitors } +} + +function invokePluginPre(file: any, passPairs: any[]) { + for (const [{ pre }, pass] of passPairs) { + if (pre) { + pre.call(pass, file) + } + } +} + +function invokePluginPost(file: any, passPairs: any[]) { + for (const [{ post }, pass] of passPairs) { + if (post) { + post.call(pass, file) + } + } +} + +function transformAstPass(file: any, pluginPairs: any[], parentSpan: Span) { + const { passPairs, passes, visitors } = getTraversalParams(file, pluginPairs) + + invokePluginPre(file, passPairs) + const visitor = traverse.visitors.merge( + visitors, + passes, + // @ts-ignore - the exported types are incorrect here + file.opts.wrapPluginVisitorMethod + ) + + parentSpan + .traceChild('babel-turbo-traverse') + .traceFn(() => traverse(file.ast, visitor, file.scope)) + + invokePluginPost(file, passPairs) +} + +function transformAst(file: any, babelConfig: any, parentSpan: Span) { + for (const pluginPairs of babelConfig.passes) { + transformAstPass(file, pluginPairs, parentSpan) + } +} + +export default function transform( + this: NextJsLoaderContext, + source: string, + inputSourceMap: object | null | undefined, + loaderOptions: any, + filename: string, + target: string, + parentSpan: Span +) { + const getConfigSpan = parentSpan.traceChild('babel-turbo-get-config') + const babelConfig = getConfig.call(this, { + source, + loaderOptions, + inputSourceMap, + target, + filename, + }) + getConfigSpan.stop() + + const normalizeSpan = parentSpan.traceChild('babel-turbo-normalize-file') + const file = consumeIterator( + normalizeFile(babelConfig.passes, normalizeOpts(babelConfig), source) + ) + normalizeSpan.stop() + + const transformSpan = parentSpan.traceChild('babel-turbo-transform') + transformAst(file, babelConfig, transformSpan) + transformSpan.stop() + + const generateSpan = parentSpan.traceChild('babel-turbo-generate') + const { code, map } = generate(file.ast, file.opts.generatorOpts, file.code) + generateSpan.stop() + + return { code, map } +} diff --git a/nextjs/packages/next/build/babel/loader/types.d.ts b/nextjs/packages/next/build/babel/loader/types.d.ts new file mode 100644 index 0000000000..8cc3ec1952 --- /dev/null +++ b/nextjs/packages/next/build/babel/loader/types.d.ts @@ -0,0 +1,20 @@ +import { loader } from 'next/dist/compiled/webpack/webpack' +import { Span } from '../../../telemetry/trace' + +export interface NextJsLoaderContext extends loader.LoaderContext { + currentTraceSpan?: Span +} + +export interface NextBabelLoaderOptions { + hasJsxRuntime: boolean + hasReactRefresh: boolean + pageExtensions: string[] + isServer: boolean + development: boolean + pagesDir: string + sourceMaps?: any[] + overrides: any + caller: any + configFile: string | undefined + cwd: string +} diff --git a/nextjs/packages/next/build/babel/loader/util.ts b/nextjs/packages/next/build/babel/loader/util.ts new file mode 100644 index 0000000000..02d15f3eeb --- /dev/null +++ b/nextjs/packages/next/build/babel/loader/util.ts @@ -0,0 +1,8 @@ +export function consumeIterator(iter: Iterator) { + while (true) { + const { value, done } = iter.next() + if (done) { + return value + } + } +} diff --git a/nextjs/packages/next/build/babel/plugins/amp-attributes.ts b/nextjs/packages/next/build/babel/plugins/amp-attributes.ts new file mode 100644 index 0000000000..b19b7f9184 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/amp-attributes.ts @@ -0,0 +1,30 @@ +import { NodePath, PluginObj, types } from 'next/dist/compiled/babel/core' + +export default function AmpAttributePatcher(): PluginObj { + return { + visitor: { + JSXOpeningElement(path: NodePath) { + const openingElement = path.node + + const { name, attributes } = openingElement + if (!(name && name.type === 'JSXIdentifier')) { + return + } + + if (!name.name.startsWith('amp-')) { + return + } + + for (const attribute of attributes) { + if (attribute.type !== 'JSXAttribute') { + continue + } + + if (attribute.name.name === 'className') { + attribute.name.name = 'class' + } + } + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/blitz-rpc-client.ts b/nextjs/packages/next/build/babel/plugins/blitz-rpc-client.ts new file mode 100644 index 0000000000..0698adc8bc --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/blitz-rpc-client.ts @@ -0,0 +1,78 @@ +import { PluginObj } from 'next/dist/compiled/babel/core' +import { BabelType } from 'babel-plugin-tester' +import { + convertPageFilePathToRoutePath, + convertPageFilePathToResolverName, + convertPageFilePathToResolverType, +} from '../../utils' + +/* This plugin changes the file contents to this: + * +import { buildRpcClient } from "next/data-client"; +export default buildRpcClient({ + "resolverName": "getUsers", + "resolverType": "query", + "routePath": "/api/rpc/getUsers" +}); + * +*/ + +// https://astexplorer.net/#/gist/02bab3c8f0488923346b607ed578e2f7/latest (may be out of date) + +const fileExtensionRegex = /\.([a-z]+)$/ + +export default function blitzRpcClient(babel: BabelType): PluginObj { + const { types: t } = babel + + return { + visitor: { + Program: { + enter(path, state) { + const { filename, cwd } = state + const fileExt = fileExtensionRegex.exec(filename)?.[1] || 'unknown' + + const relativePathFromRoot = filename.replace(cwd, '') + const resolverName = convertPageFilePathToResolverName( + relativePathFromRoot + ) + const resolverType = convertPageFilePathToResolverType( + relativePathFromRoot + ) + const routePath = convertPageFilePathToRoutePath( + relativePathFromRoot, + [fileExt as string] + ) + + const importDeclaration = t.importDeclaration( + [ + t.importSpecifier( + t.identifier('buildRpcClient'), + t.identifier('buildRpcClient') + ), + ], + t.stringLiteral('next/data-client') + ) + const exportDeclaration = t.exportDefaultDeclaration( + t.callExpression(t.identifier('buildRpcClient'), [ + t.objectExpression([ + t.objectProperty( + t.stringLiteral('resolverName'), + t.stringLiteral(resolverName) + ), + t.objectProperty( + t.stringLiteral('resolverType'), + t.stringLiteral(resolverType) + ), + t.objectProperty( + t.stringLiteral('routePath'), + t.stringLiteral(routePath) + ), + ]), + ]) + ) + path.node.body = [importDeclaration, exportDeclaration] + }, + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/blitz-rpc-server-transform.ts b/nextjs/packages/next/build/babel/plugins/blitz-rpc-server-transform.ts new file mode 100644 index 0000000000..4371350d18 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/blitz-rpc-server-transform.ts @@ -0,0 +1,160 @@ +/* @eslint-disable no-redeclare */ +import { NodePath, PluginObj, types as t } from 'next/dist/compiled/babel/core' +import { BabelType } from 'babel-plugin-tester' +// @ts-ignore +import { addNamed as addNamedImport } from '@babel/helper-module-imports' +import { + convertPageFilePathToRoutePath, + convertPageFilePathToResolverName, + convertPageFilePathToResolverType, +} from '../../utils' + +/* This plugin changes the file default export to be like this: + * + export default require('next/data-client').buildRpcResolver( + function getUsers(input) { + return db.users.findMany() + }, + { + "resolverName": "getUsers", + "resolverType": "query", + "routePath": "/api/rpc/getUsers" + } + ); + * +*/ + +function functionDeclarationToExpression(declaration: t.FunctionDeclaration) { + return t.functionExpression( + declaration.id, + declaration.params, + declaration.body, + declaration.generator, + declaration.async + ) +} + +function classDeclarationToExpression(declaration: t.ClassDeclaration) { + return t.classExpression( + declaration.id, + declaration.superClass, + declaration.body, + declaration.decorators + ) +} + +function addBuildRpcResolverImport(path: NodePath) { + return addNamedImport(path, 'buildRpcResolver', 'next/data-client') +} + +function wrapInHOF( + path: NodePath, + expr: t.Expression, + { routePath, resolverName, resolverType }: Metadata +): t.Expression { + const program = path.findParent(t.isProgram) as NodePath + if (!program) throw new Error('Missing parent') + // eslint-disable-next-line no-shadow + const hasMiddlewareExport = program.node.body.find((path) => { + if (!t.isExportNamedDeclaration(path)) return null + if (!path.declaration) return null + if (!t.isVariableDeclaration(path.declaration)) return null + // eslint-disable-next-line no-shadow + const variableDeclarator = path.declaration.declarations.find((path) => + t.isVariableDeclarator(path) + ) + if (!variableDeclarator) return null + return (variableDeclarator.id as any).name === 'middleware' + }) + + const metadataProperties = [ + t.objectProperty( + t.stringLiteral('resolverName'), + t.stringLiteral(resolverName) + ), + t.objectProperty( + t.stringLiteral('resolverType'), + t.stringLiteral(resolverType) + ), + t.objectProperty(t.stringLiteral('routePath'), t.stringLiteral(routePath)), + ] + if (hasMiddlewareExport) { + metadataProperties.push( + t.objectProperty(t.identifier('middleware'), t.identifier('middleware')) + ) + } + return t.callExpression(addBuildRpcResolverImport(path), [ + expr, + t.objectExpression(metadataProperties), + ]) +} + +function wrapExportDefaultDeclaration(path: NodePath, metadata: Metadata) { + const { node } = path + + if ( + t.isIdentifier(node.declaration) || + t.isFunctionExpression(node.declaration) || + t.isCallExpression(node.declaration) + ) { + node.declaration = wrapInHOF(path, node.declaration, metadata) + } else if ( + t.isFunctionDeclaration(node.declaration) || + t.isClassDeclaration(node.declaration) + ) { + if (node.declaration.id) { + path.insertBefore(node.declaration) + node.declaration = wrapInHOF(path, node.declaration.id, metadata) + } else { + if (t.isFunctionDeclaration(node.declaration)) { + node.declaration = wrapInHOF( + path, + functionDeclarationToExpression(node.declaration), + metadata + ) + } else { + node.declaration = wrapInHOF( + path, + classDeclarationToExpression(node.declaration), + metadata + ) + } + } + } +} + +interface Metadata { + routePath: string + resolverName: string + resolverType: string +} + +const fileExtensionRegex = /\.([a-z]+)$/ + +export default function blitzRpcServerTransform(_babel: BabelType): PluginObj { + return { + visitor: { + ExportDefaultDeclaration(path, state) { + const { filename, cwd } = state + const fileExt = fileExtensionRegex.exec(filename)?.[1] || 'unknown' + + const relativePathFromRoot = filename.replace(cwd, '') + const resolverName = convertPageFilePathToResolverName( + relativePathFromRoot + ) + const resolverType = convertPageFilePathToResolverType( + relativePathFromRoot + ) + const routePath = convertPageFilePathToRoutePath(relativePathFromRoot, [ + fileExt as string, + ]) + + wrapExportDefaultDeclaration(path, { + resolverName, + resolverType, + routePath, + }) + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/commonjs.ts b/nextjs/packages/next/build/babel/plugins/commonjs.ts new file mode 100644 index 0000000000..7661534502 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/commonjs.ts @@ -0,0 +1,29 @@ +import { NodePath, PluginObj, types } from 'next/dist/compiled/babel/core' +import commonjsPlugin from 'next/dist/compiled/babel/plugin-transform-modules-commonjs' + +// Handle module.exports in user code +export default function CommonJSModulePlugin(...args: any): PluginObj { + const commonjs = commonjsPlugin(...args) + return { + visitor: { + Program: { + exit(path: NodePath, state) { + let foundModuleExports = false + path.traverse({ + MemberExpression(expressionPath: any) { + if (expressionPath.node.object.name !== 'module') return + if (expressionPath.node.property.name !== 'exports') return + foundModuleExports = true + }, + }) + + if (!foundModuleExports) { + return + } + + commonjs.visitor.Program.exit.call(this, path, state) + }, + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/fix-node-file-trace.ts b/nextjs/packages/next/build/babel/plugins/fix-node-file-trace.ts new file mode 100644 index 0000000000..d50492b287 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/fix-node-file-trace.ts @@ -0,0 +1,151 @@ +import type { NodePath, PluginObj } from 'next/dist/compiled/babel/core' +import { addNamed as addNamedImport } from '@babel/helper-module-imports' +import { + callExpression, + ExportNamedDeclaration, + Expression, + FunctionDeclaration, + functionExpression, + isExportDefaultDeclaration, + isExportNamedDeclaration, + isFunctionDeclaration, + isIdentifier, + isVariableDeclaration, + variableDeclaration, + variableDeclarator, +} from '@babel/types' +import * as nodePath from 'path' +import { getFileName, wrapExportDefaultDeclaration } from './utils' + +function functionDeclarationToExpression(declaration: FunctionDeclaration) { + return functionExpression( + declaration.id, + declaration.params, + declaration.body, + declaration.generator, + declaration.async + ) +} + +const functionsToReplace = ['getServerSideProps', 'getStaticProps'] + +function transformPropGetters( + path: NodePath, + transform: (v: Expression) => Expression +) { + const { node } = path + + if (isFunctionDeclaration(node.declaration)) { + const { id: functionId } = node.declaration + if (!functionId) { + return + } + + if (!functionsToReplace.includes(functionId.name)) { + return + } + + node.declaration = variableDeclaration('const', [ + variableDeclarator( + functionId, + transform(functionDeclarationToExpression(node.declaration)) + ), + ]) + + return + } + + if (isVariableDeclaration(node.declaration)) { + node.declaration.declarations.forEach((declaration) => { + if ( + isIdentifier(declaration.id) && + functionsToReplace.includes(declaration.id.name) && + declaration.init + ) { + declaration.init = transform(declaration.init) + } + }) + } +} + +const HOFName = 'withFixNodeFileTrace' +const importFrom = 'next/dist/server/utils' + +function addWithFixNodeFileTraceImport(path: NodePath) { + return addNamedImport(path, HOFName, importFrom) +} + +const pagesToSkip = ([] as string[]).concat( + ...['_app', '_document', '_error'].map((name) => [ + name + '.js', + name + '.jsx', + name + '.ts', + name + '.tsx', + ]) +) + +function isPage(filePath: string) { + if (!filePath.includes(nodePath.sep + 'pages' + nodePath.sep)) { + return false + } + if ( + filePath.includes( + nodePath.sep + 'pages' + nodePath.sep + 'api' + nodePath.sep + ) + ) { + return false + } + return !pagesToSkip.some((fileToSkip) => filePath.includes(fileToSkip)) +} + +function isApiRoute(filePath: string) { + if (filePath.includes(nodePath.sep + 'api' + nodePath.sep)) { + return true + } + return false +} + +function FixNodeFileTrace(): PluginObj { + return { + name: 'FixNodeFileTrace', + visitor: { + Program(path, state) { + const filePath = + getFileName(state) ?? nodePath.join('pages', 'Default.js') + + if (isPage(filePath)) { + const body = path.get('body') + body + .filter((node) => isExportNamedDeclaration(node)) + .forEach((node) => { + transformPropGetters( + node as NodePath, + (decl) => { + return callExpression(addWithFixNodeFileTraceImport(node), [ + decl, + ]) + } + ) + }) + return + } else if (isApiRoute(filePath)) { + const body = path.get('body') + const exportDefaultDeclaration = body.find((node) => + isExportDefaultDeclaration(node) + ) + if (exportDefaultDeclaration) { + wrapExportDefaultDeclaration( + exportDefaultDeclaration, + HOFName, + importFrom + ) + return + } + } + }, + }, + } +} + +// eslint-disable-next-line import/no-default-export +export default FixNodeFileTrace diff --git a/nextjs/packages/next/build/babel/plugins/jsx-pragma.ts b/nextjs/packages/next/build/babel/plugins/jsx-pragma.ts new file mode 100644 index 0000000000..8e9432e512 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/jsx-pragma.ts @@ -0,0 +1,112 @@ +import { + NodePath, + PluginObj, + types as BabelTypes, +} from 'next/dist/compiled/babel/core' +import jsx from 'next/dist/compiled/babel/plugin-syntax-jsx' + +export default function ({ + types: t, +}: { + types: typeof BabelTypes +}): PluginObj { + return { + inherits: jsx, + visitor: { + JSXElement(_path, state) { + state.set('jsx', true) + }, + + // Fragment syntax is still JSX since it compiles to createElement(), + // but JSXFragment is not a JSXElement + JSXFragment(_path, state) { + state.set('jsx', true) + }, + + Program: { + exit(path: NodePath, state) { + if (state.get('jsx')) { + const pragma = t.identifier(state.opts.pragma) + let importAs = pragma + + // if there's already a React in scope, use that instead of adding an import + const existingBinding = + state.opts.reuseImport !== false && + state.opts.importAs && + path.scope.getBinding(state.opts.importAs) + + // var _jsx = _pragma.createElement; + if (state.opts.property) { + if (state.opts.importAs) { + importAs = t.identifier(state.opts.importAs) + } else { + importAs = path.scope.generateUidIdentifier('pragma') + } + + const mapping = t.variableDeclaration('var', [ + t.variableDeclarator( + pragma, + t.memberExpression( + importAs, + t.identifier(state.opts.property) + ) + ), + ]) + + // if the React binding came from a require('react'), + // make sure that our usage comes after it. + let newPath: NodePath + + if ( + existingBinding && + t.isVariableDeclarator(existingBinding.path.node) && + t.isCallExpression(existingBinding.path.node.init) && + t.isIdentifier(existingBinding.path.node.init.callee) && + existingBinding.path.node.init.callee.name === 'require' + ) { + ;[newPath] = existingBinding.path.parentPath.insertAfter( + mapping + ) + } else { + ;[newPath] = path.unshiftContainer('body', mapping) + } + + for (const declar of newPath.get('declarations')) { + path.scope.registerBinding( + newPath.node.kind, + declar as NodePath + ) + } + } + + if (!existingBinding) { + const importSpecifier = t.importDeclaration( + [ + state.opts.import + ? // import { $import as _pragma } from '$module' + t.importSpecifier( + importAs, + t.identifier(state.opts.import) + ) + : state.opts.importNamespace + ? t.importNamespaceSpecifier(importAs) + : // import _pragma from '$module' + t.importDefaultSpecifier(importAs), + ], + t.stringLiteral(state.opts.module || 'react') + ) + + const [newPath] = path.unshiftContainer('body', importSpecifier) + for (const specifier of newPath.get('specifiers')) { + path.scope.registerBinding( + 'module', + specifier as NodePath + ) + } + } + } + }, + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/next-page-config.ts b/nextjs/packages/next/build/babel/plugins/next-page-config.ts new file mode 100644 index 0000000000..344fd44075 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/next-page-config.ts @@ -0,0 +1,210 @@ +import { + NodePath, + PluginObj, + PluginPass, + types as BabelTypes, + Visitor, +} from 'next/dist/compiled/babel/core' +import { PageConfig } from 'next/types' +import { STRING_LITERAL_DROP_BUNDLE } from '../../../shared/lib/constants' + +const CONFIG_KEY = 'config' + +// replace program path with just a variable with the drop identifier +function replaceBundle(path: any, t: typeof BabelTypes): void { + path.parentPath.replaceWith( + t.program( + [ + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(STRING_LITERAL_DROP_BUNDLE), + t.stringLiteral(`${STRING_LITERAL_DROP_BUNDLE} ${Date.now()}`) + ), + ]), + ], + [] + ) + ) +} + +function errorMessage(state: any, details: string): string { + const pageName = + (state.filename || '').split(state.cwd || '').pop() || 'unknown' + return `Invalid page config export found. ${details} in file ${pageName}. See: https://nextjs.org/docs/messages/invalid-page-config` +} + +interface ConfigState extends PluginPass { + bundleDropped?: boolean +} + +// config to parsing pageConfig for client bundles +export default function nextPageConfig({ + types: t, +}: { + types: typeof BabelTypes +}): PluginObj { + return { + visitor: { + Program: { + enter(path, state) { + path.traverse( + { + ExportDeclaration(exportPath, exportState) { + if ( + BabelTypes.isExportNamedDeclaration(exportPath) && + (exportPath.node as BabelTypes.ExportNamedDeclaration).specifiers?.some( + (specifier) => { + return ( + (t.isIdentifier(specifier.exported) + ? specifier.exported.name + : specifier.exported.value) === CONFIG_KEY + ) + } + ) && + BabelTypes.isStringLiteral( + (exportPath.node as BabelTypes.ExportNamedDeclaration) + .source + ) + ) { + throw new Error( + errorMessage( + exportState, + 'Expected object but got export from' + ) + ) + } + }, + ExportNamedDeclaration( + exportPath: NodePath, + exportState: any + ) { + if ( + exportState.bundleDropped || + (!exportPath.node.declaration && + exportPath.node.specifiers.length === 0) + ) { + return + } + + const config: PageConfig = {} + const declarations: BabelTypes.VariableDeclarator[] = [ + ...((exportPath.node + .declaration as BabelTypes.VariableDeclaration) + ?.declarations || []), + exportPath.scope.getBinding(CONFIG_KEY)?.path + .node as BabelTypes.VariableDeclarator, + ].filter(Boolean) + + for (const specifier of exportPath.node.specifiers) { + if ( + (t.isIdentifier(specifier.exported) + ? specifier.exported.name + : specifier.exported.value) === CONFIG_KEY + ) { + // export {} from 'somewhere' + if (BabelTypes.isStringLiteral(exportPath.node.source)) { + throw new Error( + errorMessage( + exportState, + `Expected object but got import` + ) + ) + // import hello from 'world' + // export { hello as config } + } else if ( + BabelTypes.isIdentifier( + (specifier as BabelTypes.ExportSpecifier).local + ) + ) { + if ( + BabelTypes.isImportSpecifier( + exportPath.scope.getBinding( + (specifier as BabelTypes.ExportSpecifier).local.name + )?.path.node + ) + ) { + throw new Error( + errorMessage( + exportState, + `Expected object but got import` + ) + ) + } + } + } + } + + for (const declaration of declarations) { + if ( + !BabelTypes.isIdentifier(declaration.id, { + name: CONFIG_KEY, + }) + ) { + continue + } + + if (!BabelTypes.isObjectExpression(declaration.init)) { + const got = declaration.init + ? declaration.init.type + : 'undefined' + throw new Error( + errorMessage( + exportState, + `Expected object but got ${got}` + ) + ) + } + + for (const prop of declaration.init.properties) { + if (BabelTypes.isSpreadElement(prop)) { + throw new Error( + errorMessage( + exportState, + `Property spread is not allowed` + ) + ) + } + const { name } = prop.key as BabelTypes.Identifier + if (BabelTypes.isIdentifier(prop.key, { name: 'amp' })) { + if (!BabelTypes.isObjectProperty(prop)) { + throw new Error( + errorMessage( + exportState, + `Invalid property "${name}"` + ) + ) + } + if ( + !BabelTypes.isBooleanLiteral(prop.value) && + !BabelTypes.isStringLiteral(prop.value) + ) { + throw new Error( + errorMessage( + exportState, + `Invalid value for "${name}"` + ) + ) + } + config.amp = prop.value.value as PageConfig['amp'] + } + } + } + + if (config.amp === true) { + if (!exportState.file?.opts?.caller.isDev) { + // don't replace bundle in development so HMR can track + // dependencies and trigger reload when they are changed + replaceBundle(exportPath, t) + } + exportState.bundleDropped = true + return + } + }, + }, + state + ) + }, + }, + } as Visitor, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/next-page-disallow-re-export-all-exports.ts b/nextjs/packages/next/build/babel/plugins/next-page-disallow-re-export-all-exports.ts new file mode 100644 index 0000000000..93ffe83ea8 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/next-page-disallow-re-export-all-exports.ts @@ -0,0 +1,18 @@ +import { NodePath, PluginObj, types } from 'next/dist/compiled/babel/core' + +export default function NextPageDisallowReExportAllExports(): PluginObj { + return { + visitor: { + ExportAllDeclaration(path: NodePath) { + const err = new SyntaxError( + `Using \`export * from '...'\` in a page is disallowed. Please use \`export { default } from '...'\` instead.\n` + + `Read more: https://nextjs.org/docs/messages/export-all-in-page` + ) + ;(err as any).code = 'BABEL_PARSE_ERROR' + ;(err as any).loc = + path.node.loc?.start ?? path.node.loc?.end ?? path.node.loc + throw err + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/next-ssg-transform.ts b/nextjs/packages/next/build/babel/plugins/next-ssg-transform.ts new file mode 100644 index 0000000000..eb741358a1 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/next-ssg-transform.ts @@ -0,0 +1,455 @@ +import { + NodePath, + PluginObj, + types as BabelTypes, +} from 'next/dist/compiled/babel/core' +import { SERVER_PROPS_SSG_CONFLICT } from '../../../lib/constants' +import { SERVER_PROPS_ID, STATIC_PROPS_ID } from '../../../shared/lib/constants' + +export const EXPORT_NAME_GET_STATIC_PROPS = 'getStaticProps' +export const EXPORT_NAME_GET_STATIC_PATHS = 'getStaticPaths' +export const EXPORT_NAME_GET_SERVER_PROPS = 'getServerSideProps' + +const ssgExports = new Set([ + EXPORT_NAME_GET_STATIC_PROPS, + EXPORT_NAME_GET_STATIC_PATHS, + EXPORT_NAME_GET_SERVER_PROPS, + + // legacy methods added so build doesn't fail from importing + // server-side only methods + `unstable_getStaticProps`, + `unstable_getStaticPaths`, + `unstable_getServerProps`, + `unstable_getServerSideProps`, +]) + +type PluginState = { + refs: Set> + isPrerender: boolean + isServerProps: boolean + done: boolean +} + +function decorateSsgExport( + t: typeof BabelTypes, + path: NodePath, + state: PluginState +): void { + const gsspName = state.isPrerender ? STATIC_PROPS_ID : SERVER_PROPS_ID + const gsspId = t.identifier(gsspName) + + const addGsspExport = ( + exportPath: + | NodePath + | NodePath + ): void => { + if (state.done) { + return + } + state.done = true + + const [pageCompPath] = exportPath.replaceWithMultiple([ + t.exportNamedDeclaration( + t.variableDeclaration( + // We use 'var' instead of 'let' or 'const' for ES5 support. Since + // this runs in `Program#exit`, no ES2015 transforms (preset env) + // will be ran against this code. + 'var', + [t.variableDeclarator(gsspId, t.booleanLiteral(true))] + ), + [t.exportSpecifier(gsspId, gsspId)] + ), + exportPath.node, + ]) + exportPath.scope.registerDeclaration( + pageCompPath as NodePath + ) + } + + path.traverse({ + ExportDefaultDeclaration(exportDefaultPath) { + addGsspExport(exportDefaultPath) + }, + ExportNamedDeclaration(exportNamedPath) { + addGsspExport(exportNamedPath) + }, + }) +} + +const isDataIdentifier = (name: string, state: PluginState): boolean => { + if (ssgExports.has(name)) { + if (name === EXPORT_NAME_GET_SERVER_PROPS) { + if (state.isPrerender) { + throw new Error(SERVER_PROPS_SSG_CONFLICT) + } + state.isServerProps = true + } else { + if (state.isServerProps) { + throw new Error(SERVER_PROPS_SSG_CONFLICT) + } + state.isPrerender = true + } + return true + } + return false +} + +export default function nextTransformSsg({ + types: t, +}: { + types: typeof BabelTypes +}): PluginObj { + function getIdentifier( + path: + | NodePath + | NodePath + | NodePath + ): NodePath | null { + const parentPath = path.parentPath + if (parentPath.type === 'VariableDeclarator') { + const pp = parentPath as NodePath + const name = pp.get('id') + return name.node.type === 'Identifier' + ? (name as NodePath) + : null + } + + if (parentPath.type === 'AssignmentExpression') { + const pp = parentPath as NodePath + const name = pp.get('left') + return name.node.type === 'Identifier' + ? (name as NodePath) + : null + } + + if (path.node.type === 'ArrowFunctionExpression') { + return null + } + + return path.node.id && path.node.id.type === 'Identifier' + ? (path.get('id') as NodePath) + : null + } + + function isIdentifierReferenced( + ident: NodePath + ): boolean { + const b = ident.scope.getBinding(ident.node.name) + if (b?.referenced) { + // Functions can reference themselves, so we need to check if there's a + // binding outside the function scope or not. + if (b.path.type === 'FunctionDeclaration') { + return !b.constantViolations + .concat(b.referencePaths) + // Check that every reference is contained within the function: + .every((ref) => ref.findParent((p) => p === b.path)) + } + + return true + } + return false + } + + function markFunction( + path: + | NodePath + | NodePath + | NodePath, + state: PluginState + ): void { + const ident = getIdentifier(path) + if (ident?.node && isIdentifierReferenced(ident)) { + state.refs.add(ident) + } + } + + function markImport( + path: + | NodePath + | NodePath + | NodePath, + state: PluginState + ): void { + const local = path.get('local') as NodePath + if (isIdentifierReferenced(local)) { + state.refs.add(local) + } + } + + return { + visitor: { + Program: { + enter(path, state) { + state.refs = new Set>() + state.isPrerender = false + state.isServerProps = false + state.done = false + + path.traverse( + { + VariableDeclarator(variablePath, variableState) { + if (variablePath.node.id.type === 'Identifier') { + const local = variablePath.get( + 'id' + ) as NodePath + if (isIdentifierReferenced(local)) { + variableState.refs.add(local) + } + } else if (variablePath.node.id.type === 'ObjectPattern') { + const pattern = variablePath.get( + 'id' + ) as NodePath + + const properties = pattern.get('properties') + properties.forEach((p) => { + const local = p.get( + p.node.type === 'ObjectProperty' + ? 'value' + : p.node.type === 'RestElement' + ? 'argument' + : (function () { + throw new Error('invariant') + })() + ) as NodePath + if (isIdentifierReferenced(local)) { + variableState.refs.add(local) + } + }) + } else if (variablePath.node.id.type === 'ArrayPattern') { + const pattern = variablePath.get( + 'id' + ) as NodePath + + const elements = pattern.get('elements') + elements.forEach((e) => { + let local: NodePath + if (e.node?.type === 'Identifier') { + local = e as NodePath + } else if (e.node?.type === 'RestElement') { + local = e.get( + 'argument' + ) as NodePath + } else { + return + } + + if (isIdentifierReferenced(local)) { + variableState.refs.add(local) + } + }) + } + }, + FunctionDeclaration: markFunction, + FunctionExpression: markFunction, + ArrowFunctionExpression: markFunction, + ImportSpecifier: markImport, + ImportDefaultSpecifier: markImport, + ImportNamespaceSpecifier: markImport, + ExportNamedDeclaration(exportNamedPath, exportNamedState) { + const specifiers = exportNamedPath.get('specifiers') + if (specifiers.length) { + specifiers.forEach((s) => { + if ( + isDataIdentifier( + t.isIdentifier(s.node.exported) + ? s.node.exported.name + : s.node.exported.value, + exportNamedState + ) + ) { + s.remove() + } + }) + + if (exportNamedPath.node.specifiers.length < 1) { + exportNamedPath.remove() + } + return + } + + const decl = exportNamedPath.get('declaration') as NodePath< + | BabelTypes.FunctionDeclaration + | BabelTypes.VariableDeclaration + > + if (decl == null || decl.node == null) { + return + } + + switch (decl.node.type) { + case 'FunctionDeclaration': { + const name = decl.node.id!.name + if (isDataIdentifier(name, exportNamedState)) { + exportNamedPath.remove() + } + break + } + case 'VariableDeclaration': { + const inner = decl.get( + 'declarations' + ) as NodePath[] + inner.forEach((d) => { + if (d.node.id.type !== 'Identifier') { + return + } + const name = d.node.id.name + if (isDataIdentifier(name, exportNamedState)) { + d.remove() + } + }) + break + } + default: { + break + } + } + }, + }, + state + ) + + if (!state.isPrerender && !state.isServerProps) { + return + } + + const refs = state.refs + let count: number + + function sweepFunction( + sweepPath: + | NodePath + | NodePath + | NodePath + ): void { + const ident = getIdentifier(sweepPath) + if ( + ident?.node && + refs.has(ident) && + !isIdentifierReferenced(ident) + ) { + ++count + + if ( + t.isAssignmentExpression(sweepPath.parentPath) || + t.isVariableDeclarator(sweepPath.parentPath) + ) { + sweepPath.parentPath.remove() + } else { + sweepPath.remove() + } + } + } + + function sweepImport( + sweepPath: + | NodePath + | NodePath + | NodePath + ): void { + const local = sweepPath.get( + 'local' + ) as NodePath + if (refs.has(local) && !isIdentifierReferenced(local)) { + ++count + sweepPath.remove() + if ( + (sweepPath.parent as BabelTypes.ImportDeclaration).specifiers + .length === 0 + ) { + sweepPath.parentPath.remove() + } + } + } + + do { + ;(path.scope as any).crawl() + count = 0 + + path.traverse({ + // eslint-disable-next-line no-loop-func + VariableDeclarator(variablePath) { + if (variablePath.node.id.type === 'Identifier') { + const local = variablePath.get( + 'id' + ) as NodePath + if (refs.has(local) && !isIdentifierReferenced(local)) { + ++count + variablePath.remove() + } + } else if (variablePath.node.id.type === 'ObjectPattern') { + const pattern = variablePath.get( + 'id' + ) as NodePath + + const beforeCount = count + const properties = pattern.get('properties') + properties.forEach((p) => { + const local = p.get( + p.node.type === 'ObjectProperty' + ? 'value' + : p.node.type === 'RestElement' + ? 'argument' + : (function () { + throw new Error('invariant') + })() + ) as NodePath + + if (refs.has(local) && !isIdentifierReferenced(local)) { + ++count + p.remove() + } + }) + + if ( + beforeCount !== count && + pattern.get('properties').length < 1 + ) { + variablePath.remove() + } + } else if (variablePath.node.id.type === 'ArrayPattern') { + const pattern = variablePath.get( + 'id' + ) as NodePath + + const beforeCount = count + const elements = pattern.get('elements') + elements.forEach((e) => { + let local: NodePath + if (e.node?.type === 'Identifier') { + local = e as NodePath + } else if (e.node?.type === 'RestElement') { + local = e.get( + 'argument' + ) as NodePath + } else { + return + } + + if (refs.has(local) && !isIdentifierReferenced(local)) { + ++count + e.remove() + } + }) + + if ( + beforeCount !== count && + pattern.get('elements').length < 1 + ) { + variablePath.remove() + } + } + }, + FunctionDeclaration: sweepFunction, + FunctionExpression: sweepFunction, + ArrowFunctionExpression: sweepFunction, + ImportSpecifier: sweepImport, + ImportDefaultSpecifier: sweepImport, + ImportNamespaceSpecifier: sweepImport, + }) + } while (count) + + decorateSsgExport(t, path, state) + }, + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/no-anonymous-default-export.ts b/nextjs/packages/next/build/babel/plugins/no-anonymous-default-export.ts new file mode 100644 index 0000000000..2702fbe1f3 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/no-anonymous-default-export.ts @@ -0,0 +1,100 @@ +import { PluginObj, types as BabelTypes } from 'next/dist/compiled/babel/core' +import chalk from 'chalk' + +export default function NoAnonymousDefaultExport({ + types: t, + ...babel +}: { + types: typeof BabelTypes + caller: (callerCallback: (caller: any) => any) => any +}): PluginObj { + let onWarning: ((reason: string | Error) => void) | null = null + babel.caller((caller) => { + onWarning = caller.onWarning + return '' // Intentionally empty to not invalidate cache + }) + + if (typeof onWarning !== 'function') { + return { visitor: {} } + } + + const warn = onWarning! as any /* blitz */ + return { + visitor: { + ExportDefaultDeclaration(path) { + const def = path.node.declaration + + if ( + !( + def.type === 'ArrowFunctionExpression' || + def.type === 'FunctionDeclaration' + ) + ) { + return + } + + switch (def.type) { + case 'ArrowFunctionExpression': { + if ( + !process.env.__NEXT_TEST_MODE || + !!process.env.__NEXT_TEST_ANON_EXPORT + ) { + warn( + [ + chalk.yellow.bold( + 'Anonymous arrow functions cause Fast Refresh to not preserve local component state.' + ), + 'Please add a name to your function, for example:', + '', + chalk.bold('Before'), + chalk.cyan('export default () =>
    ;'), + '', + chalk.bold('After'), + chalk.cyan('const Named = () =>
    ;'), + chalk.cyan('export default Named;'), + '', + `A codemod is available to fix the most common cases: ${chalk.cyan( + 'https://nextjs.link/codemod-ndc' + )}`, + ].join('\n') + ) + } + break + } + case 'FunctionDeclaration': { + const isAnonymous = !Boolean(def.id) + if ( + isAnonymous && + (!process.env.__NEXT_TEST_MODE || + !!process.env.__NEXT_TEST_ANON_EXPORT) + ) { + warn( + [ + chalk.yellow.bold( + 'Anonymous function declarations cause Fast Refresh to not preserve local component state.' + ), + 'Please add a name to your function, for example:', + '', + chalk.bold('Before'), + chalk.cyan('export default function () { /* ... */ }'), + '', + chalk.bold('After'), + chalk.cyan('export default function Named() { /* ... */ }'), + '', + `A codemod is available to fix the most common cases: ${chalk.cyan( + 'https://nextjs.link/codemod-ndc' + )}`, + ].join('\n') + ) + } + break + } + default: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _: never = def + } + } + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/optimize-hook-destructuring.ts b/nextjs/packages/next/build/babel/plugins/optimize-hook-destructuring.ts new file mode 100644 index 0000000000..7cdfcaa7d7 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/optimize-hook-destructuring.ts @@ -0,0 +1,78 @@ +import { + NodePath, + PluginObj, + types as BabelTypes, +} from 'next/dist/compiled/babel/core' + +// matches any hook-like (the default) +const isHook = /^use[A-Z]/ + +// matches only built-in hooks provided by React et al +const isBuiltInHook = /^use(Callback|Context|DebugValue|Effect|ImperativeHandle|LayoutEffect|Memo|Reducer|Ref|State)$/ + +export default function ({ + types: t, +}: { + types: typeof BabelTypes +}): PluginObj { + const visitor = { + CallExpression(path: NodePath, state: any) { + const onlyBuiltIns = state.opts.onlyBuiltIns + + // if specified, options.lib is a list of libraries that provide hook functions + const libs = + state.opts.lib && + (state.opts.lib === true + ? ['react', 'preact/hooks'] + : [].concat(state.opts.lib)) + + // skip function calls that are not the init of a variable declaration: + if (!t.isVariableDeclarator(path.parent)) return + + // skip function calls where the return value is not Array-destructured: + if (!t.isArrayPattern(path.parent.id)) return + + // name of the (hook) function being called: + const hookName = (path.node.callee as BabelTypes.Identifier).name + + if (libs) { + const binding = path.scope.getBinding(hookName) + // not an import + if (!binding || binding.kind !== 'module') return + + const specifier = (binding.path.parent as BabelTypes.ImportDeclaration) + .source.value + // not a match + if (!libs.some((lib: any) => lib === specifier)) return + } + + // only match function calls with names that look like a hook + if (!(onlyBuiltIns ? isBuiltInHook : isHook).test(hookName)) return + + path.parent.id = t.objectPattern( + path.parent.id.elements.reduce>( + (patterns, element, i) => { + if (element === null) { + return patterns + } + + return patterns.concat( + t.objectProperty(t.numericLiteral(i), element) + ) + }, + [] + ) + ) + }, + } + + return { + name: 'optimize-hook-destructuring', + visitor: { + // this is a workaround to run before preset-env destroys destructured assignments + Program(path, state) { + path.traverse(visitor, state) + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/react-loadable-plugin.ts b/nextjs/packages/next/build/babel/plugins/react-loadable-plugin.ts new file mode 100644 index 0000000000..705b5ee5a7 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/react-loadable-plugin.ts @@ -0,0 +1,209 @@ +/** +COPYRIGHT (c) 2017-present James Kyle + MIT License + Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWAR +*/ +// This file is https://github.com/jamiebuilds/react-loadable/blob/master/src/babel.js +// Modified to also look for `next/dynamic` +// Modified to put `webpack` and `modules` under `loadableGenerated` to be backwards compatible with next/dynamic which has a `modules` key +// Modified to support `dynamic(import('something'))` and `dynamic(import('something'), options) + +import { + NodePath, + PluginObj, + types as BabelTypes, +} from 'next/dist/compiled/babel/core' + +import { relative as relativePath } from 'path' + +export default function ({ + types: t, +}: { + types: typeof BabelTypes +}): PluginObj { + return { + visitor: { + ImportDeclaration( + path: NodePath, + state: any + ) { + let source = path.node.source.value + if (source !== 'next/dynamic') return + + let defaultSpecifier = path.get('specifiers').find((specifier) => { + return specifier.isImportDefaultSpecifier() + }) + + if (!defaultSpecifier) return + + const bindingName = defaultSpecifier.node.local.name + const binding = path.scope.getBinding(bindingName) + + if (!binding) { + return + } + + binding.referencePaths.forEach((refPath) => { + let callExpression = refPath.parentPath + + if ( + callExpression.isMemberExpression() && + callExpression.node.computed === false + ) { + const property = callExpression.get('property') + if ( + !Array.isArray(property) && + property.isIdentifier({ name: 'Map' }) + ) { + callExpression = callExpression.parentPath + } + } + + if (!callExpression.isCallExpression()) return + + const callExpression_ = callExpression as NodePath< + BabelTypes.CallExpression + > + + let args = callExpression_.get('arguments') + if (args.length > 2) { + throw callExpression_.buildCodeFrameError( + 'next/dynamic only accepts 2 arguments' + ) + } + + if (!args[0]) { + return + } + + let loader + let options + + if (args[0].isObjectExpression()) { + options = args[0] + } else { + if (!args[1]) { + callExpression_.node.arguments.push(t.objectExpression([])) + } + // This is needed as the code is modified above + args = callExpression_.get('arguments') + loader = args[0] + options = args[1] + } + + if (!options.isObjectExpression()) return + const options_ = options as NodePath + + let properties = options_.get('properties') + let propertiesMap: { + [key: string]: NodePath< + | BabelTypes.ObjectProperty + | BabelTypes.ObjectMethod + | BabelTypes.SpreadElement + > + } = {} + + properties.forEach((property) => { + const key: any = property.get('key') + propertiesMap[key.node.name] = property + }) + + if (propertiesMap.loadableGenerated) { + return + } + + if (propertiesMap.loader) { + loader = propertiesMap.loader.get('value') + } + + if (propertiesMap.modules) { + loader = propertiesMap.modules.get('value') + } + + if (!loader || Array.isArray(loader)) { + return + } + const dynamicImports: BabelTypes.Expression[] = [] + const dynamicKeys: BabelTypes.Expression[] = [] + + loader.traverse({ + Import(importPath) { + const importArguments = importPath.parentPath.get('arguments') + if (!Array.isArray(importArguments)) return + const node: any = importArguments[0].node + dynamicImports.push(node) + dynamicKeys.push( + t.binaryExpression( + '+', + t.stringLiteral( + (state.file.opts.caller?.pagesDir + ? relativePath( + state.file.opts.caller.pagesDir, + state.file.opts.filename + ) + : state.file.opts.filename) + ' -> ' + ), + node + ) + ) + }, + }) + + if (!dynamicImports.length) return + + options.node.properties.push( + t.objectProperty( + t.identifier('loadableGenerated'), + t.objectExpression([ + t.objectProperty( + t.identifier('webpack'), + t.arrowFunctionExpression( + [], + t.arrayExpression( + dynamicImports.map((dynamicImport) => { + return t.callExpression( + t.memberExpression( + t.identifier('require'), + t.identifier('resolveWeak') + ), + [dynamicImport] + ) + }) + ) + ) + ), + t.objectProperty( + t.identifier('modules'), + t.arrayExpression(dynamicKeys) + ), + ]) + ) + ) + + // Turns `dynamic(import('something'))` into `dynamic(() => import('something'))` for backwards compat. + // This is the replicate the behavior in versions below Next.js 7 where we magically handled not executing the `import()` too. + // We'll deprecate this behavior and provide a codemod for it in 7.1. + if (loader.isCallExpression()) { + const arrowFunction = t.arrowFunctionExpression([], loader.node) + loader.replaceWith(arrowFunction) + } + }) + }, + }, + } +} diff --git a/nextjs/packages/next/build/babel/plugins/rewrite-imports.ts b/nextjs/packages/next/build/babel/plugins/rewrite-imports.ts new file mode 100644 index 0000000000..8da4ff3b2e --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/rewrite-imports.ts @@ -0,0 +1,148 @@ +import { PluginObj } from 'next/dist/compiled/babel/core' +import { BabelType } from 'babel-plugin-tester' + +/* + * https://astexplorer.net/#/gist/dd0cdbd56a701d8c9e078d20505b3980/latest + */ + +const defaultImportSource = 'next/stdlib' + +const specialImports: Record = { + Link: 'next/link', + Image: 'next/image', + Script: 'next/script', + + Document: 'next/document', + DocumentHead: 'next/document', + Html: 'next/document', + Main: 'next/document', + BlitzScript: 'next/document', + + // AuthenticationError: 'next/stdlib', + // AuthorizationError: 'next/stdlib', + // CSRFTokenMismatchError: 'next/stdlib', + // NotFoundError: 'next/stdlib', + // PaginationArgumentError: 'next/stdlib', + // RedirectError: 'next/stdlib', + // formatZodError: 'next/stdlib', + // recursiveFormatZodErrors: 'next/stdlib', + // validateZodSchema: 'next/stdlib', + // enhancePrisma: 'next/stdlib', + // ErrorBoundary: 'next/stdlib', + // withErrorBoundary: 'next/stdlib', + // useErrorHandler: 'next/stdlib', + // withBlitzAppRoot: 'next/stdlib', + + paginate: 'next/stdlib-server', + isLocalhost: 'next/stdlib-server', + invokeWithMiddleware: 'next/stdlib-server', + passportAuth: 'next/stdlib-server', + sessionMiddleware: 'next/stdlib-server', + simpleRolesIsAuthorized: 'next/stdlib-server', + getSession: 'next/stdlib-server', + setPublicDataForUser: 'next/stdlib-server', + SecurePassword: 'next/stdlib-server', + hash256: 'next/stdlib-server', + generateToken: 'next/stdlib-server', + resolver: 'next/stdlib-server', + connectMiddleware: 'next/stdlib-server', + + getAntiCSRFToken: 'next/data-client', + useSession: 'next/data-client', + useAuthenticatedSession: 'next/data-client', + useRedirectAuthenticated: 'next/data-client', + useAuthorize: 'next/data-client', + useQuery: 'next/data-client', + usePaginatedQuery: 'next/data-client', + useInfiniteQuery: 'next/data-client', + useMutation: 'next/data-client', + queryClient: 'next/data-client', + getQueryKey: 'next/data-client', + getInfiniteQueryKey: 'next/data-client', + invalidateQuery: 'next/data-client', + setQueryData: 'next/data-client', + useQueryErrorResetBoundary: 'next/data-client', + QueryClient: 'next/data-client', + dehydrate: 'next/data-client', + invoke: 'next/data-client', + + Head: 'next/head', + + App: 'next/app', + + dynamic: 'next/dynamic', + noSSR: 'next/dynamic', + + getConfig: 'next/config', + setConfig: 'next/config', + + ErrorComponent: 'next/error', +} + +function RewriteImports(babel: BabelType): PluginObj { + const { types: t } = babel + + return { + name: 'RewriteImports', + visitor: { + ImportDeclaration(path) { + if ( + !looksLike(path, { + node: { + source: { value: 'blitz' }, + }, + }) + ) { + return + } + + path.node.source = t.stringLiteral(defaultImportSource) + + const specifierIndexesToRemove: number[] = [] + path.node.specifiers.slice().forEach((specifier, index) => { + if (!t.isImportSpecifier(specifier)) return + const importedName = t.isStringLiteral(specifier.imported) + ? specifier.imported.value + : specifier.imported.name + if (importedName in specialImports) { + path.insertAfter( + t.importDeclaration( + [specifier], + t.stringLiteral(specialImports[importedName]) + ) + ) + + specifierIndexesToRemove.push(index) + } + }) + specifierIndexesToRemove.reverse().forEach((index) => { + path.node.specifiers.splice(index, 1) + }) + if (!path.node.specifiers.length) { + path.remove() + } + }, + }, + } +} + +function looksLike(a: any, b: any): boolean { + return ( + a && + b && + Object.keys(b).every((bKey) => { + const bVal = b[bKey] + const aVal = a[bKey] + if (typeof bVal === 'function') { + return bVal(aVal) + } + return isPrimitive(bVal) ? bVal === aVal : looksLike(aVal, bVal) + }) + ) +} +function isPrimitive(val: any) { + return val == null || /^[sbn]/.test(typeof val) +} + +// eslint-disable-next-line import/no-default-export +export default RewriteImports diff --git a/nextjs/packages/next/build/babel/plugins/types.d.ts b/nextjs/packages/next/build/babel/plugins/types.d.ts new file mode 100644 index 0000000000..ef01a96e72 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/types.d.ts @@ -0,0 +1,9 @@ +declare module '@babel/helper-module-imports' { + import { NodePath, types } from '@babel/core' + + function addNamed( + path: NodePath, + named: string, + source: string + ): types.Identifier +} diff --git a/nextjs/packages/next/build/babel/plugins/utils.ts b/nextjs/packages/next/build/babel/plugins/utils.ts new file mode 100644 index 0000000000..520a6ff441 --- /dev/null +++ b/nextjs/packages/next/build/babel/plugins/utils.ts @@ -0,0 +1,78 @@ +import { NodePath, PluginPass, types as t } from 'next/dist/compiled/babel/core' +import { addNamed as addNamedImport } from '@babel/helper-module-imports' + +export function functionDeclarationToExpression( + declaration: t.FunctionDeclaration +) { + return t.functionExpression( + declaration.id, + declaration.params, + declaration.body, + declaration.generator, + declaration.async + ) +} + +export function classDeclarationToExpression(declaration: t.ClassDeclaration) { + return t.classExpression( + declaration.id, + declaration.superClass, + declaration.body, + declaration.decorators + ) +} + +export function getFileName(state: PluginPass) { + const { filename, cwd } = state + + if (!filename) { + return undefined + } + + if (cwd && filename.startsWith(cwd)) { + return filename.slice(cwd.length) + } + + return filename +} + +export function wrapExportDefaultDeclaration( + path: NodePath, + HOFName: string, + importFrom: string +) { + // eslint-disable-next-line no-shadow + function wrapInHOF(path: NodePath, expr: t.Expression) { + return t.callExpression(addNamedImport(path, HOFName, importFrom), [expr]) + } + + const { node } = path + + if ( + t.isIdentifier(node.declaration) || + t.isFunctionExpression(node.declaration) || + t.isCallExpression(node.declaration) + ) { + node.declaration = wrapInHOF(path, node.declaration) + } else if ( + t.isFunctionDeclaration(node.declaration) || + t.isClassDeclaration(node.declaration) + ) { + if (node.declaration.id) { + path.insertBefore(node.declaration) + node.declaration = wrapInHOF(path, node.declaration.id) + } else { + if (t.isFunctionDeclaration(node.declaration)) { + node.declaration = wrapInHOF( + path, + functionDeclarationToExpression(node.declaration) + ) + } else { + node.declaration = wrapInHOF( + path, + classDeclarationToExpression(node.declaration) + ) + } + } + } +} diff --git a/nextjs/packages/next/build/babel/preset.ts b/nextjs/packages/next/build/babel/preset.ts new file mode 100644 index 0000000000..e807838b27 --- /dev/null +++ b/nextjs/packages/next/build/babel/preset.ts @@ -0,0 +1,206 @@ +import { PluginItem } from 'next/dist/compiled/babel/core' +import { dirname } from 'path' + +const isLoadIntentTest = process.env.NODE_ENV === 'test' +const isLoadIntentDevelopment = process.env.NODE_ENV === 'development' + +type StyledJsxPlugin = [string, any] | string +type StyledJsxBabelOptions = + | { + plugins?: StyledJsxPlugin[] + 'babel-test'?: boolean + } + | undefined + +// Resolve styled-jsx plugins +function styledJsxOptions(options: StyledJsxBabelOptions) { + if (!options) { + return {} + } + + if (!Array.isArray(options.plugins)) { + return options + } + + options.plugins = options.plugins.map( + (plugin: StyledJsxPlugin): StyledJsxPlugin => { + if (Array.isArray(plugin)) { + const [name, pluginOptions] = plugin + return [require.resolve(name), pluginOptions] + } + + return require.resolve(plugin) + } + ) + + return options +} + +type NextBabelPresetOptions = { + 'preset-env'?: any + 'preset-react'?: any + 'class-properties'?: any + 'transform-runtime'?: any + 'styled-jsx'?: StyledJsxBabelOptions + 'preset-typescript'?: any +} + +type BabelPreset = { + presets?: PluginItem[] | null + plugins?: PluginItem[] | null + sourceType?: 'script' | 'module' | 'unambiguous' + overrides?: Array<{ test: RegExp } & Omit> +} + +// Taken from https://github.com/babel/babel/commit/d60c5e1736543a6eac4b549553e107a9ba967051#diff-b4beead8ad9195361b4537601cc22532R158 +function supportsStaticESM(caller: any): boolean { + return !!caller?.supportsStaticESM +} + +export default ( + api: any, + options: NextBabelPresetOptions = {} +): BabelPreset => { + const supportsESM = api.caller(supportsStaticESM) + const isServer = api.caller((caller: any) => !!caller && caller.isServer) + const isCallerDevelopment = api.caller((caller: any) => caller?.isDev) + + // Look at external intent if used without a caller (e.g. via Jest): + const isTest = isCallerDevelopment == null && isLoadIntentTest + + // Look at external intent if used without a caller (e.g. Storybook): + const isDevelopment = + isCallerDevelopment === true || + (isCallerDevelopment == null && isLoadIntentDevelopment) + + // Default to production mode if not `test` nor `development`: + const isProduction = !(isTest || isDevelopment) + + const isBabelLoader = api.caller( + (caller: any) => + !!caller && + (caller.name === 'babel-loader' || + caller.name === 'next-babel-turbo-loader') + ) + + const useJsxRuntime = + options['preset-react']?.runtime === 'automatic' || + (Boolean(api.caller((caller: any) => !!caller && caller.hasJsxRuntime)) && + options['preset-react']?.runtime !== 'classic') + + const presetEnvConfig = { + // In the test environment `modules` is often needed to be set to true, babel figures that out by itself using the `'auto'` option + // In production/development this option is set to `false` so that webpack can handle import/export with tree-shaking + modules: 'auto', + exclude: ['transform-typeof-symbol'], + include: [ + '@babel/plugin-proposal-optional-chaining', + '@babel/plugin-proposal-nullish-coalescing-operator', + ], + ...options['preset-env'], + } + + // When transpiling for the server or tests, target the current Node version + // if not explicitly specified: + if ( + (isServer || isTest) && + (!presetEnvConfig.targets || + !( + typeof presetEnvConfig.targets === 'object' && + 'node' in presetEnvConfig.targets + )) + ) { + presetEnvConfig.targets = { + // Targets the current process' version of Node. This requires apps be + // built and deployed on the same version of Node. + // This is the same as using "current" but explicit + node: process.versions.node, + } + } + + return { + sourceType: 'unambiguous', + presets: [ + [require('next/dist/compiled/babel/preset-env'), presetEnvConfig], + [ + require('next/dist/compiled/babel/preset-react'), + { + // This adds @babel/plugin-transform-react-jsx-source and + // @babel/plugin-transform-react-jsx-self automatically in development + development: isDevelopment || isTest, + ...(useJsxRuntime ? { runtime: 'automatic' } : { pragma: '__jsx' }), + ...options['preset-react'], + }, + ], + [ + require('next/dist/compiled/babel/preset-typescript'), + { allowNamespaces: true, ...options['preset-typescript'] }, + ], + ], + plugins: [ + !useJsxRuntime && [ + require('./plugins/jsx-pragma'), + { + // This produces the following injected import for modules containing JSX: + // import React from 'react'; + // var __jsx = React.createElement; + module: 'react', + importAs: 'React', + pragma: '__jsx', + property: 'createElement', + }, + ], + [ + require('./plugins/optimize-hook-destructuring'), + { + // only optimize hook functions imported from React/Preact + lib: true, + }, + ], + require('next/dist/compiled/babel/plugin-syntax-dynamic-import'), + require('./plugins/react-loadable-plugin'), + [ + require('next/dist/compiled/babel/plugin-proposal-class-properties'), + options['class-properties'] || {}, + ], + [ + require('next/dist/compiled/babel/plugin-proposal-object-rest-spread'), + { + useBuiltIns: true, + }, + ], + !isServer && [ + require('next/dist/compiled/babel/plugin-transform-runtime'), + { + corejs: false, + helpers: true, + regenerator: true, + useESModules: supportsESM && presetEnvConfig.modules !== 'commonjs', + absoluteRuntime: isBabelLoader + ? dirname(require.resolve('@babel/runtime/package.json')) + : undefined, + ...options['transform-runtime'], + }, + ], + [ + isTest && options['styled-jsx'] && options['styled-jsx']['babel-test'] + ? require('styled-jsx/babel-test') + : require('styled-jsx/babel'), + styledJsxOptions(options['styled-jsx']), + ], + require('./plugins/amp-attributes'), + isProduction && [ + require('next/dist/compiled/babel/plugin-transform-react-remove-prop-types'), + { + removeImport: true, + }, + ], + isServer && require('next/dist/compiled/babel/plugin-syntax-bigint'), + // Always compile numeric separator because the resulting number is + // smaller. + require('next/dist/compiled/babel/plugin-proposal-numeric-separator'), + require('next/dist/compiled/babel/plugin-proposal-export-namespace-from'), + !isTest && require('./plugins/rewrite-imports'), + ].filter(Boolean), + } +} diff --git a/nextjs/packages/next/build/compiler.ts b/nextjs/packages/next/build/compiler.ts new file mode 100644 index 0000000000..6fbf3cfa0a --- /dev/null +++ b/nextjs/packages/next/build/compiler.ts @@ -0,0 +1,73 @@ +import { webpack } from 'next/dist/compiled/webpack/webpack' + +export type CompilerResult = { + errors: string[] + warnings: string[] +} + +function generateStats( + result: CompilerResult, + stat: webpack.Stats +): CompilerResult { + const { errors, warnings } = stat.toJson('errors-warnings') + if (errors.length > 0) { + result.errors.push(...errors) + } + + if (warnings.length > 0) { + result.warnings.push(...warnings) + } + + return result +} + +// Webpack 5 requires the compiler to be closed (to save caches) +// Webpack 4 does not have this close method so in order to be backwards compatible we check if it exists +function closeCompiler(compiler: webpack.Compiler | webpack.MultiCompiler) { + return new Promise((resolve, reject) => { + if ('close' in compiler) { + // @ts-ignore Close only exists on the compiler in webpack 5 + return compiler.close((err: any) => (err ? reject(err) : resolve())) + } + + resolve() + }) +} + +export function runCompiler( + config: webpack.Configuration | webpack.Configuration[] +): Promise { + return new Promise((resolve, reject) => { + const compiler = webpack(config) + compiler.run( + ( + err: Error, + statsOrMultiStats: { stats: webpack.Stats[] } | webpack.Stats + ) => { + closeCompiler(compiler).then(() => { + if (err) { + const reason = err?.toString() + if (reason) { + return resolve({ errors: [reason], warnings: [] }) + } + return reject(err) + } + + if ('stats' in statsOrMultiStats) { + const result: CompilerResult = statsOrMultiStats.stats.reduce( + generateStats, + { errors: [], warnings: [] } + ) + return resolve(result) + } + + const result = generateStats( + { errors: [], warnings: [] }, + statsOrMultiStats + ) + return resolve(result) + }) + } + ) + }) +} diff --git a/nextjs/packages/next/build/entries.ts b/nextjs/packages/next/build/entries.ts new file mode 100644 index 0000000000..2798c14360 --- /dev/null +++ b/nextjs/packages/next/build/entries.ts @@ -0,0 +1,179 @@ +import chalk from 'chalk' +import { posix, join } from 'path' +import { stringify } from 'querystring' +import { API_ROUTE, DOT_NEXT_ALIAS, PAGES_DIR_ALIAS } from '../lib/constants' +import { __ApiPreviewProps } from '../server/api-utils' +import { isTargetLikeServerless } from '../server/config' +import { normalizePagePath } from '../server/normalize-page-path' +import { warn } from './output/log' +import { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-loader' +import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader' +import { LoadedEnvFiles } from '@blitzjs/env' +import { convertPageFilePathToRoutePath } from './utils' +import { NextConfigComplete } from '../server/config-shared' + +type PagesMapping = { + [page: string]: string +} + +export function createPagesMapping( + pagePaths: string[], + pageExtensions: string[] +): PagesMapping { + const previousPages: PagesMapping = {} + const pages: PagesMapping = pagePaths.reduce( + (result: PagesMapping, pagePath): PagesMapping => { + let page = `${convertPageFilePathToRoutePath( + pagePath, + pageExtensions + ).replace(/\\/g, '/')}`.replace(/\/index$/, '') + + let pageKey = page === '' ? '/' : page + + if (pageKey in result) { + warn( + `Duplicate page detected. ${chalk.cyan( + previousPages[pageKey] + )} and ${chalk.cyan(pagePath)} both resolve to ${chalk.cyan( + pageKey + )}.` + ) + } else { + previousPages[pageKey] = pagePath + } + result[pageKey] = join(PAGES_DIR_ALIAS, pagePath).replace(/\\/g, '/') + return result + }, + {} + ) + + pages['/_app'] = pages['/_app'] || 'next/dist/pages/_app' + pages['/_error'] = pages['/_error'] || 'next/dist/pages/_error' + pages['/_document'] = pages['/_document'] || 'next/dist/pages/_document' + + return pages +} + +export type WebpackEntrypoints = { + [bundle: string]: + | string + | string[] + | { + import: string | string[] + dependOn?: string | string[] + } +} + +type Entrypoints = { + client: WebpackEntrypoints + server: WebpackEntrypoints +} + +interface EntrypointsCtx { + pagesDir: string +} + +export function createEntrypoints( + pages: PagesMapping, + target: 'server' | 'serverless' | 'experimental-serverless-trace', + buildId: string, + previewMode: __ApiPreviewProps, + config: NextConfigComplete, + loadedEnvFiles: LoadedEnvFiles, + { pagesDir }: EntrypointsCtx +): Entrypoints { + const client: WebpackEntrypoints = {} + const server: WebpackEntrypoints = {} + + const hasRuntimeConfig = + Object.keys(config.publicRuntimeConfig).length > 0 || + Object.keys(config.serverRuntimeConfig).length > 0 + + const defaultServerlessOptions = { + absoluteAppPath: pages['/_app'], + absoluteDocumentPath: pages['/_document'], + absoluteErrorPath: pages['/_error'], + absolute404Path: pages['/404'] || '', + distDir: DOT_NEXT_ALIAS, + pagesDir, + buildId, + assetPrefix: config.assetPrefix, + generateEtags: config.generateEtags ? 'true' : '', + poweredByHeader: config.poweredByHeader, + canonicalBase: config.amp.canonicalBase || '', + basePath: config.basePath, + runtimeConfig: hasRuntimeConfig + ? JSON.stringify({ + publicRuntimeConfig: config.publicRuntimeConfig, + serverRuntimeConfig: config.serverRuntimeConfig, + }) + : '', + previewProps: JSON.stringify(previewMode), + // base64 encode to make sure contents don't break webpack URL loading + loadedEnvFiles: Buffer.from(JSON.stringify(loadedEnvFiles)).toString( + 'base64' + ), + i18n: config.i18n ? JSON.stringify(config.i18n) : '', + } + + Object.keys(pages).forEach((page) => { + const absolutePagePath = pages[page] + const bundleFile = normalizePagePath(page) + const isApiRoute = page.match(API_ROUTE) + + const clientBundlePath = posix.join('pages', bundleFile) + const serverBundlePath = posix.join('pages', bundleFile) + + const isLikeServerless = isTargetLikeServerless(target) + + if (isApiRoute && isLikeServerless) { + const serverlessLoaderOptions: ServerlessLoaderQuery = { + page, + absolutePagePath, + ...defaultServerlessOptions, + } + server[serverBundlePath] = `next-serverless-loader?${stringify( + serverlessLoaderOptions + )}!` + } else if (isApiRoute || target === 'server') { + server[serverBundlePath] = [absolutePagePath] + } else if (isLikeServerless && page !== '/_app' && page !== '/_document') { + const serverlessLoaderOptions: ServerlessLoaderQuery = { + page, + absolutePagePath, + ...defaultServerlessOptions, + } + server[serverBundlePath] = `next-serverless-loader?${stringify( + serverlessLoaderOptions + )}!` + } + + if (page === '/_document') { + return + } + + if (!isApiRoute) { + const pageLoaderOpts: ClientPagesLoaderOptions = { + page, + absolutePagePath, + } + const pageLoader = `next-client-pages-loader?${stringify( + pageLoaderOpts + )}!` + + // Make sure next/router is a dependency of _app or else chunk splitting + // might cause the router to not be able to load causing hydration + // to fail + + client[clientBundlePath] = + page === '/_app' + ? [pageLoader, require.resolve('../client/router')] + : pageLoader + } + }) + + return { + client, + server, + } +} diff --git a/nextjs/packages/next/build/generate-build-id.ts b/nextjs/packages/next/build/generate-build-id.ts new file mode 100644 index 0000000000..aed1d6c864 --- /dev/null +++ b/nextjs/packages/next/build/generate-build-id.ts @@ -0,0 +1,22 @@ +export async function generateBuildId( + generate: () => string | null, + fallback: () => string +): Promise { + let buildId = await generate() + // If there's no buildId defined we'll fall back + if (buildId === null) { + // We also create a new buildId if it contains the word `ad` to avoid false + // positives with ad blockers + while (!buildId || /ad/i.test(buildId)) { + buildId = fallback() + } + } + + if (typeof buildId !== 'string') { + throw new Error( + 'generateBuildId did not return a string. https://nextjs.org/docs/messages/generatebuildid-not-a-string' + ) + } + + return buildId.trim() +} diff --git a/nextjs/packages/next/build/index.ts b/nextjs/packages/next/build/index.ts new file mode 100644 index 0000000000..cf678d9477 --- /dev/null +++ b/nextjs/packages/next/build/index.ts @@ -0,0 +1,1677 @@ +import { loadEnvConfig } from '@blitzjs/env' +import chalk from 'chalk' +import crypto from 'crypto' +import { promises, writeFileSync } from 'fs' +import { Worker } from '../lib/worker' +import devalue from 'next/dist/compiled/devalue' +import escapeStringRegexp from 'next/dist/compiled/escape-string-regexp' +import findUp from 'next/dist/compiled/find-up' +import { nanoid } from 'next/dist/compiled/nanoid/index.cjs' +import { pathToRegexp } from 'next/dist/compiled/path-to-regexp' +import path from 'path' +import formatWebpackMessages from '../client/dev/error-overlay/format-webpack-messages' +import { + STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR, + PUBLIC_DIR_MIDDLEWARE_CONFLICT, +} from '../lib/constants' +import { fileExists } from '../lib/file-exists' +import loadCustomRoutes, { + CustomRoutes, + getRedirectStatus, + modifyRouteRegex, + normalizeRouteRegex, + Redirect, + Rewrite, + RouteType, +} from '../lib/load-custom-routes' +import { nonNullable } from '../lib/non-nullable' +import { recursiveDelete } from '../lib/recursive-delete' +import { verifyAndLint } from '../lib/verifyAndLint' +import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup' +import { + BUILD_ID_FILE, + BUILD_MANIFEST, + CLIENT_STATIC_FILES_PATH, + EXPORT_DETAIL, + EXPORT_MARKER, + FONT_MANIFEST, + IMAGES_MANIFEST, + PAGES_MANIFEST, + PHASE_PRODUCTION_BUILD, + PRERENDER_MANIFEST, + REACT_LOADABLE_MANIFEST, + ROUTES_MANIFEST, + SERVERLESS_DIRECTORY, + SERVER_DIRECTORY, + SERVER_FILES_MANIFEST, + STATIC_STATUS_PAGES, +} from '../shared/lib/constants' +import { + getRouteRegex, + getSortedRoutes, + isDynamicRoute, +} from '../shared/lib/router/utils' +import { __ApiPreviewProps } from '../server/api-utils' +import loadConfig, { isTargetLikeServerless } from '../server/config' +import { BuildManifest } from '../server/get-page-files' +import '../server/node-polyfill-fetch' +import { normalizePagePath } from '../server/normalize-page-path' +import { getPagePath } from '../server/require' +import * as ciEnvironment from '../telemetry/ci-info' +import { + eventBuildCompleted, + eventBuildOptimize, + eventCliSession, + eventNextPlugins, + eventTypeCheckCompleted, +} from '../telemetry/events' +import { Telemetry } from '../telemetry/storage' +import { CompilerResult, runCompiler } from './compiler' +import { createEntrypoints, createPagesMapping } from './entries' +import { generateBuildId } from './generate-build-id' +import { isWriteable } from './is-writeable' +import * as Log from './output/log' +import createSpinner from './spinner' +import { trace, setGlobal } from '../telemetry/trace' +import { + collectPages, + detectConflictingPaths, + computeFromManifest, + getJsPageSizeInKb, + PageInfo, + printCustomRoutes, + printTreeView, + getCssFilePaths, +} from './utils' +import getBaseWebpackConfig from './webpack-config' +import { PagesManifest } from './webpack/plugins/pages-manifest-plugin' +import { writeBuildId } from './write-build-id' +import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' +import { isWebpack5 } from 'next/dist/compiled/webpack/webpack' +import { NextConfigComplete } from '../server/config-shared' +import { saveRouteManifest } from './routes' + +const staticCheckWorker = require.resolve('./utils') + +export type SsgRoute = { + initialRevalidateSeconds: number | false + srcRoute: string | null + dataRoute: string +} + +export type DynamicSsgRoute = { + routeRegex: string + fallback: string | null | false + dataRoute: string + dataRouteRegex: string +} + +export type PrerenderManifest = { + version: 3 + routes: { [route: string]: SsgRoute } + dynamicRoutes: { [route: string]: DynamicSsgRoute } + notFoundRoutes: string[] + preview: __ApiPreviewProps +} + +export default async function build( + dir: string, + conf = null, + reactProductionProfiling = false, + debugOutput = false, + runLint = true +): Promise { + const nextBuildSpan = trace('next-build') + + return nextBuildSpan.traceAsyncFn(async () => { + // attempt to load global env values so they are available in blitz.config.js + const { loadedEnvFiles } = nextBuildSpan + .traceChild('load-dotenv') + .traceFn(() => loadEnvConfig(dir, false, Log)) + + const config: NextConfigComplete = await nextBuildSpan + .traceChild('load-next-config') + .traceAsyncFn(() => loadConfig(PHASE_PRODUCTION_BUILD, dir, conf)) + const { target } = config + const buildId: string = await nextBuildSpan + .traceChild('generate-buildid') + .traceAsyncFn(() => generateBuildId(config.generateBuildId, nanoid)) + const distDir = path.join(dir, config.distDir) + + const customRoutes: CustomRoutes = await nextBuildSpan + .traceChild('load-custom-routes') + .traceAsyncFn(() => loadCustomRoutes(config)) + + const { headers, rewrites, redirects } = customRoutes + + const cacheDir = path.join(distDir, 'cache') + if (ciEnvironment.isCI && !ciEnvironment.hasNextSupport) { + const hasCache = await fileExists(cacheDir) + + if (!hasCache) { + // Intentionally not piping to stderr in case people fail in CI when + // stderr is detected. + console.log( + `${Log.prefixes.warn} No build cache found. Please configure build caching for faster rebuilds. Read more: https://nextjs.org/docs/messages/no-cache` + ) + } + } + + const telemetry = new Telemetry({ distDir }) + setGlobal('telemetry', telemetry) + + const publicDir = path.join(dir, 'public') + const pagesDir = dir + const hasPublicDir = await fileExists(publicDir) + + telemetry.record( + eventCliSession(PHASE_PRODUCTION_BUILD, dir, { + webpackVersion: isWebpack5 ? 5 : 4, + cliCommand: 'build', + isSrcDir: path.relative(dir, pagesDir!).startsWith('src'), + hasNowJson: !!(await findUp('now.json', { cwd: dir })), + isCustomServer: null, + }) + ) + + eventNextPlugins(path.resolve(dir)).then((events) => + telemetry.record(events) + ) + + const routeManifestSpinner = createSpinner({ + prefixText: `${Log.prefixes.info} Generating route manifest`, + }) + await saveRouteManifest(pagesDir, config) + routeManifestSpinner?.stopAndPersist() + + const ignoreTypeScriptErrors = Boolean(config.typescript?.ignoreBuildErrors) + const typeCheckStart = process.hrtime() + const typeCheckingSpinner = createSpinner({ + prefixText: `${Log.prefixes.info} ${ + ignoreTypeScriptErrors + ? 'Skipping validation of types' + : 'Checking validity of types' + }`, + }) + + const verifyResult = await nextBuildSpan + .traceChild('verify-typescript-setup') + .traceAsyncFn(() => + verifyTypeScriptSetup( + dir, + pagesDir, + !ignoreTypeScriptErrors, + !config.images.disableStaticImages, + cacheDir + ) + ) + + const typeCheckEnd = process.hrtime(typeCheckStart) + + if (!ignoreTypeScriptErrors) { + telemetry.record( + eventTypeCheckCompleted({ + durationInSeconds: typeCheckEnd[0], + typescriptVersion: verifyResult.version, + inputFilesCount: verifyResult.result?.inputFilesCount, + totalFilesCount: verifyResult.result?.totalFilesCount, + incremental: verifyResult.result?.incremental, + }) + ) + } + + if (typeCheckingSpinner) { + typeCheckingSpinner.stopAndPersist() + } + + const ignoreESLint = Boolean(config.eslint?.ignoreDuringBuilds) + const lintDirs = config.eslint?.dirs + if (!ignoreESLint && runLint) { + await nextBuildSpan + .traceChild('verify-and-lint') + .traceAsyncFn(async () => { + await verifyAndLint( + dir, + lintDirs, + config.experimental.cpus, + config.experimental.workerThreads, + telemetry + ) + }) + } + + const buildSpinner = createSpinner({ + prefixText: `${Log.prefixes.info} Creating an optimized production build`, + }) + + const isLikeServerless = isTargetLikeServerless(target) + + const pagePaths: string[] = await nextBuildSpan + .traceChild('collect-pages') + .traceAsyncFn(() => collectPages(pagesDir, config.pageExtensions)) + + // needed for static exporting since we want to replace with HTML + // files + const allStaticPages = new Set() + let allPageInfos = new Map() + + const previewProps: __ApiPreviewProps = { + previewModeId: crypto.randomBytes(16).toString('hex'), + previewModeSigningKey: crypto.randomBytes(32).toString('hex'), + previewModeEncryptionKey: crypto.randomBytes(32).toString('hex'), + } + + const mappedPages = nextBuildSpan + .traceChild('create-pages-mapping') + .traceFn(() => createPagesMapping(pagePaths, config.pageExtensions)) + const entrypoints = nextBuildSpan + .traceChild('create-entrypoints') + .traceFn(() => + createEntrypoints( + mappedPages, + target, + buildId, + previewProps, + config, + loadedEnvFiles, + { pagesDir } + ) + ) + const pageKeys = Object.keys(mappedPages) + const conflictingPublicFiles: string[] = [] + const hasCustomErrorPage: boolean = mappedPages['/_error'].startsWith( + 'private-next-pages' + ) + const hasPages404 = Boolean( + mappedPages['/404'] && + mappedPages['/404'].startsWith('private-next-pages') + ) + + if (hasPublicDir) { + const hasPublicUnderScoreNextDir = await fileExists( + path.join(publicDir, '_next') + ) + if (hasPublicUnderScoreNextDir) { + throw new Error(PUBLIC_DIR_MIDDLEWARE_CONFLICT) + } + } + + await nextBuildSpan + .traceChild('public-dir-conflict-check') + .traceAsyncFn(async () => { + // Check if pages conflict with files in `public` + // Only a page of public file can be served, not both. + for (const page in mappedPages) { + const hasPublicPageFile = await fileExists( + path.join(publicDir, page === '/' ? '/index' : page), + 'file' + ) + if (hasPublicPageFile) { + conflictingPublicFiles.push(page) + } + } + + const numConflicting = conflictingPublicFiles.length + + if (numConflicting) { + throw new Error( + `Conflicting public and page file${ + numConflicting === 1 ? ' was' : 's were' + } found. https://nextjs.org/docs/messages/conflicting-public-file-page\n${conflictingPublicFiles.join( + '\n' + )}` + ) + } + }) + + const nestedReservedPages = pageKeys.filter((page) => { + return ( + page.match(/\/(_app|_document|_error)$/) && path.dirname(page) !== '/' + ) + }) + + if (nestedReservedPages.length) { + Log.warn( + `The following reserved Blitz.js pages were detected not directly under the pages directory:\n` + + nestedReservedPages.join('\n') + + `\nSee more info here: https://nextjs.org/docs/messages/nested-reserved-page\n` + ) + } + + const restrictedRedirectPaths = ['/_next'].map((p) => + config.basePath ? `${config.basePath}${p}` : p + ) + + const buildCustomRoute = ( + r: { + source: string + locale?: false + basePath?: false + statusCode?: number + destination?: string + }, + type: RouteType + ) => { + const keys: any[] = [] + + const routeRegex = pathToRegexp(r.source, keys, { + strict: true, + sensitive: false, + delimiter: '/', // default is `/#?`, but Next does not pass query info + }) + let regexSource = routeRegex.source + + if (!(r as any).internal) { + regexSource = modifyRouteRegex( + routeRegex.source, + type === 'redirect' ? restrictedRedirectPaths : undefined + ) + } + + return { + ...r, + ...(type === 'redirect' + ? { + statusCode: getRedirectStatus(r as Redirect), + permanent: undefined, + } + : {}), + regex: normalizeRouteRegex(regexSource), + } + } + + const routesManifestPath = path.join(distDir, ROUTES_MANIFEST) + const routesManifest: { + version: number + pages404: boolean + basePath: string + redirects: Array> + rewrites: + | Array> + | { + beforeFiles: Array> + afterFiles: Array> + fallback: Array> + } + headers: Array> + dynamicRoutes: Array<{ + page: string + regex: string + namedRegex?: string + routeKeys?: { [key: string]: string } + }> + dataRoutes: Array<{ + page: string + routeKeys?: { [key: string]: string } + dataRouteRegex: string + namedDataRouteRegex?: string + }> + i18n?: { + domains?: Array<{ + http?: true + domain: string + locales?: string[] + defaultLocale: string + }> + locales: string[] + defaultLocale: string + localeDetection?: false + } + } = nextBuildSpan.traceChild('generate-routes-manifest').traceFn(() => ({ + version: 3, + pages404: true, + basePath: config.basePath, + redirects: redirects.map((r: any) => buildCustomRoute(r, 'redirect')), + headers: headers.map((r: any) => buildCustomRoute(r, 'header')), + dynamicRoutes: getSortedRoutes(pageKeys) + .filter(isDynamicRoute) + .map((page) => { + const routeRegex = getRouteRegex(page) + return { + page, + regex: normalizeRouteRegex(routeRegex.re.source), + routeKeys: routeRegex.routeKeys, + namedRegex: routeRegex.namedRegex, + } + }), + dataRoutes: [], + i18n: config.i18n || undefined, + })) + + if (rewrites.beforeFiles.length === 0 && rewrites.fallback.length === 0) { + routesManifest.rewrites = rewrites.afterFiles.map((r: any) => + buildCustomRoute(r, 'rewrite') + ) + } else { + routesManifest.rewrites = { + beforeFiles: rewrites.beforeFiles.map((r: any) => + buildCustomRoute(r, 'rewrite') + ), + afterFiles: rewrites.afterFiles.map((r: any) => + buildCustomRoute(r, 'rewrite') + ), + fallback: rewrites.fallback.map((r: any) => + buildCustomRoute(r, 'rewrite') + ), + } + } + const combinedRewrites: Rewrite[] = [ + ...rewrites.beforeFiles, + ...rewrites.afterFiles, + ...rewrites.fallback, + ] + + const distDirCreated = await nextBuildSpan + .traceChild('create-dist-dir') + .traceAsyncFn(async () => { + try { + await promises.mkdir(distDir, { recursive: true }) + return true + } catch (err) { + if (err.code === 'EPERM') { + return false + } + throw err + } + }) + + if (!distDirCreated || !(await isWriteable(distDir))) { + throw new Error( + '> Build directory is not writeable. https://nextjs.org/docs/messages/build-dir-not-writeable' + ) + } + + if (config.cleanDistDir) { + await recursiveDelete(distDir, /^cache/) + } + + // We need to write the manifest with rewrites before build + // so serverless can import the manifest + await nextBuildSpan + .traceChild('write-routes-manifest') + .traceAsyncFn(() => + promises.writeFile( + routesManifestPath, + JSON.stringify(routesManifest), + 'utf8' + ) + ) + + const manifestPath = path.join( + distDir, + isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, + PAGES_MANIFEST + ) + + const requiredServerFiles = nextBuildSpan + .traceChild('generate-required-server-files') + .traceFn(() => ({ + version: 1, + config: { + ...config, + compress: false, + configFile: undefined, + }, + appDir: dir, + files: [ + ROUTES_MANIFEST, + path.relative(distDir, manifestPath), + BUILD_MANIFEST, + PRERENDER_MANIFEST, + REACT_LOADABLE_MANIFEST, + config.optimizeFonts + ? path.join( + isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, + FONT_MANIFEST + ) + : null, + BUILD_ID_FILE, + ] + .filter(nonNullable) + .map((file) => path.join(config.distDir, file)), + ignore: [] as string[], + })) + + const configs = await nextBuildSpan + .traceChild('generate-webpack-config') + .traceAsyncFn(() => + Promise.all([ + getBaseWebpackConfig(dir, { + buildId, + reactProductionProfiling, + isServer: false, + config, + target, + pagesDir, + entrypoints: entrypoints.client, + rewrites, + }), + getBaseWebpackConfig(dir, { + buildId, + reactProductionProfiling, + isServer: true, + config, + target, + pagesDir, + entrypoints: entrypoints.server, + rewrites, + }), + ]) + ) + + const clientConfig = configs[0] + + if ( + clientConfig.optimization && + (clientConfig.optimization.minimize !== true || + (clientConfig.optimization.minimizer && + clientConfig.optimization.minimizer.length === 0)) + ) { + Log.warn( + `Production code optimization has been disabled in your project. Read more: https://nextjs.org/docs/messages/minification-disabled` + ) + } + + const webpackBuildStart = process.hrtime() + + let result: CompilerResult = { warnings: [], errors: [] } + // We run client and server compilation separately to optimize for memory usage + await nextBuildSpan + .traceChild('run-webpack-compiler') + .traceAsyncFn(async () => { + const clientResult = await runCompiler(clientConfig) + // Fail build if clientResult contains errors + if (clientResult.errors.length > 0) { + result = { + warnings: [...clientResult.warnings], + errors: [...clientResult.errors], + } + } else { + const serverResult = await runCompiler(configs[1]) + result = { + warnings: [...clientResult.warnings, ...serverResult.warnings], + errors: [...clientResult.errors, ...serverResult.errors], + } + } + }) + + const webpackBuildEnd = process.hrtime(webpackBuildStart) + if (buildSpinner) { + buildSpinner.stopAndPersist() + } + + result = nextBuildSpan + .traceChild('format-webpack-messages') + .traceFn(() => formatWebpackMessages(result)) + + if (result.errors.length > 0) { + // Only keep the first error. Others are often indicative + // of the same problem, but confuse the reader with noise. + if (result.errors.length > 1) { + result.errors.length = 1 + } + const error = result.errors.join('\n\n') + + console.error(chalk.red('Failed to compile.\n')) + + if ( + error.indexOf('private-next-pages') > -1 && + error.indexOf('does not contain a default export') > -1 + ) { + const page_name_regex = /'private-next-pages\/(?[^']*)'/ + const parsed = page_name_regex.exec(error) + const page_name = parsed && parsed.groups && parsed.groups.page_name + throw new Error( + `webpack build failed: found page without a React Component as default export in pages/${page_name}\n\nSee https://nextjs.org/docs/messages/page-without-valid-component for more info.` + ) + } + + console.error(error) + console.error() + + if ( + error.indexOf('private-next-pages') > -1 || + error.indexOf('__next_polyfill__') > -1 + ) { + throw new Error( + '> webpack config.resolve.alias was incorrectly overridden. https://nextjs.org/docs/messages/invalid-resolve-alias' + ) + } + throw new Error('> Build failed because of webpack errors') + } else { + telemetry.record( + eventBuildCompleted(pagePaths, { + durationInSeconds: webpackBuildEnd[0], + }) + ) + + if (result.warnings.length > 0) { + Log.warn('Compiled with warnings\n') + console.warn(result.warnings.join('\n\n')) + console.warn() + } else { + Log.info('Compiled successfully') + } + } + + const postCompileSpinner = createSpinner({ + prefixText: `${Log.prefixes.info} Collecting page data`, + }) + + const buildManifestPath = path.join(distDir, BUILD_MANIFEST) + + const ssgPages = new Set() + const ssgStaticFallbackPages = new Set() + const ssgBlockingFallbackPages = new Set() + const staticPages = new Set() + const invalidPages = new Set() + const hybridAmpPages = new Set() + const serverPropsPages = new Set() + const additionalSsgPaths = new Map>() + const additionalSsgPathsEncoded = new Map>() + const pageInfos = new Map() + const pagesManifest = JSON.parse( + await promises.readFile(manifestPath, 'utf8') + ) as PagesManifest + const buildManifest = JSON.parse( + await promises.readFile(buildManifestPath, 'utf8') + ) as BuildManifest + + const analysisBegin = process.hrtime() + + const staticCheckSpan = nextBuildSpan.traceChild('static-check') + const { + customAppGetInitialProps, + namedExports, + isNextImageImported, + hasSsrAmpPages, + hasNonStaticErrorPage, + } = await staticCheckSpan.traceAsyncFn(async () => { + process.env.NEXT_PHASE = PHASE_PRODUCTION_BUILD + + const timeout = config.experimental.pageDataCollectionTimeout || 0 + let infoPrinted = false + const staticCheckWorkers = new Worker(staticCheckWorker, { + timeout: timeout * 1000, + onRestart: (_method, [pagePath], attempts) => { + if (attempts >= 2) { + throw new Error( + `Collecting page data for ${pagePath} is still timing out after 2 attempts. See more info here https://nextjs.org/docs/messages/page-data-collection-timeout` + ) + } + Log.warn( + `Restarted collecting page data for ${pagePath} because it took more than ${timeout} seconds` + ) + if (!infoPrinted) { + Log.warn( + 'See more info here https://nextjs.org/docs/messages/page-data-collection-timeout' + ) + infoPrinted = true + } + }, + numWorkers: config.experimental.cpus, + enableWorkerThreads: config.experimental.workerThreads, + exposedMethods: [ + 'hasCustomGetInitialProps', + 'isPageStatic', + 'getNamedExports', + ], + }) as Worker & + Pick< + typeof import('./utils'), + 'hasCustomGetInitialProps' | 'isPageStatic' | 'getNamedExports' + > + + const runtimeEnvConfig = { + publicRuntimeConfig: config.publicRuntimeConfig, + serverRuntimeConfig: config.serverRuntimeConfig, + } + + const nonStaticErrorPageSpan = staticCheckSpan.traceChild( + 'check-static-error-page' + ) + const errorPageHasCustomGetInitialProps = nonStaticErrorPageSpan.traceAsyncFn( + async () => + hasCustomErrorPage && + (await staticCheckWorkers.hasCustomGetInitialProps( + '/_error', + distDir, + isLikeServerless, + runtimeEnvConfig, + false + )) + ) + + const errorPageStaticResult = nonStaticErrorPageSpan.traceAsyncFn( + async () => + hasCustomErrorPage && + staticCheckWorkers.isPageStatic( + '/_error', + distDir, + isLikeServerless, + runtimeEnvConfig, + config.httpAgentOptions, + config.i18n?.locales, + config.i18n?.defaultLocale + ) + ) + + // we don't output _app in serverless mode so use _app export + // from _error instead + const appPageToCheck = isLikeServerless ? '/_error' : '/_app' + + const customAppGetInitialPropsPromise = staticCheckWorkers.hasCustomGetInitialProps( + appPageToCheck, + distDir, + isLikeServerless, + runtimeEnvConfig, + true + ) + + const namedExportsPromise = staticCheckWorkers.getNamedExports( + appPageToCheck, + distDir, + isLikeServerless, + runtimeEnvConfig + ) + + // eslint-disable-next-line no-shadow + let isNextImageImported: boolean | undefined + // eslint-disable-next-line no-shadow + let hasSsrAmpPages = false + + const computedManifestData = await computeFromManifest( + buildManifest, + distDir, + config.experimental.gzipSize + ) + await Promise.all( + pageKeys.map(async (page) => { + const checkPageSpan = staticCheckSpan.traceChild('check-page', { + page, + }) + return checkPageSpan.traceAsyncFn(async () => { + const actualPage = normalizePagePath(page) + const [selfSize, allSize] = await getJsPageSizeInKb( + actualPage, + distDir, + buildManifest, + config.experimental.gzipSize, + computedManifestData + ) + + let isSsg = false + let isStatic = false + let isHybridAmp = false + let ssgPageRoutes: string[] | null = null + + const nonReservedPage = !page.match( + /^\/(_app|_error|_document|api(\/|$))/ + ) + + if (nonReservedPage) { + try { + let isPageStaticSpan = checkPageSpan.traceChild( + 'is-page-static' + ) + let workerResult = await isPageStaticSpan.traceAsyncFn(() => { + return staticCheckWorkers.isPageStatic( + page, + distDir, + isLikeServerless, + runtimeEnvConfig, + config.httpAgentOptions, + config.i18n?.locales, + config.i18n?.defaultLocale, + isPageStaticSpan.id + ) + }) + + if ( + workerResult.isStatic === false && + (workerResult.isHybridAmp || workerResult.isAmpOnly) + ) { + hasSsrAmpPages = true + } + + if (workerResult.isHybridAmp) { + isHybridAmp = true + hybridAmpPages.add(page) + } + + if (workerResult.isNextImageImported) { + isNextImageImported = true + } + + if (workerResult.hasStaticProps) { + ssgPages.add(page) + isSsg = true + + if ( + workerResult.prerenderRoutes && + workerResult.encodedPrerenderRoutes + ) { + additionalSsgPaths.set(page, workerResult.prerenderRoutes) + additionalSsgPathsEncoded.set( + page, + workerResult.encodedPrerenderRoutes + ) + ssgPageRoutes = workerResult.prerenderRoutes + } + + if (workerResult.prerenderFallback === 'blocking') { + ssgBlockingFallbackPages.add(page) + } else if (workerResult.prerenderFallback === true) { + ssgStaticFallbackPages.add(page) + } + } else if (workerResult.hasServerProps) { + serverPropsPages.add(page) + } else if ( + workerResult.isStatic && + (await customAppGetInitialPropsPromise) === false + ) { + staticPages.add(page) + isStatic = true + } + + if (hasPages404 && page === '/404') { + if (!workerResult.isStatic && !workerResult.hasStaticProps) { + throw new Error( + `\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}` + ) + } + // we need to ensure the 404 lambda is present since we use + // it when _app has getInitialProps + if ( + (await customAppGetInitialPropsPromise) && + !workerResult.hasStaticProps + ) { + staticPages.delete(page) + } + } + + if ( + STATIC_STATUS_PAGES.includes(page) && + !workerResult.isStatic && + !workerResult.hasStaticProps + ) { + throw new Error( + `\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}` + ) + } + } catch (err) { + if (err.message !== 'INVALID_DEFAULT_EXPORT') throw err + invalidPages.add(page) + } + } + + pageInfos.set(page, { + size: selfSize, + totalSize: allSize, + static: isStatic, + isSsg, + isHybridAmp, + ssgPageRoutes, + initialRevalidateSeconds: false, + pageDuration: undefined, + ssgPageDurations: undefined, + }) + }) + }) + ) + + const errorPageResult = await errorPageStaticResult + const nonStaticErrorPage = + (await errorPageHasCustomGetInitialProps) || + (errorPageResult && errorPageResult.hasServerProps) + + const returnValue = { + customAppGetInitialProps: await customAppGetInitialPropsPromise, + namedExports: await namedExportsPromise, + isNextImageImported, + hasSsrAmpPages, + hasNonStaticErrorPage: nonStaticErrorPage, + } + + staticCheckWorkers.end() + return returnValue + }) + + if (customAppGetInitialProps) { + console.warn( + chalk.bold.yellow(`Warning: `) + + chalk.yellow( + `You have opted-out of Automatic Static Optimization due to \`getInitialProps\` in \`pages/_app\`. This does not opt-out pages with \`getStaticProps\`` + ) + ) + console.warn( + 'Read more: https://nextjs.org/docs/messages/opt-out-auto-static-optimization\n' + ) + } + + if (!hasSsrAmpPages) { + requiredServerFiles.ignore.push( + path.relative( + dir, + path.join( + path.dirname( + require.resolve( + 'next/dist/compiled/@ampproject/toolbox-optimizer' + ) + ), + '**/*' + ) + ) + ) + } + + if (serverPropsPages.size > 0 || ssgPages.size > 0) { + // We update the routes manifest after the build with the + // data routes since we can't determine these until after build + routesManifest.dataRoutes = getSortedRoutes([ + ...serverPropsPages, + ...ssgPages, + ]).map((page) => { + const pagePath = normalizePagePath(page) + const dataRoute = path.posix.join( + '/_next/data', + buildId, + `${pagePath}.json` + ) + + let dataRouteRegex: string + let namedDataRouteRegex: string | undefined + let routeKeys: { [named: string]: string } | undefined + + if (isDynamicRoute(page)) { + const routeRegex = getRouteRegex(dataRoute.replace(/\.json$/, '')) + + dataRouteRegex = normalizeRouteRegex( + routeRegex.re.source.replace(/\(\?:\\\/\)\?\$$/, '\\.json$') + ) + namedDataRouteRegex = routeRegex.namedRegex!.replace( + /\(\?:\/\)\?\$$/, + '\\.json$' + ) + routeKeys = routeRegex.routeKeys + } else { + dataRouteRegex = normalizeRouteRegex( + new RegExp( + `^${path.posix.join( + '/_next/data', + escapeStringRegexp(buildId), + `${pagePath}.json` + )}$` + ).source + ) + } + + return { + page, + routeKeys, + dataRouteRegex, + namedDataRouteRegex, + } + }) + + await promises.writeFile( + routesManifestPath, + JSON.stringify(routesManifest), + 'utf8' + ) + } + + // Since custom _app.js can wrap the 404 page we have to opt-out of static optimization if it has getInitialProps + // Only export the static 404 when there is no /_error present + const useStatic404 = + !customAppGetInitialProps && (!hasNonStaticErrorPage || hasPages404) + + if (invalidPages.size > 0) { + throw new Error( + `Build optimization failed: found page${ + invalidPages.size === 1 ? '' : 's' + } without a React Component as default export in \n${[...invalidPages] + .map((pg) => `pages${pg}`) + .join( + '\n' + )}\n\nSee https://nextjs.org/docs/messages/page-without-valid-component for more info.\n` + ) + } + + await writeBuildId(distDir, buildId) + + if (config.experimental.optimizeCss) { + const cssFilePaths = getCssFilePaths(buildManifest) + + requiredServerFiles.files.push( + ...cssFilePaths.map((filePath) => path.join(config.distDir, filePath)) + ) + } + + await promises.writeFile( + path.join(distDir, SERVER_FILES_MANIFEST), + JSON.stringify(requiredServerFiles), + 'utf8' + ) + + const finalPrerenderRoutes: { [route: string]: SsgRoute } = {} + const tbdPrerenderRoutes: string[] = [] + let ssgNotFoundPaths: string[] = [] + + if (postCompileSpinner) postCompileSpinner.stopAndPersist() + + const { i18n } = config + + const usedStaticStatusPages = STATIC_STATUS_PAGES.filter( + (page) => + mappedPages[page] && mappedPages[page].startsWith('private-next-pages') + ) + usedStaticStatusPages.forEach((page) => { + if (!ssgPages.has(page) && !customAppGetInitialProps) { + staticPages.add(page) + } + }) + + const hasPages500 = usedStaticStatusPages.includes('/500') + const useDefaultStatic500 = + !hasPages500 && !hasNonStaticErrorPage && !customAppGetInitialProps + + const combinedPages = [...staticPages, ...ssgPages] + + if (combinedPages.length > 0 || useStatic404 || useDefaultStatic500) { + const staticGenerationSpan = nextBuildSpan.traceChild('static-generation') + await staticGenerationSpan.traceAsyncFn(async () => { + detectConflictingPaths( + [ + ...combinedPages, + ...pageKeys.filter((page) => !combinedPages.includes(page)), + ], + ssgPages, + additionalSsgPaths + ) + const exportApp = require('../export').default + const exportOptions = { + silent: false, + buildExport: true, + threads: config.experimental.cpus, + pages: combinedPages, + outdir: path.join(distDir, 'export'), + statusMessage: 'Generating static pages', + } + const exportConfig: any = { + ...config, + initialPageRevalidationMap: {}, + pageDurationMap: {}, + ssgNotFoundPaths: [] as string[], + // Default map will be the collection of automatic statically exported + // pages and incremental pages. + // n.b. we cannot handle this above in combinedPages because the dynamic + // page must be in the `pages` array, but not in the mapping. + exportPathMap: (defaultMap: any) => { + // Dynamically routed pages should be prerendered to be used as + // a client-side skeleton (fallback) while data is being fetched. + // This ensures the end-user never sees a 500 or slow response from the + // server. + // + // Note: prerendering disables automatic static optimization. + ssgPages.forEach((page) => { + if (isDynamicRoute(page)) { + tbdPrerenderRoutes.push(page) + + if (ssgStaticFallbackPages.has(page)) { + // Override the rendering for the dynamic page to be treated as a + // fallback render. + if (i18n) { + defaultMap[`/${i18n.defaultLocale}${page}`] = { + page, + query: { __nextFallback: true }, + } + } else { + defaultMap[page] = { page, query: { __nextFallback: true } } + } + } else { + // Remove dynamically routed pages from the default path map when + // fallback behavior is disabled. + delete defaultMap[page] + } + } + }) + // Append the "well-known" routes we should prerender for, e.g. blog + // post slugs. + additionalSsgPaths.forEach((routes, page) => { + const encodedRoutes = additionalSsgPathsEncoded.get(page) + + routes.forEach((route, routeIdx) => { + defaultMap[route] = { + page, + query: { __nextSsgPath: encodedRoutes?.[routeIdx] }, + } + }) + }) + + if (useStatic404) { + defaultMap['/404'] = { + page: hasPages404 ? '/404' : '/_error', + } + } + + if (useDefaultStatic500) { + defaultMap['/500'] = { + page: '/_error', + } + } + + if (i18n) { + for (const page of [ + ...staticPages, + ...ssgPages, + ...(useStatic404 ? ['/404'] : []), + ...(useDefaultStatic500 ? ['/500'] : []), + ]) { + const isSsg = ssgPages.has(page) + const isDynamic = isDynamicRoute(page) + const isFallback = isSsg && ssgStaticFallbackPages.has(page) + + for (const locale of i18n.locales) { + // skip fallback generation for SSG pages without fallback mode + if (isSsg && isDynamic && !isFallback) continue + const outputPath = `/${locale}${page === '/' ? '' : page}` + + defaultMap[outputPath] = { + page: defaultMap[page]?.page || page, + query: { __nextLocale: locale }, + } + + if (isFallback) { + defaultMap[outputPath].query.__nextFallback = true + } + } + + if (isSsg) { + // remove non-locale prefixed variant from defaultMap + delete defaultMap[page] + } + } + } + return defaultMap + }, + } + + await exportApp(dir, exportOptions, exportConfig) + + const postBuildSpinner = createSpinner({ + prefixText: `${Log.prefixes.info} Finalizing page optimization`, + }) + ssgNotFoundPaths = exportConfig.ssgNotFoundPaths + + // remove server bundles that were exported + for (const page of staticPages) { + const serverBundle = getPagePath(page, distDir, isLikeServerless) + await promises.unlink(serverBundle) + } + const serverOutputDir = path.join( + distDir, + isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY + ) + + const moveExportedPage = async ( + originPage: string, + page: string, + file: string, + isSsg: boolean, + ext: 'html' | 'json', + additionalSsgFile = false + ) => { + return staticGenerationSpan + .traceChild('move-exported-page') + .traceAsyncFn(async () => { + file = `${file}.${ext}` + const orig = path.join(exportOptions.outdir, file) + const pagePath = getPagePath( + originPage, + distDir, + isLikeServerless + ) + + const relativeDest = path + .relative( + serverOutputDir, + path.join( + path.join( + pagePath, + // strip leading / and then recurse number of nested dirs + // to place from base folder + originPage + .substr(1) + .split('/') + .map(() => '..') + .join('/') + ), + file + ) + ) + .replace(/\\/g, '/') + + const dest = path.join( + distDir, + isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, + relativeDest + ) + + if ( + !isSsg && + !( + // don't add static status page to manifest if it's + // the default generated version e.g. no pages/500 + ( + STATIC_STATUS_PAGES.includes(page) && + !usedStaticStatusPages.includes(page) + ) + ) + ) { + pagesManifest[page] = relativeDest + } + + const isNotFound = ssgNotFoundPaths.includes(page) + + // for SSG files with i18n the non-prerendered variants are + // output with the locale prefixed so don't attempt moving + // without the prefix + if ((!i18n || additionalSsgFile) && !isNotFound) { + await promises.mkdir(path.dirname(dest), { recursive: true }) + await promises.rename(orig, dest) + } else if (i18n && !isSsg) { + // this will be updated with the locale prefixed variant + // since all files are output with the locale prefix + delete pagesManifest[page] + } + + if (i18n) { + if (additionalSsgFile) return + + for (const locale of i18n.locales) { + const curPath = `/${locale}${page === '/' ? '' : page}` + const localeExt = page === '/' ? path.extname(file) : '' + const relativeDestNoPages = relativeDest.substr( + 'pages/'.length + ) + + if (isSsg && ssgNotFoundPaths.includes(curPath)) { + continue + } + + const updatedRelativeDest = path + .join( + 'pages', + locale + localeExt, + // if it's the top-most index page we want it to be locale.EXT + // instead of locale/index.html + page === '/' ? '' : relativeDestNoPages + ) + .replace(/\\/g, '/') + + const updatedOrig = path.join( + exportOptions.outdir, + locale + localeExt, + page === '/' ? '' : file + ) + const updatedDest = path.join( + distDir, + isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, + updatedRelativeDest + ) + + if (!isSsg) { + pagesManifest[curPath] = updatedRelativeDest + } + await promises.mkdir(path.dirname(updatedDest), { + recursive: true, + }) + await promises.rename(updatedOrig, updatedDest) + } + } + }) + } + + // Only move /404 to /404 when there is no custom 404 as in that case we don't know about the 404 page + if (!hasPages404 && useStatic404) { + await moveExportedPage('/_error', '/404', '/404', false, 'html') + } + + if (useDefaultStatic500) { + await moveExportedPage('/_error', '/500', '/500', false, 'html') + } + + for (const page of combinedPages) { + const isSsg = ssgPages.has(page) + const isStaticSsgFallback = ssgStaticFallbackPages.has(page) + const isDynamic = isDynamicRoute(page) + const hasAmp = hybridAmpPages.has(page) + const file = normalizePagePath(page) + + const pageInfo = pageInfos.get(page) + const durationInfo = exportConfig.pageDurationMap[page] + if (pageInfo && durationInfo) { + // Set Build Duration + if (pageInfo.ssgPageRoutes) { + pageInfo.ssgPageDurations = pageInfo.ssgPageRoutes.map( + (pagePath) => durationInfo[pagePath] + ) + } + pageInfo.pageDuration = durationInfo[page] + } + + // The dynamic version of SSG pages are only prerendered if the + // fallback is enabled. Below, we handle the specific prerenders + // of these. + const hasHtmlOutput = !(isSsg && isDynamic && !isStaticSsgFallback) + + if (hasHtmlOutput) { + await moveExportedPage(page, page, file, isSsg, 'html') + } + + if (hasAmp && (!isSsg || (isSsg && !isDynamic))) { + const ampPage = `${file}.amp` + await moveExportedPage(page, ampPage, ampPage, isSsg, 'html') + + if (isSsg) { + await moveExportedPage(page, ampPage, ampPage, isSsg, 'json') + } + } + + if (isSsg) { + // For a non-dynamic SSG page, we must copy its data file + // from export, we already moved the HTML file above + if (!isDynamic) { + await moveExportedPage(page, page, file, isSsg, 'json') + + if (i18n) { + // TODO: do we want to show all locale variants in build output + for (const locale of i18n.locales) { + const localePage = `/${locale}${page === '/' ? '' : page}` + + if (!ssgNotFoundPaths.includes(localePage)) { + finalPrerenderRoutes[localePage] = { + initialRevalidateSeconds: + exportConfig.initialPageRevalidationMap[localePage], + srcRoute: null, + dataRoute: path.posix.join( + '/_next/data', + buildId, + `${file}.json` + ), + } + } + } + } else { + finalPrerenderRoutes[page] = { + initialRevalidateSeconds: + exportConfig.initialPageRevalidationMap[page], + srcRoute: null, + dataRoute: path.posix.join( + '/_next/data', + buildId, + `${file}.json` + ), + } + } + // Set Page Revalidation Interval + if (pageInfo) { + pageInfo.initialRevalidateSeconds = + exportConfig.initialPageRevalidationMap[page] + } + } else { + // For a dynamic SSG page, we did not copy its data exports and only + // copy the fallback HTML file (if present). + // We must also copy specific versions of this page as defined by + // `getStaticPaths` (additionalSsgPaths). + const extraRoutes = additionalSsgPaths.get(page) || [] + for (const route of extraRoutes) { + const pageFile = normalizePagePath(route) + await moveExportedPage( + page, + route, + pageFile, + isSsg, + 'html', + true + ) + await moveExportedPage( + page, + route, + pageFile, + isSsg, + 'json', + true + ) + + if (hasAmp) { + const ampPage = `${pageFile}.amp` + await moveExportedPage( + page, + ampPage, + ampPage, + isSsg, + 'html', + true + ) + await moveExportedPage( + page, + ampPage, + ampPage, + isSsg, + 'json', + true + ) + } + + finalPrerenderRoutes[route] = { + initialRevalidateSeconds: + exportConfig.initialPageRevalidationMap[route], + srcRoute: page, + dataRoute: path.posix.join( + '/_next/data', + buildId, + `${normalizePagePath(route)}.json` + ), + } + + // Set route Revalidation Interval + if (pageInfo) { + pageInfo.initialRevalidateSeconds = + exportConfig.initialPageRevalidationMap[route] + } + } + } + } + } + + // remove temporary export folder + await recursiveDelete(exportOptions.outdir) + await promises.rmdir(exportOptions.outdir) + await promises.writeFile( + manifestPath, + JSON.stringify(pagesManifest, null, 2), + 'utf8' + ) + + if (postBuildSpinner) postBuildSpinner.stopAndPersist() + console.log() + }) + } + + const analysisEnd = process.hrtime(analysisBegin) + telemetry.record( + eventBuildOptimize(pagePaths, { + durationInSeconds: analysisEnd[0], + staticPageCount: staticPages.size, + staticPropsPageCount: ssgPages.size, + serverPropsPageCount: serverPropsPages.size, + ssrPageCount: + pagePaths.length - + (staticPages.size + ssgPages.size + serverPropsPages.size), + hasStatic404: useStatic404, + hasReportWebVitals: namedExports?.includes('reportWebVitals') ?? false, + rewritesCount: combinedRewrites.length, + headersCount: headers.length, + redirectsCount: redirects.length - 1, // reduce one for trailing slash + headersWithHasCount: headers.filter((r: any) => !!r.has).length, + rewritesWithHasCount: combinedRewrites.filter((r: any) => !!r.has) + .length, + redirectsWithHasCount: redirects.filter((r: any) => !!r.has).length, + }) + ) + + if (ssgPages.size > 0) { + const finalDynamicRoutes: PrerenderManifest['dynamicRoutes'] = {} + tbdPrerenderRoutes.forEach((tbdRoute) => { + const normalizedRoute = normalizePagePath(tbdRoute) + const dataRoute = path.posix.join( + '/_next/data', + buildId, + `${normalizedRoute}.json` + ) + + finalDynamicRoutes[tbdRoute] = { + routeRegex: normalizeRouteRegex(getRouteRegex(tbdRoute).re.source), + dataRoute, + fallback: ssgBlockingFallbackPages.has(tbdRoute) + ? null + : ssgStaticFallbackPages.has(tbdRoute) + ? `${normalizedRoute}.html` + : false, + dataRouteRegex: normalizeRouteRegex( + getRouteRegex(dataRoute.replace(/\.json$/, '')).re.source.replace( + /\(\?:\\\/\)\?\$$/, + '\\.json$' + ) + ), + } + }) + const prerenderManifest: PrerenderManifest = { + version: 3, + routes: finalPrerenderRoutes, + dynamicRoutes: finalDynamicRoutes, + notFoundRoutes: ssgNotFoundPaths, + preview: previewProps, + } + + await promises.writeFile( + path.join(distDir, PRERENDER_MANIFEST), + JSON.stringify(prerenderManifest), + 'utf8' + ) + await generateClientSsgManifest(prerenderManifest, { + distDir, + buildId, + locales: config.i18n?.locales || [], + }) + } else { + const prerenderManifest: PrerenderManifest = { + version: 3, + routes: {}, + dynamicRoutes: {}, + preview: previewProps, + notFoundRoutes: [], + } + await promises.writeFile( + path.join(distDir, PRERENDER_MANIFEST), + JSON.stringify(prerenderManifest), + 'utf8' + ) + } + + const images = { ...config.images } + const { deviceSizes, imageSizes } = images + ;(images as any).sizes = [...deviceSizes, ...imageSizes] + + await promises.writeFile( + path.join(distDir, IMAGES_MANIFEST), + JSON.stringify({ + version: 1, + images, + }), + 'utf8' + ) + await promises.writeFile( + path.join(distDir, EXPORT_MARKER), + JSON.stringify({ + version: 1, + hasExportPathMap: typeof config.exportPathMap === 'function', + exportTrailingSlash: config.trailingSlash === true, + isNextImageImported: isNextImageImported === true, + }), + 'utf8' + ) + await promises.unlink(path.join(distDir, EXPORT_DETAIL)).catch((err) => { + if (err.code === 'ENOENT') { + return Promise.resolve() + } + return Promise.reject(err) + }) + + staticPages.forEach((pg) => allStaticPages.add(pg)) + pageInfos.forEach((info: PageInfo, key: string) => { + allPageInfos.set(key, info) + }) + + await nextBuildSpan.traceChild('print-tree-view').traceAsyncFn(() => + printTreeView(Object.keys(mappedPages), allPageInfos, isLikeServerless, { + distPath: distDir, + buildId: buildId, + pagesDir, + useStatic404, + pageExtensions: config.pageExtensions, + buildManifest, + gzipSize: config.experimental.gzipSize, + }) + ) + + if (debugOutput) { + nextBuildSpan + .traceChild('print-custom-routes') + .traceFn(() => printCustomRoutes({ redirects, rewrites, headers })) + } + + if (config.analyticsId) { + console.log( + chalk.bold.green('Blitz.js Analytics') + + ' is enabled for this production build. ' + + "You'll receive a Real Experience Score computed by all of your visitors." + ) + console.log('') + } + + await nextBuildSpan + .traceChild('telemetry-flush') + .traceAsyncFn(() => telemetry.flush()) + }) +} + +export type ClientSsgManifest = Set + +function generateClientSsgManifest( + prerenderManifest: PrerenderManifest, + { + buildId, + distDir, + locales, + }: { buildId: string; distDir: string; locales: string[] } +) { + const ssgPages: ClientSsgManifest = new Set([ + ...Object.entries(prerenderManifest.routes) + // Filter out dynamic routes + .filter(([, { srcRoute }]) => srcRoute == null) + .map(([route]) => normalizeLocalePath(route, locales).pathname), + ...Object.keys(prerenderManifest.dynamicRoutes), + ]) + + const clientSsgManifestContent = `self.__SSG_MANIFEST=${devalue( + ssgPages + )};self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()` + + writeFileSync( + path.join(distDir, CLIENT_STATIC_FILES_PATH, buildId, '_ssgManifest.js'), + clientSsgManifestContent + ) +} diff --git a/nextjs/packages/next/build/is-writeable.ts b/nextjs/packages/next/build/is-writeable.ts new file mode 100644 index 0000000000..0b9e9abb4f --- /dev/null +++ b/nextjs/packages/next/build/is-writeable.ts @@ -0,0 +1,10 @@ +import fs from 'fs' + +export async function isWriteable(directory: string): Promise { + try { + await fs.promises.access(directory, (fs.constants || fs).W_OK) + return true + } catch (err) { + return false + } +} diff --git a/nextjs/packages/next/build/output/index.ts b/nextjs/packages/next/build/output/index.ts new file mode 100644 index 0000000000..a0ea1dc7a6 --- /dev/null +++ b/nextjs/packages/next/build/output/index.ts @@ -0,0 +1,244 @@ +import chalk from 'chalk' +import stripAnsi from 'next/dist/compiled/strip-ansi' +import textTable from 'next/dist/compiled/text-table' +import createStore from 'next/dist/compiled/unistore' +import formatWebpackMessages from '../../client/dev/error-overlay/format-webpack-messages' +import { OutputState, store as consoleStore } from './store' + +export function startedDevelopmentServer(appUrl: string, bindAddr: string) { + consoleStore.setState({ appUrl, bindAddr }) +} + +let previousClient: import('webpack').Compiler | null = null +let previousServer: import('webpack').Compiler | null = null + +type CompilerDiagnostics = { + errors: string[] | null + warnings: string[] | null +} + +type WebpackStatus = + | { loading: true } + | ({ loading: false } & CompilerDiagnostics) + +type AmpStatus = { + message: string + line: number + col: number + specUrl: string | null + code: string +} + +export type AmpPageStatus = { + [page: string]: { errors: AmpStatus[]; warnings: AmpStatus[] } +} + +type BuildStatusStore = { + client: WebpackStatus + server: WebpackStatus + amp: AmpPageStatus +} + +// eslint typescript has a bug with TS enums +/* eslint-disable no-shadow */ +enum WebpackStatusPhase { + COMPILING = 1, + COMPILED_WITH_ERRORS = 2, + COMPILED_WITH_WARNINGS = 4, + COMPILED = 5, +} + +function getWebpackStatusPhase(status: WebpackStatus): WebpackStatusPhase { + if (status.loading) { + return WebpackStatusPhase.COMPILING + } + if (status.errors) { + return WebpackStatusPhase.COMPILED_WITH_ERRORS + } + if (status.warnings) { + return WebpackStatusPhase.COMPILED_WITH_WARNINGS + } + return WebpackStatusPhase.COMPILED +} + +export function formatAmpMessages(amp: AmpPageStatus) { + let output = chalk.bold('Amp Validation') + '\n\n' + let messages: string[][] = [] + + const chalkError = chalk.red('error') + function ampError(page: string, error: AmpStatus) { + messages.push([page, chalkError, error.message, error.specUrl || '']) + } + + const chalkWarn = chalk.yellow('warn') + function ampWarn(page: string, warn: AmpStatus) { + messages.push([page, chalkWarn, warn.message, warn.specUrl || '']) + } + + for (const page in amp) { + let { errors, warnings } = amp[page] + + const devOnlyFilter = (err: AmpStatus) => err.code !== 'DEV_MODE_ONLY' + errors = errors.filter(devOnlyFilter) + warnings = warnings.filter(devOnlyFilter) + if (!(errors.length || warnings.length)) { + // Skip page with no non-dev warnings + continue + } + + if (errors.length) { + ampError(page, errors[0]) + for (let index = 1; index < errors.length; ++index) { + ampError('', errors[index]) + } + } + if (warnings.length) { + ampWarn(errors.length ? '' : page, warnings[0]) + for (let index = 1; index < warnings.length; ++index) { + ampWarn('', warnings[index]) + } + } + messages.push(['', '', '', '']) + } + + if (!messages.length) { + return '' + } + + output += textTable(messages, { + align: ['l', 'l', 'l', 'l'], + stringLength(str: string) { + return stripAnsi(str).length + }, + }) + + return output +} + +const buildStore = createStore() + +buildStore.subscribe((state) => { + const { amp, client, server } = state + + const [{ status }] = [ + { status: client, phase: getWebpackStatusPhase(client) }, + { status: server, phase: getWebpackStatusPhase(server) }, + ].sort((a, b) => a.phase.valueOf() - b.phase.valueOf()) + + const { bootstrap: bootstrapping, appUrl } = consoleStore.getState() + if (bootstrapping && status.loading) { + return + } + + let partialState: Partial = { + bootstrap: false, + appUrl: appUrl!, + } + + if (status.loading) { + consoleStore.setState( + { ...partialState, loading: true } as OutputState, + true + ) + } else { + let { errors, warnings } = status + + if (errors == null) { + if (Object.keys(amp).length > 0) { + warnings = (warnings || []).concat(formatAmpMessages(amp) || []) + if (!warnings.length) warnings = null + } + } + + consoleStore.setState( + { + ...partialState, + loading: false, + typeChecking: false, + errors, + warnings, + } as OutputState, + true + ) + } +}) + +export function ampValidation( + page: string, + errors: AmpStatus[], + warnings: AmpStatus[] +) { + const { amp } = buildStore.getState() + if (!(errors.length || warnings.length)) { + buildStore.setState({ + amp: Object.keys(amp) + .filter((k) => k !== page) + .sort() + // eslint-disable-next-line no-sequences + .reduce((a, c) => ((a[c] = amp[c]), a), {} as AmpPageStatus), + }) + return + } + + const newAmp: AmpPageStatus = { ...amp, [page]: { errors, warnings } } + buildStore.setState({ + amp: Object.keys(newAmp) + .sort() + // eslint-disable-next-line no-sequences + .reduce((a, c) => ((a[c] = newAmp[c]), a), {} as AmpPageStatus), + }) +} + +export function watchCompilers( + client: import('webpack').Compiler, + server: import('webpack').Compiler +) { + if (previousClient === client && previousServer === server) { + return + } + + buildStore.setState({ + client: { loading: true }, + server: { loading: true }, + }) + + function tapCompiler( + key: string, + compiler: any, + onEvent: (status: WebpackStatus) => void + ) { + compiler.hooks.invalid.tap(`NextJsInvalid-${key}`, () => { + onEvent({ loading: true }) + }) + + compiler.hooks.done.tap( + `NextJsDone-${key}`, + (stats: import('webpack').Stats) => { + buildStore.setState({ amp: {} }) + + const { errors, warnings } = formatWebpackMessages( + stats.toJson({ all: false, warnings: true, errors: true }) + ) + + const hasErrors = !!errors?.length + const hasWarnings = !!warnings?.length + + onEvent({ + loading: false, + errors: hasErrors ? errors : null, + warnings: hasWarnings ? warnings : null, + }) + } + ) + } + + tapCompiler('client', client, (status) => + buildStore.setState({ client: status }) + ) + tapCompiler('server', server, (status) => + buildStore.setState({ server: status }) + ) + + previousClient = client + previousServer = server +} diff --git a/nextjs/packages/next/build/output/log.ts b/nextjs/packages/next/build/output/log.ts new file mode 100644 index 0000000000..3cf1f28a86 --- /dev/null +++ b/nextjs/packages/next/build/output/log.ts @@ -0,0 +1,34 @@ +import chalk from 'chalk' + +export const prefixes = { + wait: chalk.cyan('wait') + ' -', + error: chalk.red('error') + ' -', + warn: chalk.yellow('warn') + ' -', + ready: chalk.green('ready') + ' -', + info: chalk.cyan('info') + ' -', + event: chalk.magenta('event') + ' -', +} + +export function wait(...message: string[]) { + console.log(prefixes.wait, ...message) +} + +export function error(...message: string[]) { + console.error(prefixes.error, ...message) +} + +export function warn(...message: string[]) { + console.warn(prefixes.warn, ...message) +} + +export function ready(...message: string[]) { + console.log(prefixes.ready, ...message) +} + +export function info(...message: string[]) { + console.log(prefixes.info, ...message) +} + +export function event(...message: string[]) { + console.log(prefixes.event, ...message) +} diff --git a/nextjs/packages/next/build/output/store.ts b/nextjs/packages/next/build/output/store.ts new file mode 100644 index 0000000000..3c371b30ee --- /dev/null +++ b/nextjs/packages/next/build/output/store.ts @@ -0,0 +1,91 @@ +import createStore from 'next/dist/compiled/unistore' +import stripAnsi from 'next/dist/compiled/strip-ansi' + +import * as Log from './log' + +export type OutputState = + | { bootstrap: true; appUrl: string | null; bindAddr: string | null } + | ({ bootstrap: false; appUrl: string | null; bindAddr: string | null } & ( + | { loading: true } + | { + loading: false + typeChecking: boolean + errors: string[] | null + warnings: string[] | null + } + )) + +export const store = createStore({ + appUrl: null, + bindAddr: null, + bootstrap: true, +}) + +let lastStore: OutputState = { appUrl: null, bindAddr: null, bootstrap: true } +function hasStoreChanged(nextStore: OutputState) { + if ( + ([ + ...new Set([...Object.keys(lastStore), ...Object.keys(nextStore)]), + ] as Array).every((key) => + Object.is(lastStore[key], nextStore[key]) + ) + ) { + return false + } + + lastStore = nextStore + return true +} + +store.subscribe((state) => { + if (!hasStoreChanged(state)) { + return + } + + if (state.bootstrap) { + if (state.appUrl) { + Log.ready(`started server on ${state.bindAddr}, url: ${state.appUrl}`) + } + return + } + + if (state.loading) { + Log.wait('compiling...') + return + } + + if (state.errors) { + Log.error(state.errors[0]) + + const cleanError = stripAnsi(state.errors[0]) + if (cleanError.indexOf('SyntaxError') > -1) { + const matches = cleanError.match(/\[.*\]=/) + if (matches) { + for (const match of matches) { + const prop = (match.split(']').shift() || '').substr(1) + console.log( + `AMP bind syntax [${prop}]='' is not supported in JSX, use 'data-amp-bind-${prop}' instead. https://nextjs.org/docs/messages/amp-bind-jsx-alt` + ) + } + return + } + } + + return + } + + if (state.warnings) { + Log.warn(state.warnings.join('\n\n')) + if (state.appUrl) { + Log.info(`ready on ${state.appUrl}`) + } + return + } + + if (state.typeChecking) { + Log.info('bundled successfully, waiting for typecheck results...') + return + } + + Log.event('compiled successfully') +}) diff --git a/nextjs/packages/next/build/polyfills/fetch/index.js b/nextjs/packages/next/build/polyfills/fetch/index.js new file mode 100644 index 0000000000..24b29bb986 --- /dev/null +++ b/nextjs/packages/next/build/polyfills/fetch/index.js @@ -0,0 +1,4 @@ +/* globals self */ +var fetch = self.fetch.bind(self) +module.exports = fetch +module.exports.default = module.exports diff --git a/nextjs/packages/next/build/polyfills/fetch/whatwg-fetch.js b/nextjs/packages/next/build/polyfills/fetch/whatwg-fetch.js new file mode 100644 index 0000000000..703706cba3 --- /dev/null +++ b/nextjs/packages/next/build/polyfills/fetch/whatwg-fetch.js @@ -0,0 +1,5 @@ +/* globals self */ +exports.Headers = self.Headers +exports.Request = self.Request +exports.Response = self.Response +exports.fetch = self.fetch diff --git a/nextjs/packages/next/build/polyfills/object-assign.js b/nextjs/packages/next/build/polyfills/object-assign.js new file mode 100644 index 0000000000..62b59a0583 --- /dev/null +++ b/nextjs/packages/next/build/polyfills/object-assign.js @@ -0,0 +1,3 @@ +var assign = Object.assign.bind(Object) +module.exports = assign +module.exports.default = module.exports diff --git a/nextjs/packages/next/build/polyfills/object.assign/auto.js b/nextjs/packages/next/build/polyfills/object.assign/auto.js new file mode 100644 index 0000000000..172f1ae6a4 --- /dev/null +++ b/nextjs/packages/next/build/polyfills/object.assign/auto.js @@ -0,0 +1 @@ +// noop diff --git a/nextjs/packages/next/build/polyfills/object.assign/implementation.js b/nextjs/packages/next/build/polyfills/object.assign/implementation.js new file mode 100644 index 0000000000..97fcde1c0d --- /dev/null +++ b/nextjs/packages/next/build/polyfills/object.assign/implementation.js @@ -0,0 +1 @@ +module.exports = Object.assign diff --git a/nextjs/packages/next/build/polyfills/object.assign/index.js b/nextjs/packages/next/build/polyfills/object.assign/index.js new file mode 100644 index 0000000000..abccfddaf2 --- /dev/null +++ b/nextjs/packages/next/build/polyfills/object.assign/index.js @@ -0,0 +1,10 @@ +var assign = Object.assign.bind(Object) +function g() { + return assign +} +Object.defineProperties(g(), { + implementation: { get: g }, + shim: { value: g }, + getPolyfill: { value: g }, +}) +module.exports = g() diff --git a/nextjs/packages/next/build/polyfills/object.assign/polyfill.js b/nextjs/packages/next/build/polyfills/object.assign/polyfill.js new file mode 100644 index 0000000000..c328e36acc --- /dev/null +++ b/nextjs/packages/next/build/polyfills/object.assign/polyfill.js @@ -0,0 +1,3 @@ +module.exports = function () { + return Object.assign +} diff --git a/nextjs/packages/next/build/polyfills/object.assign/shim.js b/nextjs/packages/next/build/polyfills/object.assign/shim.js new file mode 100644 index 0000000000..c328e36acc --- /dev/null +++ b/nextjs/packages/next/build/polyfills/object.assign/shim.js @@ -0,0 +1,3 @@ +module.exports = function () { + return Object.assign +} diff --git a/nextjs/packages/next/build/profiler/profiler.js b/nextjs/packages/next/build/profiler/profiler.js new file mode 100644 index 0000000000..87057ed852 --- /dev/null +++ b/nextjs/packages/next/build/profiler/profiler.js @@ -0,0 +1,147 @@ +import fs from 'fs' +import path from 'path' + +let maybeInspector +try { + maybeInspector = require('inspector') +} catch (e) { + console.log('Unable to CPU profile in < node 8.0') +} + +class Profiler { + constructor(inspector) { + this.session = undefined + this.inspector = inspector + } + + hasSession() { + return this.session !== undefined + } + + startProfiling() { + if (this.inspector === undefined) { + return Promise.resolve() + } + + try { + this.session = new maybeInspector.Session() + this.session.connect() + } catch (_) { + this.session = undefined + return Promise.resolve() + } + + return Promise.all([ + this.sendCommand('Profiler.setSamplingInterval', { + interval: 100, + }), + this.sendCommand('Profiler.enable'), + this.sendCommand('Profiler.start'), + ]) + } + + sendCommand(method, params) { + if (this.hasSession()) { + return new Promise((resolve, reject) => { + return this.session.post(method, params, (err, sessionParams) => { + if (err !== null) { + reject(err) + } else { + resolve(sessionParams) + } + }) + }) + } else { + return Promise.resolve() + } + } + + destroy() { + if (this.hasSession()) { + this.session.disconnect() + } + + return Promise.resolve() + } + + stopProfiling() { + return this.sendCommand('Profiler.stop') + } +} + +// eslint-disable-next-line import/no-extraneous-dependencies +const { Tracer } = require('chrome-trace-event') + +/** + * an object that wraps Tracer and Profiler with a counter + * @typedef {Object} Trace + * @property {Tracer} trace instance of Tracer + * @property {number} counter Counter + * @property {Profiler} profiler instance of Profiler + * @property {Function} end the end function + */ + +/** + * @param {string} outputPath The location where to write the log. + * @returns {Trace} The trace object + */ +export const createTrace = (outputPath) => { + const trace = new Tracer({ + noStream: true, + }) + const profiler = new Profiler(maybeInspector) + if (/\/|\\/.test(outputPath)) { + const dirPath = path.dirname(outputPath) + fs.mkdirSync(dirPath, { recursive: true }) + } + const fsStream = fs.createWriteStream(outputPath) + + let counter = 0 + + trace.pipe(fsStream) + // These are critical events that need to be inserted so that tools like + // chrome dev tools can load the profile. + trace.instantEvent({ + name: 'TracingStartedInPage', + id: ++counter, + cat: ['disabled-by-default-devtools.timeline'], + args: { + data: { + sessionId: '-1', + page: '0xfff', + frames: [ + { + frame: '0xfff', + url: 'webpack', + name: '', + }, + ], + }, + }, + }) + + trace.instantEvent({ + name: 'TracingStartedInBrowser', + id: ++counter, + cat: ['disabled-by-default-devtools.timeline'], + args: { + data: { + sessionId: '-1', + }, + }, + }) + + return { + trace, + counter, + profiler, + end: (callback) => { + // Wait until the write stream finishes. + fsStream.on('finish', () => { + callback() + }) + // Tear down the readable trace stream. + trace.push(null) + }, + } +} diff --git a/nextjs/packages/next/build/routes.ts b/nextjs/packages/next/build/routes.ts new file mode 100644 index 0000000000..434e6fe3f1 --- /dev/null +++ b/nextjs/packages/next/build/routes.ts @@ -0,0 +1,374 @@ +import { promises } from 'fs' +import { NextConfigComplete } from '../server/config-shared' +import { createPagesMapping } from './entries' +import { collectPages, getIsRpcFile } from './utils' +import { newline, baseLogger } from '../server/lib/logging' +import { isInternalBlitzMonorepoDevelopment } from '../server/utils' +import { join, dirname } from 'path' +import { outputFile } from 'fs-extra' +import findUp from 'next/dist/compiled/find-up' +import resolveFrom from 'resolve-from' +const readFile = promises.readFile +const manifestDebug = require('debug')('blitz:manifest') + +export type RouteType = 'page' | 'rpc' | 'api' +export type RouteVerb = 'get' | 'post' | 'patch' | 'head' | 'delete' | '*' +export type RouteCacheEntry = { + filePath: string + route: string + verb: string + type: RouteType +} + +function getVerb(type: RouteType): RouteVerb { + switch (type) { + case 'api': + return '*' + case 'rpc': + return 'post' + default: + return 'get' + } +} + +// from https://github.com/angus-c/just/blob/master/packages/array-partition/index.js +function partition(arr: any[], predicate: (value: any) => boolean) { + if (!Array.isArray(arr)) { + throw new Error('expected first argument to be an array') + } + if (typeof predicate != 'function') { + throw new Error('expected second argument to be a function') + } + var first = [] + var second = [] + var length = arr.length + for (var i = 0; i < length; i++) { + var nextValue = arr[i] + if (predicate(nextValue)) { + first.push(nextValue) + } else { + second.push(nextValue) + } + } + return [first, second] +} + +const apiPathRegex = /([\\/]api[\\/])/ + +export async function collectAllRoutes( + directory: string, + config: NextConfigComplete +) { + const routeFiles = await collectPages(directory, config.pageExtensions!) + const rawRouteMappings = createPagesMapping( + routeFiles, + config.pageExtensions! + ) + const routes: RouteCacheEntry[] = [] + for (const [route, filePath] of Object.entries(rawRouteMappings)) { + if (['/_app', '/_document', '/_error'].includes(route)) continue + let type: RouteType + if (getIsRpcFile(filePath)) { + type = 'rpc' + } else if (apiPathRegex.test(filePath)) { + type = 'api' + } else { + type = 'page' + } + routes.push({ + filePath: filePath.replace('private-next-pages/', ''), + route, + type, + verb: getVerb(type), + }) + } + return routes +} + +type Parameter = { + name: string + optional: boolean +} +interface RouteManifestEntry { + name: string + parameters: Parameter[] + multipleParameters: Parameter[] + mdx?: boolean +} + +const pascalCase = (value: string): string => { + const val = value.replace(/[-_\s/.]+(.)?/g, (_match, chr) => + chr ? chr.toUpperCase() : '' + ) + return val.substr(0, 1).toUpperCase() + val.substr(1) +} + +export async function saveRouteManifest( + directory: string, + config: NextConfigComplete +) { + const allRoutes = await collectAllRoutes(directory, config) + const routes: Record = {} + + for (let { filePath, route, type } of allRoutes) { + if (type === 'api' || type === 'rpc') continue + + if (/\.mdx$/.test(filePath)) { + routes[route] = { + ...parseParametersFromRoute(route), + name: route === '/' ? 'Index' : pascalCase(route), + mdx: true, + } + } else { + const fileContents = await readFile(join(directory, filePath), { + encoding: 'utf-8', + }) + + const defaultExportName = parseDefaultExportName(fileContents) + if (!defaultExportName) continue + + routes[route] = { + ...parseParametersFromRoute(route), + name: defaultExportName, + } + } + } + + const { declaration, implementation } = generateManifest(routes) + + const dotBlitz = join(await findNodeModulesRoot(directory), '.blitz') + + await outputFile(join(dotBlitz, 'index.js'), implementation, { + encoding: 'utf-8', + }) + await outputFile(join(dotBlitz, 'index-browser.js'), implementation, { + encoding: 'utf-8', + }) + await outputFile(join(dotBlitz, 'index.d.ts'), declaration, { + encoding: 'utf-8', + }) +} + +async function findNodeModulesRoot(src: string) { + /* + * Because of our package structure, and because of how things like pnpm link modules, + * we must first find blitz package, and then find `next` and then + * the root of `next` + * + * This is because we import from `.blitz` inside `next/stdlib`. + * If that changes, then this logic here will need to change + */ + manifestDebug('src ' + src) + let root: string + if (process.env.NEXT_PNPM_TEST) { + const nextPkgLocation = dirname( + (await findUp('package.json', { + cwd: resolveFrom(src, 'next'), + })) ?? '' + ) + manifestDebug('nextPkgLocation ' + nextPkgLocation) + if (!nextPkgLocation) { + throw new Error( + "Internal Blitz Error: unable to find 'next' package location" + ) + } + root = join(nextPkgLocation, '../') + } else if (isInternalBlitzMonorepoDevelopment) { + root = join(src, 'node_modules') + } else { + const blitzPkgLocation = dirname( + (await findUp('package.json', { + cwd: resolveFrom(src, 'blitz'), + })) ?? '' + ) + manifestDebug('blitzPkgLocation ' + blitzPkgLocation) + if (!blitzPkgLocation) { + throw new Error( + "Internal Blitz Error: unable to find 'blitz' package location" + ) + } + const nextPkgLocation = dirname( + (await findUp('package.json', { + cwd: resolveFrom(blitzPkgLocation, 'next'), + })) ?? '' + ) + manifestDebug('nextPkgLocation ' + nextPkgLocation) + if (!nextPkgLocation) { + throw new Error( + "Internal Blitz Error: unable to find 'next' package location" + ) + } + root = join(nextPkgLocation, '../') + if (root.endsWith('@blitzjs/')) { + root = join(nextPkgLocation, '../../') + } + } + manifestDebug('root ' + root) + return root +} + +export function parseDefaultExportName(contents: string): string | null { + const result = contents.match( + /export\s+default(?:\s+(?:const|let|class|var|function))?\s+(\w+)/ + ) + if (!result) { + return null + } + + return result[1] ?? null +} + +function dedupeBy( + arr: [string, T][], + by: (v: [string, T]) => string +): [string, T][] { + const allKeys = arr.map(by) + const countKeys = allKeys.reduce( + (obj, key) => ({ ...obj, [key]: (obj[key] || 0) + 1 }), + {} as { [key: string]: number } + ) + const duplicateKeys = Object.keys(countKeys).filter( + (key) => countKeys[key] > 1 + ) + + if (duplicateKeys.length) { + newline() + const log = baseLogger({ displayDateTime: false }).getChildLogger() + + duplicateKeys.forEach((key) => { + let errorMessage = `The page component is named "${key}" on the following routes:\n\n` + arr + .filter((v) => by(v) === key) + .forEach(([route]) => { + errorMessage += `\t${route}\n` + }) + log.error(errorMessage) + }) + + console.error( + 'The page component must have a unique name across all routes, so change the component names so they are all unique.\n' + ) + + // Don't throw error in internal monorepo development because existing nextjs + // integration tests all have duplicate page names + if ( + process.env.NODE_ENV === 'production' && + !isInternalBlitzMonorepoDevelopment + ) { + const error = Error('Duplicate Page Name') + delete error.stack + throw error + } + } + + return arr.filter((v) => !duplicateKeys.includes(by(v))) +} + +export function generateManifest( + routes: Record +): { implementation: string; declaration: string } { + const routesWithoutDuplicates = dedupeBy( + Object.entries(routes), + ([_path, { name }]) => name + ) + + const implementationLines = routesWithoutDuplicates.map( + ([path, { name }]) => `${name}: (query) => ({ pathname: "${path}", query })` + ) + + const declarationLines = routesWithoutDuplicates.map( + ([_path, { name, parameters, multipleParameters }]) => { + if (parameters.length === 0 && multipleParameters.length === 0) { + return `${name}(query?: ParsedUrlQueryInput): RouteUrlObject` + } + + return `${name}(query: { ${[ + ...parameters.map( + (param) => + param.name + (param.optional ? '?' : '') + ': string | number' + ), + ...multipleParameters.map( + (param) => + param.name + (param.optional ? '?' : '') + ': (string | number)[]' + ), + ].join('; ')} } & ParsedUrlQueryInput): RouteUrlObject` + } + ) + + const declarationEnding = declarationLines.length > 0 ? ';' : '' + + const moduleName = process.env.NEXT_PNPM_TEST ? 'next/types' : 'blitz' + + return { + implementation: + 'exports.Routes = {\n' + + implementationLines.map((line) => ' ' + line).join(',\n') + + '\n}', + declaration: ` +import type { ParsedUrlQueryInput } from "querystring" +import type { RouteUrlObject } from "${moduleName}" + +export const Routes: { +${declarationLines.map((line) => ' ' + line).join(';\n') + declarationEnding} +}`.trim(), + } +} + +function removeSquareBracketsFromSegments(value: string): string + +function removeSquareBracketsFromSegments(value: string[]): string[] + +function removeSquareBracketsFromSegments( + value: string | string[] +): string | string[] { + if (typeof value === 'string') { + return value.replace('[', '').replace(']', '') + } + return value.map((val) => val.replace('[', '').replace(']', '')) +} + +const squareBracketsRegex = /\[\[.*?\]\]|\[.*?\]/g + +export function parseParametersFromRoute( + path: string +): Pick { + const parameteredSegments = path.match(squareBracketsRegex) ?? [] + const withoutBrackets = removeSquareBracketsFromSegments(parameteredSegments) + + const [multipleParameters, parameters] = partition(withoutBrackets, (p) => + p.includes('...') + ) + + return { + parameters: parameters.map((value) => { + const containsSquareBrackets = squareBracketsRegex.test(value) + if (containsSquareBrackets) { + return { + name: removeSquareBracketsFromSegments(value), + optional: true, + } + } + + return { + name: value, + optional: false, + } + }), + multipleParameters: multipleParameters.map((param) => { + const withoutEllipsis = param.replace('...', '') + const containsSquareBrackets = squareBracketsRegex.test(withoutEllipsis) + + if (containsSquareBrackets) { + return { + name: removeSquareBracketsFromSegments(withoutEllipsis), + optional: true, + } + } + + return { + name: withoutEllipsis, + optional: false, + } + }), + } +} diff --git a/nextjs/packages/next/build/spinner.ts b/nextjs/packages/next/build/spinner.ts new file mode 100644 index 0000000000..26e7911915 --- /dev/null +++ b/nextjs/packages/next/build/spinner.ts @@ -0,0 +1,63 @@ +import ora from 'next/dist/compiled/ora' + +const dotsSpinner = { + frames: ['.', '..', '...'], + interval: 200, +} + +export default function createSpinner( + text: string | { prefixText: string }, + options: ora.Options = {}, + logFn: (...data: any[]) => void = console.log +) { + let spinner: undefined | ora.Ora + let prefixText = text && typeof text === 'object' && text.prefixText + + if (process.stdout.isTTY) { + spinner = ora({ + text: typeof text === 'string' ? text : undefined, + prefixText: typeof prefixText === 'string' ? prefixText : undefined, + spinner: dotsSpinner, + stream: process.stdout, + ...options, + }).start() + + // Add capturing of console.log/warn/error to allow pausing + // the spinner before logging and then restarting spinner after + const origLog = console.log + const origWarn = console.warn + const origError = console.error + const origStop = spinner.stop.bind(spinner) + const origStopAndPersist = spinner.stopAndPersist.bind(spinner) + + const logHandle = (method: any, args: any[]) => { + origStop() + method(...args) + spinner!.start() + } + + console.log = (...args: any) => logHandle(origLog, args) + console.warn = (...args: any) => logHandle(origWarn, args) + console.error = (...args: any) => logHandle(origError, args) + + const resetLog = () => { + console.log = origLog + console.warn = origWarn + console.error = origError + } + spinner.stop = (): ora.Ora => { + origStop() + resetLog() + return spinner! + } + spinner.stopAndPersist = (): ora.Ora => { + origStopAndPersist() + resetLog() + return spinner! + } + } else if (prefixText || text) { + logFn(prefixText ? prefixText + '...' : text) + } + + return spinner +} diff --git a/nextjs/packages/next/build/swc/.cargo/config b/nextjs/packages/next/build/swc/.cargo/config new file mode 100644 index 0000000000..9399b83f8c --- /dev/null +++ b/nextjs/packages/next/build/swc/.cargo/config @@ -0,0 +1,4 @@ +[build] +rustflags = [ + "--cfg", "procmacro2_semver_exempt", +] \ No newline at end of file diff --git a/nextjs/packages/next/build/swc/.rustfmt.toml b/nextjs/packages/next/build/swc/.rustfmt.toml new file mode 100644 index 0000000000..69a311ce68 --- /dev/null +++ b/nextjs/packages/next/build/swc/.rustfmt.toml @@ -0,0 +1,3 @@ +format_strings = true +use_field_init_shorthand = true +wrap_comments = true diff --git a/nextjs/packages/next/build/swc/Cargo.lock b/nextjs/packages/next/build/swc/Cargo.lock new file mode 100644 index 0000000000..b730db7b88 --- /dev/null +++ b/nextjs/packages/next/build/swc/Cargo.lock @@ -0,0 +1,2201 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +dependencies = [ + "getrandom 0.2.3", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ast_node" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93f52ce8fac3d0e6720a92b0576d737c01b1b5db4dd786e962e5925f00bf755" +dependencies = [ + "darling", + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "build_const" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + +[[package]] +name = "ctor" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if 1.0.0", + "num_cpus", +] + +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "enum_kind" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b940da354ae81ef0926c5eaa428207b8f4f091d3956c891dfbd124162bed99" +dependencies = [ + "pmutil", + "proc-macro2", + "swc_macros_common", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "from_variant" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0951635027ca477be98f8774abd6f0345233439d63f307e47101acb40c7cc63d" +dependencies = [ + "pmutil", + "proc-macro2", + "swc_macros_common", + "syn", +] + +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7280c75fb2e2fc47080ec80ccc481376923acb04501957fc38f935c3de5088" + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg 1.0.1", + "hashbrown", +] + +[[package]] +name = "is-macro" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a322dd16d960e322c3d92f541b4c1a4f0a2e81e1fdeee430d8cecc8b72e8015f" +dependencies = [ + "Inflector", + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "jemalloc-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3b9f3f5c9b31aa0f5ed3260385ac205db665baa41d49bb8338008ae94ede45" +dependencies = [ + "cc", + "fs_extra", + "libc", +] + +[[package]] +name = "jemallocator" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ae63fcfc45e99ab3d1b29a46782ad679e98436c3169d15a167a1108a724b69" +dependencies = [ + "jemalloc-sys", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lexical" +version = "5.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f404a90a744e32e8be729034fc33b90cf2a56418fbf594d69aa3c0214ad414e5" +dependencies = [ + "cfg-if 1.0.0", + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 1.0.0", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "libmimalloc-sys" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1b8479c593dba88c2741fc50b92e13dbabbbe0bd504d979f244ccc1a5b1c01" +dependencies = [ + "cc", +] + +[[package]] +name = "lock_api" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" +dependencies = [ + "owning_ref", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "lru" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea2d928b485416e8908cff2d97d621db22b27f7b3b6729e438bcf42c671ba91" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "mimalloc" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb74897ce508e6c49156fd1476fc5922cbc6e75183c65e399c765a09122e5130" +dependencies = [ + "libmimalloc-sys", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg 1.0.1", +] + +[[package]] +name = "napi" +version = "1.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cd02f5de365f9bd6e85f1d11176a9ea70ff63ce55ea4412cb4e00fd5a0fe6c" +dependencies = [ + "napi-sys", + "serde", + "serde_json", + "winapi", +] + +[[package]] +name = "napi-build" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbe11972c601a48aa12a0e2aa032e9e251655ce6c6836cac26e5c0b3b5a5dcc" + +[[package]] +name = "napi-derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7ed160148f94ee17936f00288029cb0cfb37c08bbace9f514f735dcd869ed7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43563506c466587478849d80f46383d859b91bbec586580dadeb3639588f2f7e" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "next-swc" +version = "0.0.0" +dependencies = [ + "anyhow", + "backtrace", + "fxhash", + "log", + "napi", + "napi-build", + "napi-derive", + "path-clean", + "regex", + "retain_mut", + "serde", + "serde_json", + "swc", + "swc_atoms", + "swc_common", + "swc_ecma_preset_env", + "swc_ecma_transforms_testing", + "swc_ecmascript", + "swc_node_base", + "testing", +] + +[[package]] +name = "normpath" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a9da8c9922c35a1033d76f7272dfc2e7ee20392083d75aeea6ced23c6266578" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg 1.0.1", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg 1.0.1", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "ordered-float" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039f02eb0f69271f26abe3202189275d7aa2258b903cb0281b5de710a2570ff3" +dependencies = [ + "num-traits", +] + +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi", +] + +[[package]] +name = "owning_ref" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "parking_lot" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" +dependencies = [ + "libc", + "rand 0.6.5", + "rustc_version", + "smallvec 0.6.14", + "winapi", +] + +[[package]] +name = "path-clean" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" + +[[package]] +name = "pathdiff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pmutil" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3894e5d549cccbe44afecf72922f277f603cd4bb0219c8342631ef18fffbe004" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_assertions" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +dependencies = [ + "ansi_term 0.11.0", + "ctor", + "difference", + "output_vt100", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_fmt" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce082a9940a7ace2ad4a8b7d0b1eac6aa378895f18be598230c5f2284ac05426" + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg 0.1.2", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", + "rand_pcg 0.2.1", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "relative-path" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9629de8974fd69c97684736786b807edd3da456d3e3f95341dd9d4cbd8f5ad6" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "retain_mut" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c17925a9027d298a4603d286befe3f9dc0e8ed02523141914eb628798d6e5b" + +[[package]] +name = "rustc-demangle" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scopeguard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", + "serde", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "siphasher" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1" + +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "sourcemap" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e031f2463ecbdd5f34c950f89f5c1e1032f22c0f8e3dc4bdb2e8b6658cf61eb" +dependencies = [ + "base64 0.11.0", + "if_chain", + "lazy_static", + "regex", + "rustc_version", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "st-map" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caeb13a58f859600a7b75fffe66322e1fca0122ca02cfc7262344a7e30502d1" +dependencies = [ + "arrayvec", + "static-map-macro", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static-map-macro" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5503e07f148238811bbfd578684a0457c7284bab41b60d76def35431a1295fd" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_enum" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f584cc881e9e5f1fd6bf827b0444aa94c30d8fe6378cf241071b5f5700b2871f" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "swc" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c967d3cd0f16a199613c1f7471776b74afb6cb478a463289e8914c428525c06c" +dependencies = [ + "ahash", + "anyhow", + "base64 0.12.3", + "dashmap", + "either", + "log", + "lru", + "once_cell", + "pathdiff", + "regex", + "serde", + "serde_json", + "sourcemap", + "swc_atoms", + "swc_bundler", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_ext_transforms", + "swc_ecma_loader", + "swc_ecma_minifier", + "swc_ecma_parser", + "swc_ecma_preset_env", + "swc_ecma_transforms", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", + "swc_node_base", + "swc_visit", +] + +[[package]] +name = "swc_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bcdb70cb6ecee568e5acfda1a8c6e851ecf49443e5fb51f1b13613b5d04d2b0" +dependencies = [ + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "swc_bundler" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c214b3f56aa5fc76fca81d65358f3c6390a170518253c941578bb8b45e700d1" +dependencies = [ + "ahash", + "anyhow", + "crc", + "fxhash", + "indexmap", + "is-macro", + "log", + "once_cell", + "petgraph", + "radix_fmt", + "relative-path", + "retain_mut", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_loader", + "swc_ecma_parser", + "swc_ecma_transforms", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_common" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f303985ebd578a033371a87be28cb54189d3ae5d492622523200f4de08db5275" +dependencies = [ + "ast_node", + "atty", + "cfg-if 0.1.10", + "either", + "from_variant", + "fxhash", + "log", + "num-bigint", + "once_cell", + "owning_ref", + "parking_lot", + "scoped-tls", + "serde", + "sourcemap", + "string_cache", + "swc_eq_ignore_macros", + "swc_visit", + "termcolor", + "unicode-width", +] + +[[package]] +name = "swc_ecma_ast" +version = "0.49.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ab89e5a11289d43f14da2b65b9e37c8fb31091c33f4fad66791ef65c04a1b" +dependencies = [ + "is-macro", + "num-bigint", + "serde", + "string_enum", + "swc_atoms", + "swc_common", +] + +[[package]] +name = "swc_ecma_codegen" +version = "0.64.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41597b6fbcd6001575be2b9fb38eb8fb3ebd972827998bcb522078a8d8606cc" +dependencies = [ + "bitflags", + "num-bigint", + "sourcemap", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen_macros", + "swc_ecma_parser", +] + +[[package]] +name = "swc_ecma_codegen_macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51af418026cb4ea588e2b15fa206c44e09a3184b718e12a0919729c7c3ad20d3" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "swc_ecma_ext_transforms" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5588dbe1dbd6c3ee50c3b292a3adfe4dd63f8bf487bbfe3f1b54f64894a67b15" +dependencies = [ + "phf", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_loader" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05e3b054b690e610384fc8f5d244182cbd62691423755ca1c4b05cb0bfa6b46" +dependencies = [ + "anyhow", + "dashmap", + "lru", + "normpath", + "once_cell", + "regex", + "serde", + "serde_json", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_minifier" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f57ea673271d857cc278f2e62b9915520e4bf2defefb46d48291b8304ce0138" +dependencies = [ + "fxhash", + "indexmap", + "log", + "once_cell", + "regex", + "retain_mut", + "serde", + "serde_json", + "serde_regex", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_transforms", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", + "unicode-xid", +] + +[[package]] +name = "swc_ecma_parser" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47e935b2ff6bdb6cd7ccb20aa1a6cb94f9993e5fc44ab69692dcb87bd8b4238" +dependencies = [ + "either", + "enum_kind", + "fxhash", + "lexical", + "log", + "num-bigint", + "serde", + "smallvec 1.6.1", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", + "unicode-xid", +] + +[[package]] +name = "swc_ecma_preset_env" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812a7b0a6d1b9eff332f01068327d7f55dc60ab3eb301ee69e591fbac063ea17" +dependencies = [ + "dashmap", + "fxhash", + "indexmap", + "once_cell", + "semver", + "serde", + "serde_json", + "st-map", + "string_enum", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_transforms", + "swc_ecma_utils", + "swc_ecma_visit", + "walkdir", +] + +[[package]] +name = "swc_ecma_transforms" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c52640be334b353c4c762e786dd054dae6f8ee8534418058f1184cfca057786c" +dependencies = [ + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_transforms_compat", + "swc_ecma_transforms_module", + "swc_ecma_transforms_optimization", + "swc_ecma_transforms_proposal", + "swc_ecma_transforms_react", + "swc_ecma_transforms_typescript", + "swc_ecma_utils", + "swc_ecma_visit", + "unicode-xid", +] + +[[package]] +name = "swc_ecma_transforms_base" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f9357fffe6d1b2922cf0a91a0c93e9a103811a0647f1a7ad80c215f9afbd09" +dependencies = [ + "fxhash", + "once_cell", + "phf", + "scoped-tls", + "smallvec 1.6.1", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_classes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f17fb4fe2ad6651f7c832970526e81a3263ab89657b844d7b63564f6a25d19" +dependencies = [ + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_compat" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b5243d08c700668f16e1fbbec6e645f302bc1dd40375f5faf15a860e41090f2" +dependencies = [ + "arrayvec", + "fxhash", + "indexmap", + "is-macro", + "num-bigint", + "ordered-float", + "serde", + "smallvec 1.6.1", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_transforms_base", + "swc_ecma_transforms_classes", + "swc_ecma_transforms_macros", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7680ada61fa22c2164c3f32864efba31566710b503c30631ccc3b6f0fa800bc" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "swc_ecma_transforms_module" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a054b76d72f294ddff1576eb0b61f3d78edceb670e6caf62c06adce8b861fdb" +dependencies = [ + "Inflector", + "anyhow", + "fxhash", + "indexmap", + "pathdiff", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_loader", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_optimization" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76ddc2c8e023af9e34aaa8f295605f82e2cd7dddbdaa69ec155bde50ed2d81c" +dependencies = [ + "dashmap", + "fxhash", + "indexmap", + "log", + "once_cell", + "retain_mut", + "serde_json", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_proposal" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf6f5f9afea77d720798604b9f755f0f4a63b9996b437c84712ce59ca7f78a" +dependencies = [ + "either", + "fxhash", + "serde", + "smallvec 1.6.1", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_transforms_classes", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_react" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d614204eca90380510fc6cc9a5f00e7a1dcac20939d39f883f770341ee08b670" +dependencies = [ + "base64 0.13.0", + "dashmap", + "indexmap", + "once_cell", + "regex", + "serde", + "sha-1", + "string_enum", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_testing" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f92855b778ba45bf441335e7d95ed0ff1df8b1deea08d9b4301cf214cb78e64" +dependencies = [ + "ansi_term 0.12.1", + "serde", + "serde_json", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", + "tempfile", + "testing", +] + +[[package]] +name = "swc_ecma_transforms_typescript" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531efb950048802b6330bf8dab8dc858974a1807393175050d806d928ac0d27f" +dependencies = [ + "fxhash", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_utils" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "725fb37b8f7ac49b9edd7001dba006f0aa9b9a85623337994ab89b0f2b12032b" +dependencies = [ + "once_cell", + "scoped-tls", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", + "unicode-xid", +] + +[[package]] +name = "swc_ecma_visit" +version = "0.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754afc3c64e5f39fc17874170548e4f94612bcfb52fd7882437c149f713b3473" +dependencies = [ + "num-bigint", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_visit", +] + +[[package]] +name = "swc_ecmascript" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d7cf5f01d49cba27028518c15bf45343d0d99799f60ab7aabcbc0b6a440204" +dependencies = [ + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_transforms", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_eq_ignore_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c8f200a2eaed938e7c1a685faaa66e6d42fa9e17da5f62572d3cbc335898f5e" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "swc_macros_common" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ed2e930f5a1a4071fe62c90fd3a296f6030e5d94bfe13993244423caf59a78" +dependencies = [ + "pmutil", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "swc_node_base" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26cb24556d6123caed150c6736fec8ad86f368b1bd97ed44db78f2779fb505a" +dependencies = [ + "dashmap", + "jemallocator", + "mimalloc", + "swc_common", +] + +[[package]] +name = "swc_visit" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b24bd6153b94e06cae99d98d6a12b1fbe6063709cdb4b0f7ea25d2c0a62bdee7" +dependencies = [ + "either", + "swc_visit_macros", +] + +[[package]] +name = "swc_visit_macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b2825fee79f10d0166e8e650e79c7a862fb991db275743083f07555d7641f0" +dependencies = [ + "Inflector", + "pmutil", + "proc-macro2", + "quote", + "swc_macros_common", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "testing" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c1ecb819cc019f96a0d1f268e080588c47eab4cc804d8dadac52e34f264430" +dependencies = [ + "ansi_term 0.12.1", + "difference", + "env_logger", + "log", + "once_cell", + "pretty_assertions", + "regex", + "swc_common", + "testing_macros", +] + +[[package]] +name = "testing_macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c96cc628186322f8ba6c3b9368497bb0a99c07da8b3641cf2efc7eb7cb5299" +dependencies = [ + "anyhow", + "glob", + "pmutil", + "proc-macro2", + "quote", + "regex", + "relative-path", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "unicode-bidi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/nextjs/packages/next/build/swc/Cargo.toml b/nextjs/packages/next/build/swc/Cargo.toml new file mode 100644 index 0000000000..29641a782d --- /dev/null +++ b/nextjs/packages/next/build/swc/Cargo.toml @@ -0,0 +1,37 @@ +[package] +edition = "2018" +name = "next-swc" +version = "0.0.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1.0" +backtrace = "0.3" +napi = { version = "1", features = ["serde-json"] } +napi-derive = "1" +path-clean = "0.1" +regex = "1.5" +serde = "1" +serde_json = "1" +swc = "0.33" +swc_atoms = "0.2.6" +swc_common = { version = "0.11.1", features = ["tty-emitter", "sourcemap"] } +swc_node_base = "0.2.0" +swc_ecmascript = { version = "0.50.0", features = ["codegen", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"] } +swc_ecma_preset_env = "0.31.0" +fxhash = "0.2.1" +retain_mut = "0.1.3" +log = "0.4.14" + + +[build-dependencies] +napi-build = "1" + +[dev-dependencies] +swc_ecma_transforms_testing = "0.24.0" +testing = "0.12.0" + +[profile.release] +lto = true diff --git a/nextjs/packages/next/build/swc/build.rs b/nextjs/packages/next/build/swc/build.rs new file mode 100644 index 0000000000..1f866b6a3c --- /dev/null +++ b/nextjs/packages/next/build/swc/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/nextjs/packages/next/build/swc/index.js b/nextjs/packages/next/build/swc/index.js new file mode 100644 index 0000000000..9048fd5b1a --- /dev/null +++ b/nextjs/packages/next/build/swc/index.js @@ -0,0 +1,71 @@ +const { loadBinding } = require('@node-rs/helper') +const path = require('path') + +/** + * __dirname means load native addon from current dir + * 'next-swc' is the name of native addon + * the second arguments was decided by `napi.name` field in `package.json` + * the third arguments was decided by `name` field in `package.json` + * `loadBinding` helper will load `next-swc.[PLATFORM].node` from `__dirname` first + * If failed to load addon, it will fallback to load from `next-swc-[PLATFORM]` + */ +const bindings = loadBinding( + path.join(__dirname, '../../../native'), + 'next-swc', + 'next-swc' +) + +async function transform(src, options) { + const isModule = typeof src !== 'string' + options = options || {} + + if (options?.jsc?.parser) { + options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript' + } + + const { plugin, ...newOptions } = options + + if (plugin) { + const m = + typeof src === 'string' + ? await this.parse(src, options?.jsc?.parser) + : src + return this.transform(plugin(m), newOptions) + } + + return bindings.transform( + isModule ? JSON.stringify(src) : src, + isModule, + toBuffer(newOptions) + ) +} + +function transformSync(src, options) { + const isModule = typeof src !== 'string' + options = options || {} + + if (options?.jsc?.parser) { + options.jsc.parser.syntax = options.jsc.parser.syntax ?? 'ecmascript' + } + + const { plugin, ...newOptions } = options + + if (plugin) { + const m = + typeof src === 'string' ? this.parseSync(src, options?.jsc?.parser) : src + return this.transformSync(plugin(m), newOptions) + } + + return bindings.transformSync( + isModule ? JSON.stringify(src) : src, + isModule, + toBuffer(newOptions) + ) +} + +function toBuffer(t) { + return Buffer.from(JSON.stringify(t)) +} + +module.exports.transform = transform +module.exports.transformSync = transformSync diff --git a/nextjs/packages/next/build/swc/rust-toolchain b/nextjs/packages/next/build/swc/rust-toolchain new file mode 100644 index 0000000000..6f213785de --- /dev/null +++ b/nextjs/packages/next/build/swc/rust-toolchain @@ -0,0 +1 @@ +nightly-2021-03-25 \ No newline at end of file diff --git a/nextjs/packages/next/build/swc/src/amp_attributes.rs b/nextjs/packages/next/build/swc/src/amp_attributes.rs new file mode 100644 index 0000000000..b222d019d4 --- /dev/null +++ b/nextjs/packages/next/build/swc/src/amp_attributes.rs @@ -0,0 +1,63 @@ +use swc_atoms::JsWord; +use swc_ecmascript::ast::{ + Ident, JSXAttr, JSXAttrName, JSXAttrOrSpread, JSXElementName, JSXOpeningElement, +}; +use swc_ecmascript::visit::Fold; + +pub fn amp_attributes() -> impl Fold { + AmpAttributePatcher::default() +} + +#[derive(Debug, Default)] +struct AmpAttributePatcher {} + +impl Fold for AmpAttributePatcher { + fn fold_jsx_opening_element(&mut self, node: JSXOpeningElement) -> JSXOpeningElement { + let JSXOpeningElement { + name, + mut attrs, + span, + self_closing, + type_args, + } = node; + let n = name.clone(); + + if let JSXElementName::Ident(Ident { sym, .. }) = name { + if sym.starts_with("amp-") { + for i in 0..attrs.len() { + if let JSXAttrOrSpread::JSXAttr(JSXAttr { + name: + JSXAttrName::Ident(Ident { + sym, + span: s, + optional: o, + }), + span, + value, + }) = &attrs[i] + { + if sym as &str == "className" { + attrs[i] = JSXAttrOrSpread::JSXAttr(JSXAttr { + name: JSXAttrName::Ident(Ident { + sym: JsWord::from("class"), + span: s.clone(), + optional: o.clone(), + }), + span: span.clone(), + value: value.clone(), + }) + } + } + } + } + } + + JSXOpeningElement { + name: n, + attrs, + span, + self_closing, + type_args, + } + } +} diff --git a/nextjs/packages/next/build/swc/src/hook_optimizer.rs b/nextjs/packages/next/build/swc/src/hook_optimizer.rs new file mode 100644 index 0000000000..4bb9791562 --- /dev/null +++ b/nextjs/packages/next/build/swc/src/hook_optimizer.rs @@ -0,0 +1,119 @@ +use swc_atoms::JsWord; +use swc_common::DUMMY_SP; +use swc_ecmascript::ast::{ + ArrayPat, Decl, Expr, ExprOrSuper, Ident, ImportDecl, ImportSpecifier, KeyValuePatProp, Number, + ObjectPat, ObjectPatProp, Pat, PropName, VarDecl, VarDeclarator, +}; +use swc_ecmascript::visit::{Fold, FoldWith}; + +pub fn hook_optimizer() -> impl Fold { + HookOptimizer::default() +} + +#[derive(Debug, Default)] +struct HookOptimizer { + hooks: Vec, +} + +impl Fold for HookOptimizer { + // Find hooks imported from react/preact + fn fold_import_decl(&mut self, decl: ImportDecl) -> ImportDecl { + let ImportDecl { + ref src, + ref specifiers, + .. + } = decl; + if &src.value == "react" || &src.value == "preact/hooks" { + for specifier in specifiers { + if let ImportSpecifier::Named(named_specifier) = specifier { + if named_specifier.local.sym.starts_with("use") { + self.hooks.push(named_specifier.local.sym.clone()) + } + } + } + } + + decl + } + // Transform array desctructing to object destructuring for relevant hooks + fn fold_decl(&mut self, node: Decl) -> Decl { + let node = node.fold_children_with(self); + match node { + Decl::Var(VarDecl { + decls, + span, + kind, + declare, + }) => { + let mut new_decls = Vec::with_capacity(decls.len()); + for decl in decls { + new_decls.push(self.get_decl(decl)); + } + + Decl::Var(VarDecl { + decls: new_decls, + span, + kind, + declare, + }) + } + _ => node, + } + } +} + +impl HookOptimizer { + fn get_decl(&mut self, decl: VarDeclarator) -> VarDeclarator { + let VarDeclarator { + name, + init, + span, + definite, + } = &decl; + let init_clone = init.clone(); + if let Pat::Array(a) = name { + if let Expr::Call(c) = &*init.as_deref().unwrap() { + if let ExprOrSuper::Expr(i) = &c.callee { + if let Expr::Ident(Ident { sym, .. }) = &**i { + if self.hooks.contains(&sym) { + let name = get_object_pattern(&a); + return VarDeclarator { + name, + init: init_clone, + span: *span, + definite: *definite, + }; + } + } + } + } + } + + return decl; + } +} + +fn get_object_pattern(array_pattern: &ArrayPat) -> Pat { + let props: Vec = array_pattern + .elems + .iter() + .enumerate() + .filter_map(|(i, elem)| match elem { + Some(elem) => Some(ObjectPatProp::KeyValue(KeyValuePatProp { + key: PropName::Num(Number { + value: i as f64, + span: DUMMY_SP, + }), + value: Box::new(elem.clone()), + })), + None => None, + }) + .collect(); + + Pat::Object(ObjectPat { + props, + span: DUMMY_SP, + optional: false, + type_ann: None, + }) +} diff --git a/nextjs/packages/next/build/swc/src/lib.rs b/nextjs/packages/next/build/swc/src/lib.rs new file mode 100644 index 0000000000..f4495e4783 --- /dev/null +++ b/nextjs/packages/next/build/swc/src/lib.rs @@ -0,0 +1,96 @@ +/* +Copyright (c) 2017 The swc Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +#![recursion_limit = "2048"] +//#![deny(clippy::all)] + +#[macro_use] +extern crate napi_derive; +/// Explicit extern crate to use allocator. +extern crate swc_node_base; + +use backtrace::Backtrace; +use napi::{CallContext, Env, JsObject, JsUndefined}; +use std::{env, panic::set_hook, sync::Arc}; +use swc::{Compiler, TransformOutput}; +use swc_common::{ + self, + errors::{ColorConfig, Handler}, + sync::Lazy, + FilePathMapping, SourceMap, +}; + +mod amp_attributes; +mod hook_optimizer; +mod next_dynamic; +pub mod next_ssg; +mod transform; +mod util; + +static COMPILER: Lazy> = Lazy::new(|| { + let cm = Arc::new(SourceMap::new(FilePathMapping::empty())); + let handler = Arc::new(Handler::with_tty_emitter( + ColorConfig::Always, + true, + false, + Some(cm.clone()), + )); + + Arc::new(Compiler::new(cm.clone(), handler)) +}); + +#[module_exports] +fn init(mut exports: JsObject) -> napi::Result<()> { + if cfg!(debug_assertions) || env::var("SWC_DEBUG").unwrap_or_default() == "1" { + set_hook(Box::new(|panic_info| { + let backtrace = Backtrace::new(); + println!("Panic: {:?}\nBacktrace: {:?}", panic_info, backtrace); + })); + } + + exports.create_named_method("transform", transform::transform)?; + exports.create_named_method("transformSync", transform::transform_sync)?; + + Ok(()) +} + +fn get_compiler(_ctx: &CallContext) -> Arc { + COMPILER.clone() +} + +#[js_function] +fn construct_compiler(ctx: CallContext) -> napi::Result { + // TODO: Assign swc::Compiler + ctx.env.get_undefined() +} + +pub fn complete_output(env: &Env, output: TransformOutput) -> napi::Result { + env.to_js_value(&output)?.coerce_to_object() +} + +pub type ArcCompiler = Arc; diff --git a/nextjs/packages/next/build/swc/src/next_dynamic.rs b/nextjs/packages/next/build/swc/src/next_dynamic.rs new file mode 100644 index 0000000000..fb49d9872b --- /dev/null +++ b/nextjs/packages/next/build/swc/src/next_dynamic.rs @@ -0,0 +1,214 @@ +use swc_atoms::js_word; +use swc_common::{FileName, DUMMY_SP}; +use swc_ecmascript::ast::{ + ArrayLit, ArrowExpr, BinExpr, BinaryOp, BlockStmtOrExpr, CallExpr, Expr, ExprOrSpread, + ExprOrSuper, Ident, ImportDecl, ImportSpecifier, KeyValueProp, Lit, MemberExpr, ObjectLit, Prop, + PropName, PropOrSpread, Str, StrKind, +}; +use swc_ecmascript::utils::{ + ident::{Id, IdentLike}, + HANDLER, +}; +use swc_ecmascript::visit::{Fold, FoldWith}; + +pub fn next_dynamic(filename: FileName) -> impl Fold { + NextDynamicPatcher { + filename, + dynamic_bindings: vec![], + } +} + +#[derive(Debug)] +struct NextDynamicPatcher { + filename: FileName, + dynamic_bindings: Vec, +} + +impl Fold for NextDynamicPatcher { + fn fold_import_decl(&mut self, decl: ImportDecl) -> ImportDecl { + let ImportDecl { + ref src, + ref specifiers, + .. + } = decl; + if &src.value == "next/dynamic" { + for specifier in specifiers { + if let ImportSpecifier::Default(default_specifier) = specifier { + self.dynamic_bindings.push(default_specifier.local.to_id()); + } + } + } + + decl + } + + fn fold_call_expr(&mut self, expr: CallExpr) -> CallExpr { + let mut expr = expr.fold_children_with(self); + if let ExprOrSuper::Expr(i) = &expr.callee { + if let Expr::Ident(identifier) = &**i { + if self.dynamic_bindings.contains(&identifier.to_id()) { + if expr.args.len() == 0 { + HANDLER.with(|handler| { + handler + .struct_span_err( + identifier.span, + "next/dynamic requires at least one argument", + ) + .emit() + }); + } else if expr.args.len() > 2 { + HANDLER.with(|handler| { + handler + .struct_span_err(identifier.span, "next/dynamic only accepts 2 arguments") + .emit() + }); + } + + let mut import_specifier = None; + if let Expr::Arrow(ArrowExpr { + body: BlockStmtOrExpr::Expr(e), + .. + }) = &*expr.args[0].expr + { + if let Expr::Call(CallExpr { + args: a, callee, .. + }) = &**e + { + if let ExprOrSuper::Expr(e) = callee { + if let Expr::Ident(Ident { sym, .. }) = &**e { + if sym == "import" { + if a.len() == 0 { + // Do nothing, import_specifier will remain None + // triggering error below + } else if let Expr::Lit(Lit::Str(Str { value, .. })) = &*a[0].expr { + import_specifier = Some(value.clone()); + } + } + } + } + } + } + + if let None = import_specifier { + HANDLER.with(|handler| { + handler + .struct_span_err( + identifier.span, + "First argument for next/dynamic must be an arrow function returning a valid \ + dynamic import call e.g. `dynamic(() => import('../some-component'))`", + ) + .emit() + }); + } + + // loadableGenerated: { + // webpack: () => [require.resolveWeak('../components/hello')], + // modules: + // ["/project/src/file-being-transformed.js -> " + '../components/hello'] } + let generated = Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props: vec![ + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(Ident::new("webpack".into(), DUMMY_SP)), + value: Box::new(Expr::Arrow(ArrowExpr { + params: vec![], + body: BlockStmtOrExpr::Expr(Box::new(Expr::Array(ArrayLit { + elems: vec![Some(ExprOrSpread { + expr: Box::new(Expr::Call(CallExpr { + callee: ExprOrSuper::Expr(Box::new(Expr::Member(MemberExpr { + obj: ExprOrSuper::Expr(Box::new(Expr::Ident(Ident { + sym: js_word!("require"), + span: DUMMY_SP, + optional: false, + }))), + prop: Box::new(Expr::Ident(Ident { + sym: "resolveWeak".into(), + span: DUMMY_SP, + optional: false, + })), + computed: false, + span: DUMMY_SP, + }))), + args: vec![ExprOrSpread { + expr: Box::new(Expr::Lit(Lit::Str(Str { + value: self.filename.to_string().into(), + span: DUMMY_SP, + kind: StrKind::Synthesized {}, + has_escape: false, + }))), + spread: None, + }], + span: DUMMY_SP, + type_args: None, + })), + spread: None, + })], + span: DUMMY_SP, + }))), + is_async: false, + is_generator: false, + span: DUMMY_SP, + return_type: None, + type_params: None, + })), + }))), + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(Ident::new("modules".into(), DUMMY_SP)), + value: Box::new(Expr::Array(ArrayLit { + elems: vec![Some(ExprOrSpread { + expr: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::Add, + left: Box::new(Expr::Lit(Lit::Str(Str { + value: format!("{} -> ", self.filename).into(), + span: DUMMY_SP, + kind: StrKind::Synthesized {}, + has_escape: false, + }))), + right: Box::new(Expr::Lit(Lit::Str(Str { + value: import_specifier.unwrap(), + span: DUMMY_SP, + kind: StrKind::Normal { + contains_quote: false, + }, + has_escape: false, + }))), + })), + spread: None, + })], + span: DUMMY_SP, + })), + }))), + ], + })); + + let mut props = vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident(Ident::new("loadableGenerated".into(), DUMMY_SP)), + value: generated, + })))]; + + if expr.args.len() == 2 { + if let Expr::Object(ObjectLit { + props: options_props, + .. + }) = &*expr.args[1].expr + { + props.extend(options_props.iter().cloned()); + } + } + + let second_arg = ExprOrSpread { + spread: None, + expr: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props, + })), + }; + + expr.args.push(second_arg); + } + } + } + expr + } +} diff --git a/nextjs/packages/next/build/swc/src/next_ssg.rs b/nextjs/packages/next/build/swc/src/next_ssg.rs new file mode 100644 index 0000000000..b89ec11241 --- /dev/null +++ b/nextjs/packages/next/build/swc/src/next_ssg.rs @@ -0,0 +1,623 @@ +use fxhash::FxHashSet; +use std::mem::take; +use swc_common::pass::{Repeat, Repeated}; +use swc_common::DUMMY_SP; +use swc_ecmascript::ast::*; +use swc_ecmascript::utils::ident::IdentLike; +use swc_ecmascript::visit::FoldWith; +use swc_ecmascript::{ + utils::Id, + visit::{noop_fold_type, Fold}, +}; + +/// Note: This paths requires runnning `resolver` **before** running this. +pub fn next_ssg() -> impl Fold { + Repeat::new(NextSsg { + state: Default::default(), + in_lhs_of_var: false, + }) +} + +/// State of the transforms. Shared by the anayzer and the tranform. +#[derive(Debug, Default)] +struct State { + /// Identifiers referenced by non-data function codes. + /// + /// Cleared before running each pass, because we drop ast nodes between the + /// passes. + refs_from_other: FxHashSet, + + /// Identifiers referenced by data functions or derivatives. + /// + /// Preserved between runs, because we should remember derivatives of data + /// functions as the data function itself is already removed. + refs_from_data_fn: FxHashSet, + + cur_declaring: FxHashSet, + + is_prerenderer: bool, + is_server_props: bool, + done: bool, + + should_run_again: bool, +} + +impl State { + fn is_data_identifier(&mut self, i: &Ident) -> bool { + let ssg_exports = &[ + "getStaticProps", + "getStaticPaths", + "getServerSideProps", + "unstable_getStaticProps", + "unstable_getStaticPaths", + "unstable_getServerProps", + "unstable_getServerSideProps", + ]; + + if ssg_exports.contains(&&*i.sym) { + if &*i.sym == "" { + if self.is_prerenderer { + panic!( + "You can not use getStaticProps or getStaticPaths with \ + getServerSideProps. To use SSG, please remove getServerSideProps" + ) + } + + self.is_server_props = true; + } else { + if self.is_server_props { + panic!( + "You can not use getStaticProps or getStaticPaths with \ + getServerSideProps. To use SSG, please remove getServerSideProps" + ) + } + + self.is_prerenderer = true; + } + + true + } else { + false + } + } +} + +struct Analyzer<'a> { + state: &'a mut State, + in_lhs_of_var: bool, + in_data_fn: bool, +} + +impl Analyzer<'_> { + fn add_ref(&mut self, id: Id) { + log::trace!("add_ref({}{:?}, data = {})", id.0, id.1, self.in_data_fn); + if self.in_data_fn { + self.state.refs_from_data_fn.insert(id); + } else { + if self.state.cur_declaring.contains(&id) { + return; + } + + self.state.refs_from_other.insert(id); + } + } +} + +impl Fold for Analyzer<'_> { + // This is important for reducing binary sizes. + noop_fold_type!(); + + fn fold_binding_ident(&mut self, i: BindingIdent) -> BindingIdent { + if !self.in_lhs_of_var || self.in_data_fn { + self.add_ref(i.id.to_id()); + } + + i + } + + fn fold_export_named_specifier(&mut self, s: ExportNamedSpecifier) -> ExportNamedSpecifier { + self.add_ref(s.orig.to_id()); + + s + } + + fn fold_expr(&mut self, e: Expr) -> Expr { + let e = e.fold_children_with(self); + + match &e { + Expr::Ident(i) => { + self.add_ref(i.to_id()); + } + _ => {} + } + + e + } + + fn fold_fn_decl(&mut self, f: FnDecl) -> FnDecl { + let old_in_data = self.in_data_fn; + + self.state.cur_declaring.insert(f.ident.to_id()); + + self.in_data_fn |= self.state.is_data_identifier(&f.ident); + log::trace!( + "ssg: Handling `{}{:?}`; in_data_fn = {:?}", + f.ident.sym, + f.ident.span.ctxt, + self.in_data_fn + ); + + let f = f.fold_children_with(self); + + self.state.cur_declaring.remove(&f.ident.to_id()); + + self.in_data_fn = old_in_data; + + f + } + + fn fold_fn_expr(&mut self, f: FnExpr) -> FnExpr { + let f = f.fold_children_with(self); + + if let Some(id) = &f.ident { + self.add_ref(id.to_id()); + } + + f + } + + fn fold_member_expr(&mut self, mut e: MemberExpr) -> MemberExpr { + e.obj = e.obj.fold_with(self); + + if e.computed { + e.prop = e.prop.fold_with(self); + } + + e + } + + /// Drops [ExportDecl] if all speicifers are removed. + fn fold_module_item(&mut self, s: ModuleItem) -> ModuleItem { + match s { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if !e.specifiers.is_empty() => { + let e = e.fold_with(self); + + if e.specifiers.is_empty() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + + return ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)); + } + _ => {} + }; + + // Visit children to ensure that all references is added to the scope. + let s = s.fold_children_with(self); + + match &s { + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => match &e.decl { + Decl::Fn(f) => { + // Drop getStaticProps. + if self.state.is_data_identifier(&f.ident) { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + } + + Decl::Var(d) => { + if d.decls.is_empty() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + } + _ => {} + }, + + _ => {} + } + + s + } + + fn fold_named_export(&mut self, mut n: NamedExport) -> NamedExport { + if n.src.is_some() { + n.specifiers = n.specifiers.fold_with(self); + } + + n + } + + fn fold_prop(&mut self, p: Prop) -> Prop { + let p = p.fold_children_with(self); + + match &p { + Prop::Shorthand(i) => { + self.add_ref(i.to_id()); + } + _ => {} + } + + p + } + + fn fold_var_declarator(&mut self, mut v: VarDeclarator) -> VarDeclarator { + let old_in_data = self.in_data_fn; + + match &v.name { + Pat::Ident(name) => { + if self.state.is_data_identifier(&name.id) { + self.in_data_fn = true; + } + } + _ => {} + } + + let old_in_lhs_of_var = self.in_lhs_of_var; + + self.in_lhs_of_var = true; + v.name = v.name.fold_with(self); + + self.in_lhs_of_var = false; + v.init = v.init.fold_with(self); + + self.in_lhs_of_var = old_in_lhs_of_var; + + self.in_data_fn = old_in_data; + + v + } +} + +/// Actual implementation of the transform. +struct NextSsg { + state: State, + in_lhs_of_var: bool, +} + +impl NextSsg { + fn should_remove(&self, id: Id) -> bool { + self.state.refs_from_data_fn.contains(&id) && !self.state.refs_from_other.contains(&id) + } + + /// Mark identifiers in `n` as a candidate for removal. + fn mark_as_candidate(&mut self, n: N) -> N + where + N: for<'aa> FoldWith>, + { + log::debug!("mark_as_candidate"); + + // Analyzer never change `in_data_fn` to false, so all identifiers in `n` will + // be marked as referenced from a data function. + let mut v = Analyzer { + state: &mut self.state, + in_lhs_of_var: false, + in_data_fn: true, + }; + + let n = n.fold_with(&mut v); + self.state.should_run_again = true; + n + } +} + +impl Repeated for NextSsg { + fn changed(&self) -> bool { + self.state.should_run_again + } + + fn reset(&mut self) { + self.state.refs_from_other.clear(); + self.state.cur_declaring.clear(); + self.state.should_run_again = false; + } +} + +/// `VisitMut` is faster than [Fold], but we use [Fold] because it's much easier +/// to read. +/// +/// Note: We don't implement `fold_script` because next.js doesn't use it. +impl Fold for NextSsg { + // This is important for reducing binary sizes. + noop_fold_type!(); + + fn fold_import_decl(&mut self, mut i: ImportDecl) -> ImportDecl { + // Imports for side effects. + if i.specifiers.is_empty() { + return i; + } + + i.specifiers.retain(|s| match s { + ImportSpecifier::Named(ImportNamedSpecifier { local, .. }) + | ImportSpecifier::Default(ImportDefaultSpecifier { local, .. }) + | ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => { + if self.should_remove(local.to_id()) { + log::trace!( + "Dropping import `{}{:?}` because it should be removed", + local.sym, + local.span.ctxt + ); + + self.state.should_run_again = true; + false + } else { + true + } + } + }); + + i + } + + fn fold_module(&mut self, mut m: Module) -> Module { + log::info!("ssg: Start"); + { + // Fill the state. + let mut v = Analyzer { + state: &mut self.state, + in_lhs_of_var: false, + in_data_fn: false, + }; + m = m.fold_with(&mut v); + } + + // TODO: Use better detection logic + // if !self.state.is_prerenderer && !self.state.is_server_props { + // return m; + // } + + m.fold_children_with(self) + } + + fn fold_module_item(&mut self, i: ModuleItem) -> ModuleItem { + match i { + ModuleItem::ModuleDecl(ModuleDecl::Import(i)) => { + let is_for_side_effect = i.specifiers.is_empty(); + let i = i.fold_with(self); + + if !is_for_side_effect && i.specifiers.is_empty() { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + } + + return ModuleItem::ModuleDecl(ModuleDecl::Import(i)); + } + _ => {} + } + + let i = i.fold_children_with(self); + + match &i { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if e.specifiers.is_empty() => { + return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) + } + _ => {} + } + + i + } + + fn fold_module_items(&mut self, mut items: Vec) -> Vec { + items = items.fold_children_with(self); + + // Drop nodes. + items.retain(|s| match s { + ModuleItem::Stmt(Stmt::Empty(..)) => false, + _ => true, + }); + + if !self.state.done && !self.state.should_run_again { + self.state.done = true; + + if items.iter().any(|s| s.is_module_decl()) { + let mut var = Some(VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident( + Ident::new( + if self.state.is_prerenderer { + "__N_SSG".into() + } else { + "__N_SSP".into() + }, + DUMMY_SP, + ) + .into(), + ), + init: Some(Box::new(Expr::Lit(Lit::Bool(Bool { + span: DUMMY_SP, + value: true, + })))), + definite: Default::default(), + }); + + let mut new = Vec::with_capacity(items.len() + 1); + for item in take(&mut items) { + match &item { + ModuleItem::ModuleDecl( + ModuleDecl::ExportNamed(..) + | ModuleDecl::ExportDecl(..) + | ModuleDecl::ExportDefaultDecl(..) + | ModuleDecl::ExportDefaultExpr(..), + ) => { + if let Some(var) = var.take() { + new.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl( + ExportDecl { + span: DUMMY_SP, + decl: Decl::Var(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + declare: Default::default(), + decls: vec![var], + }), + }, + ))) + } + } + + _ => {} + } + + new.push(item); + } + + return new; + } + } + + items + } + + fn fold_named_export(&mut self, mut n: NamedExport) -> NamedExport { + n.specifiers = n.specifiers.fold_with(self); + + n.specifiers.retain(|s| { + let preserve = match s { + ExportSpecifier::Namespace(ExportNamespaceSpecifier { name: exported, .. }) + | ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. }) + | ExportSpecifier::Named(ExportNamedSpecifier { + exported: Some(exported), + .. + }) => !self.state.is_data_identifier(&exported), + ExportSpecifier::Named(s) => !self.state.is_data_identifier(&s.orig), + }; + + if !preserve { + log::trace!("Dropping a export specifier because it's a data identifier"); + + match s { + ExportSpecifier::Named(ExportNamedSpecifier { orig, .. }) => { + self.state.should_run_again = true; + self.state.refs_from_data_fn.insert(orig.to_id()); + } + _ => {} + } + } + + preserve + }); + + n + } + + /// This methods returns [Pat::Invalid] if the pattern should be removed. + fn fold_pat(&mut self, mut p: Pat) -> Pat { + p = p.fold_children_with(self); + + if self.in_lhs_of_var { + match &mut p { + Pat::Ident(name) => { + if self.should_remove(name.id.to_id()) { + self.state.should_run_again = true; + log::trace!( + "Dropping var `{}{:?}` because it should be removed", + name.id.sym, + name.id.span.ctxt + ); + + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + Pat::Array(arr) => { + if !arr.elems.is_empty() { + arr.elems.retain(|e| match e { + Some(Pat::Invalid(..)) => return false, + _ => true, + }); + + if arr.elems.is_empty() { + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + } + Pat::Object(obj) => { + if !obj.props.is_empty() { + obj.props = take(&mut obj.props) + .into_iter() + .filter_map(|prop| match prop { + ObjectPatProp::KeyValue(prop) => { + if prop.value.is_invalid() { + None + } else { + Some(ObjectPatProp::KeyValue(prop)) + } + } + ObjectPatProp::Assign(prop) => { + if self.should_remove(prop.key.to_id()) { + self.mark_as_candidate(prop.value); + + None + } else { + Some(ObjectPatProp::Assign(prop)) + } + } + ObjectPatProp::Rest(prop) => { + if prop.arg.is_invalid() { + None + } else { + Some(ObjectPatProp::Rest(prop)) + } + } + }) + .collect(); + + if obj.props.is_empty() { + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + } + Pat::Rest(rest) => { + if rest.arg.is_invalid() { + return Pat::Invalid(Invalid { span: DUMMY_SP }); + } + } + _ => {} + } + } + + p + } + + fn fold_stmt(&mut self, mut s: Stmt) -> Stmt { + match s { + Stmt::Decl(Decl::Fn(f)) => { + if self.should_remove(f.ident.to_id()) { + self.mark_as_candidate(f.function); + return Stmt::Empty(EmptyStmt { span: DUMMY_SP }); + } + + s = Stmt::Decl(Decl::Fn(f)); + } + _ => {} + } + + let s = s.fold_children_with(self); + match s { + Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => { + return Stmt::Empty(EmptyStmt { span: DUMMY_SP }); + } + _ => {} + } + + s + } + + /// This method make `name` of [VarDeclarator] to [Pat::Invalid] if it + /// should be removed. + fn fold_var_declarator(&mut self, mut d: VarDeclarator) -> VarDeclarator { + let old = self.in_lhs_of_var; + self.in_lhs_of_var = true; + let name = d.name.fold_with(self); + + self.in_lhs_of_var = false; + if name.is_invalid() { + d.init = self.mark_as_candidate(d.init); + } + let init = d.init.fold_with(self); + self.in_lhs_of_var = old; + + VarDeclarator { name, init, ..d } + } + + fn fold_var_declarators(&mut self, mut decls: Vec) -> Vec { + decls = decls.fold_children_with(self); + decls.retain(|d| !d.name.is_invalid()); + + decls + } +} diff --git a/nextjs/packages/next/build/swc/src/transform.rs b/nextjs/packages/next/build/swc/src/transform.rs new file mode 100644 index 0000000000..87a1b8f618 --- /dev/null +++ b/nextjs/packages/next/build/swc/src/transform.rs @@ -0,0 +1,232 @@ +/* +Copyright (c) 2017 The swc Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +use crate::{ + amp_attributes::amp_attributes, + complete_output, get_compiler, + hook_optimizer::hook_optimizer, + next_dynamic::next_dynamic, + next_ssg::next_ssg, + util::{CtxtExt, MapErr}, +}; +use anyhow::{bail, Error}; +use napi::{CallContext, Env, JsBoolean, JsObject, JsString, Task}; +use std::sync::Arc; +use swc::config::{BuiltConfig, Options}; +use swc::{Compiler, TransformOutput}; +use swc_common::{chain, comments::Comment, BytePos, FileName, SourceFile}; +use swc_ecmascript::ast::Program; +use swc_ecmascript::transforms::helpers::{self, Helpers}; +use swc_ecmascript::utils::HANDLER; +use swc_ecmascript::visit::FoldWith; + +/// Input to transform +#[derive(Debug)] +pub enum Input { + /// json string + Program(String), + /// Raw source code. + Source(Arc), +} + +pub struct TransformTask { + pub c: Arc, + pub input: Input, + pub options: Options, +} + +impl Task for TransformTask { + type Output = TransformOutput; + type JsValue = JsObject; + + fn compute(&mut self) -> napi::Result { + self.c + .run(|| match self.input { + Input::Program(ref s) => { + let program: Program = + serde_json::from_str(&s).expect("failed to deserialize Program"); + // TODO: Source map + self.c.process_js(program, &self.options) + } + + Input::Source(ref s) => process_js_custom(&self.c, s.clone(), &self.options), + }) + .convert_err() + } + + fn resolve(self, env: Env, result: Self::Output) -> napi::Result { + complete_output(&env, result) + } +} + +/// returns `compiler, (src / path), options, plugin, callback` +pub fn schedule_transform(cx: CallContext, op: F) -> napi::Result +where + F: FnOnce(&Arc, String, bool, Options) -> TransformTask, +{ + let c = get_compiler(&cx); + + let s = cx.get::(0)?.into_utf8()?.as_str()?.to_owned(); + let is_module = cx.get::(1)?; + let options: Options = cx.get_deserialized(2)?; + + let task = op(&c, s, is_module.get_value()?, options); + + cx.env.spawn(task).map(|t| t.promise_object()) +} + +pub fn exec_transform(cx: CallContext, op: F) -> napi::Result +where + F: FnOnce(&Compiler, String, &Options) -> Result, Error>, +{ + let c = get_compiler(&cx); + + let s = cx.get::(0)?.into_utf8()?; + let is_module = cx.get::(1)?; + let options: Options = cx.get_deserialized(2)?; + + let output = c.run(|| -> napi::Result<_> { + if is_module.get_value()? { + let program: Program = + serde_json::from_str(s.as_str()?).expect("failed to deserialize Program"); + c.process_js(program, &options).convert_err() + } else { + let fm = op(&c, s.as_str()?.to_string(), &options).expect("failed to create fm"); + c.process_js_file(fm, &options).convert_err() + } + })?; + + complete_output(cx.env, output) +} + +#[js_function(4)] +pub fn transform(cx: CallContext) -> napi::Result { + schedule_transform(cx, |c, src, is_module, options| { + let input = if is_module { + Input::Program(src) + } else { + Input::Source(c.cm.new_source_file( + if options.filename.is_empty() { + FileName::Anon + } else { + FileName::Real(options.filename.clone().into()) + }, + src, + )) + }; + + TransformTask { + c: c.clone(), + input, + options, + } + }) +} + +#[js_function(4)] +pub fn transform_sync(cx: CallContext) -> napi::Result { + exec_transform(cx, |c, src, options| { + Ok(c.cm.new_source_file( + if options.filename.is_empty() { + FileName::Anon + } else { + FileName::Real(options.filename.clone().into()) + }, + src, + )) + }) +} + +fn process_js_custom( + compiler: &Arc, + source: Arc, + options: &Options, +) -> Result { + let config = compiler.run(|| compiler.config_for_file(options, &source.name))?; + let config = match config { + Some(v) => v, + None => { + bail!("cannot process file because it's ignored by .swcrc") + } + }; + let config = BuiltConfig { + pass: chain!( + hook_optimizer(), + next_ssg(), + amp_attributes(), + next_dynamic(source.name.clone()), + config.pass + ), + syntax: config.syntax, + target: config.target, + minify: config.minify, + external_helpers: config.external_helpers, + source_maps: config.source_maps, + input_source_map: config.input_source_map, + is_module: config.is_module, + output_path: config.output_path, + }; + //let orig = compiler.get_orig_src_map(&source, + // &options.config.input_source_map)?; + let program = compiler.parse_js( + source.clone(), + config.target, + config.syntax, + config.is_module, + true, + )?; + + //compiler.process_js_inner(program, orig.as_ref(), config) + + compiler.run(|| { + if config.minify { + let preserve_excl = |_: &BytePos, vc: &mut Vec| -> bool { + vc.retain(|c: &Comment| c.text.starts_with("!")); + !vc.is_empty() + }; + compiler.comments().leading.retain(preserve_excl); + compiler.comments().trailing.retain(preserve_excl); + } + let mut pass = config.pass; + let program = helpers::HELPERS.set(&Helpers::new(config.external_helpers), || { + HANDLER.set(&compiler.handler, || { + // Fold module + program.fold_with(&mut pass) + }) + }); + + compiler.print( + &program, + config.output_path, + config.target, + config.source_maps, + None, // TODO: figure out sourcemaps + config.minify, + ) + }) +} diff --git a/nextjs/packages/next/build/swc/src/util.rs b/nextjs/packages/next/build/swc/src/util.rs new file mode 100644 index 0000000000..38d95dba78 --- /dev/null +++ b/nextjs/packages/next/build/swc/src/util.rs @@ -0,0 +1,61 @@ +/* +Copyright (c) 2017 The swc Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +use anyhow::Context; +use napi::{CallContext, JsBuffer, Status}; +use serde::de::DeserializeOwned; + +pub trait MapErr: Into> { + fn convert_err(self) -> napi::Result { + self.into() + .map_err(|err| napi::Error::new(Status::GenericFailure, format!("{:?}", err))) + } +} + +impl MapErr for Result {} + +pub trait CtxtExt { + /// Currently this uses JsBuffer + fn get_deserialized(&self, index: usize) -> napi::Result + where + T: DeserializeOwned; +} + +impl CtxtExt for CallContext<'_> { + fn get_deserialized(&self, index: usize) -> napi::Result + where + T: DeserializeOwned, + { + let buffer = self.get::(index)?.into_value()?; + let v = serde_json::from_slice(&buffer) + .with_context(|| format!("Argument at `{}` is not JsBuffer", index)) + .convert_err()?; + + Ok(v) + } +} diff --git a/nextjs/packages/next/build/swc/tests/fixture.rs b/nextjs/packages/next/build/swc/tests/fixture.rs new file mode 100644 index 0000000000..fb30db8efc --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture.rs @@ -0,0 +1,76 @@ +use self::amp_attributes::amp_attributes; +use self::next_dynamic::next_dynamic; +use self::next_ssg::next_ssg; +use std::path::PathBuf; +use swc_common::{chain, comments::SingleThreadedComments, FileName}; +use swc_ecma_transforms_testing::{test, test_fixture}; +use swc_ecmascript::{ + parser::{EsConfig, Syntax}, + transforms::react::jsx, +}; +use testing::fixture; + +#[path = "../src/amp_attributes.rs"] +mod amp_attributes; +#[path = "../src/next_dynamic.rs"] +mod next_dynamic; +#[path = "../src/next_ssg.rs"] +mod next_ssg; + +fn syntax() -> Syntax { + Syntax::Es(EsConfig { + jsx: true, + dynamic_import: true, + ..Default::default() + }) +} + +#[fixture("tests/fixture/amp/**/input.js")] +fn amp_attributes_fixture(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture(syntax(), &|_tr| amp_attributes(), &input, &output); +} + +#[fixture("tests/fixture/next-dynamic/**/input.js")] +fn next_dynamic_fixture(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|_tr| { + next_dynamic(FileName::Real(PathBuf::from( + "/some-project/src/some-file.js", + ))) + }, + &input, + &output, + ); +} + +#[fixture("tests/fixture/ssg/**/input.js")] +fn next_ssg_fixture(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|tr| { + let jsx = jsx::( + tr.cm.clone(), + None, + swc_ecmascript::transforms::react::Options { + next: false, + runtime: None, + import_source: "".into(), + pragma: "__jsx".into(), + pragma_frag: "__jsxFrag".into(), + throw_if_namespace: false, + development: false, + use_builtins: true, + use_spread: true, + refresh: Default::default(), + }, + ); + chain!(next_ssg(), jsx) + }, + &input, + &output, + ); +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/amp/amp-classname/input.js b/nextjs/packages/next/build/swc/tests/fixture/amp/amp-classname/input.js new file mode 100644 index 0000000000..2cf35e282e --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/amp/amp-classname/input.js @@ -0,0 +1,2 @@ +import React from 'react' +const comp = () => diff --git a/nextjs/packages/next/build/swc/tests/fixture/amp/amp-classname/output.js b/nextjs/packages/next/build/swc/tests/fixture/amp/amp-classname/output.js new file mode 100644 index 0000000000..12d1197a0a --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/amp/amp-classname/output.js @@ -0,0 +1,2 @@ +import React from 'react' +const comp = () => diff --git a/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/input.js b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/input.js new file mode 100644 index 0000000000..617649e968 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/input.js @@ -0,0 +1,5 @@ +import dynamic1 from 'next/dynamic' +import dynamic2 from 'next/dynamic' + +const DynamicComponent1 = dynamic1(() => import('../components/hello1')) +const DynamicComponent2 = dynamic2(() => import('../components/hello2')) diff --git a/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/output.js b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/output.js new file mode 100644 index 0000000000..e281495c7f --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/duplicated-imports/output.js @@ -0,0 +1,14 @@ +import dynamic1 from 'next/dynamic' +import dynamic2 from 'next/dynamic' +const DynamicComponent1 = dynamic1(() => import('../components/hello1'), { + loadableGenerated: { + webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], + modules: ['/some-project/src/some-file.js -> ' + '../components/hello1'], + }, +}) +const DynamicComponent2 = dynamic2(() => import('../components/hello2'), { + loadableGenerated: { + webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], + modules: ['/some-project/src/some-file.js -> ' + '../components/hello2'], + }, +}) diff --git a/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/input.js b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/input.js new file mode 100644 index 0000000000..6b58972934 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/input.js @@ -0,0 +1,5 @@ +import dynamic from 'next/dynamic' +import somethingElse from 'something-else' + +const DynamicComponent = dynamic(() => import('../components/hello')) +somethingElse.dynamic('should not be transformed') diff --git a/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/output.js b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/output.js new file mode 100644 index 0000000000..484b4e9871 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/member-with-same-name/output.js @@ -0,0 +1,10 @@ +import dynamic from 'next/dynamic' +import somethingElse from 'something-else' + +const DynamicComponent = dynamic(() => import('../components/hello'), { + loadableGenerated: { + webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], + modules: ['/some-project/src/some-file.js -> ' + '../components/hello'], + }, +}) +somethingElse.dynamic('should not be transformed') diff --git a/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/no-options/input.js b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/no-options/input.js new file mode 100644 index 0000000000..fe844e0593 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/no-options/input.js @@ -0,0 +1,3 @@ +import dynamic from 'next/dynamic' + +const DynamicComponent = dynamic(() => import('../components/hello')) diff --git a/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/no-options/output.js b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/no-options/output.js new file mode 100644 index 0000000000..2426dbf7ad --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/no-options/output.js @@ -0,0 +1,8 @@ +import dynamic from 'next/dynamic' + +const DynamicComponent = dynamic(() => import('../components/hello'), { + loadableGenerated: { + webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], + modules: ['/some-project/src/some-file.js -> ' + '../components/hello'], + }, +}) diff --git a/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/with-options/input.js b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/with-options/input.js new file mode 100644 index 0000000000..91987b1357 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/with-options/input.js @@ -0,0 +1,6 @@ +import dynamic from 'next/dynamic' + +const DynamicComponentWithCustomLoading = dynamic( + () => import('../components/hello'), + { loading: () =>

    ...

    } +) diff --git a/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/with-options/output.js b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/with-options/output.js new file mode 100644 index 0000000000..458a440448 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/next-dynamic/with-options/output.js @@ -0,0 +1,13 @@ +import dynamic from 'next/dynamic' + +const DynamicComponentWithCustomLoading = dynamic( + () => import('../components/hello'), + { loading: () =>

    ...

    }, + { + loadableGenerated: { + webpack: () => [require.resolveWeak('/some-project/src/some-file.js')], + modules: ['/some-project/src/some-file.js -> ' + '../components/hello'], + }, + loading: () =>

    ...

    , + } +) diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-array/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-array/input.js new file mode 100644 index 0000000000..9b1b647d0c --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-array/input.js @@ -0,0 +1,15 @@ +import fs from 'fs' +import other from 'other' + +const [a, b, ...rest] = fs.promises +const [foo, bar] = other + +export async function getStaticProps() { + a + b + rest + bar +} +export default function Home() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-array/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-array/output.js new file mode 100644 index 0000000000..bd2fa7e2ca --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-array/output.js @@ -0,0 +1,6 @@ +import other from 'other' +const [foo] = other +export var __N_SSG = true +export default function Home() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-object/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-object/input.js new file mode 100644 index 0000000000..1a6933ab69 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-object/input.js @@ -0,0 +1,17 @@ +import fs from 'fs' +import other from 'other' + +const { readFile, readdir, access: foo } = fs.promises +const { a, b, cat: bar, ...rem } = other + +export async function getStaticProps() { + readFile + readdir + foo + b + cat + rem +} +export default function Home() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-object/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-object/output.js new file mode 100644 index 0000000000..d9d0086bae --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/destructuring-assignment-object/output.js @@ -0,0 +1,6 @@ +import other from 'other' +const { a, cat: bar } = other +export var __N_SSG = true +export default function Home() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-crash-for-class-declarations/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-crash-for-class-declarations/input.js new file mode 100644 index 0000000000..293a7a81b1 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-crash-for-class-declarations/input.js @@ -0,0 +1,11 @@ +function getStaticPaths() { + return [] +} + +export { getStaticPaths } + +export class MyClass {} + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-crash-for-class-declarations/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-crash-for-class-declarations/output.js new file mode 100644 index 0000000000..3a6bd07759 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-crash-for-class-declarations/output.js @@ -0,0 +1,5 @@ +export var __N_SSG = true +export class MyClass {} +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-mix-up-bindings/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-mix-up-bindings/input.js new file mode 100644 index 0000000000..49e953e9dd --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-mix-up-bindings/input.js @@ -0,0 +1,14 @@ +function Function1() { + return { + a: function bug(a) { + return 2 + }, + } +} + +function Function2() { + var bug = 1 + return { bug } +} + +export { getStaticProps } from 'a' diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-mix-up-bindings/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-mix-up-bindings/output.js new file mode 100644 index 0000000000..a5115b8a6a --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-mix-up-bindings/output.js @@ -0,0 +1,11 @@ +function Function1() { + return { + a: function bug(a) { + return 2 + }, + } +} +function Function2() { + var bug = 1 + return { bug } +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-function-declarations/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-function-declarations/input.js new file mode 100644 index 0000000000..dd99734ab3 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-function-declarations/input.js @@ -0,0 +1,9 @@ +export function getStaticProps() { + return { props: {} } +} + +export function Noop() {} + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-function-declarations/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-function-declarations/output.js new file mode 100644 index 0000000000..75349dd296 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-function-declarations/output.js @@ -0,0 +1,5 @@ +export var __N_SSG = true +export function Noop() {} +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-variable-declarations/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-variable-declarations/input.js new file mode 100644 index 0000000000..45aaf6cdbd --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-variable-declarations/input.js @@ -0,0 +1,12 @@ +export const getStaticPaths = () => { + return [] + }, + foo = 2 + +export const getStaticProps = function () { + return { props: {} } +} + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-variable-declarations/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-variable-declarations/output.js new file mode 100644 index 0000000000..582269c79a --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-not-remove-extra-named-export-variable-declarations/output.js @@ -0,0 +1,5 @@ +export var __N_SSG = true +export const foo = 2 +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-combined-named-export-specifiers/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-combined-named-export-specifiers/input.js new file mode 100644 index 0000000000..1674051592 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-combined-named-export-specifiers/input.js @@ -0,0 +1,5 @@ +export { getStaticPaths, a as getStaticProps } from '.' + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-combined-named-export-specifiers/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-combined-named-export-specifiers/output.js new file mode 100644 index 0000000000..495a40db45 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-combined-named-export-specifiers/output.js @@ -0,0 +1,4 @@ +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-extra-named-export-speicifers/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-extra-named-export-speicifers/input.js new file mode 100644 index 0000000000..4ec4b63095 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-extra-named-export-speicifers/input.js @@ -0,0 +1,5 @@ +export { getStaticPaths, a as getStaticProps, foo, bar as baz } from '.' + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-extra-named-export-speicifers/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-extra-named-export-speicifers/output.js new file mode 100644 index 0000000000..442a2f2ff0 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-extra-named-export-speicifers/output.js @@ -0,0 +1,5 @@ +export var __N_SSG = true +export { foo, bar as baz } from '.' +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations-async/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations-async/input.js new file mode 100644 index 0000000000..d37b62e153 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations-async/input.js @@ -0,0 +1,11 @@ +export async function getStaticPaths() { + return [] +} + +export async function getStaticProps() { + return { props: {} } +} + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations-async/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations-async/output.js new file mode 100644 index 0000000000..495a40db45 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations-async/output.js @@ -0,0 +1,4 @@ +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations/input.js new file mode 100644 index 0000000000..a47f70f069 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations/input.js @@ -0,0 +1,11 @@ +export function getStaticPaths() { + return [] +} + +export function getStaticProps() { + return { props: {} } +} + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations/output.js new file mode 100644 index 0000000000..495a40db45 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-function-declarations/output.js @@ -0,0 +1,4 @@ +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations-async/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations-async/input.js new file mode 100644 index 0000000000..69d1157ad3 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations-async/input.js @@ -0,0 +1,11 @@ +export const getStaticPaths = async () => { + return [] +} + +export const getStaticProps = async function () { + return { props: {} } +} + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations-async/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations-async/output.js new file mode 100644 index 0000000000..495a40db45 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations-async/output.js @@ -0,0 +1,4 @@ +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations/input.js new file mode 100644 index 0000000000..45d80f6aee --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations/input.js @@ -0,0 +1,11 @@ +export const getStaticPaths = () => { + return [] +} + +export const getStaticProps = function () { + return { props: {} } +} + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations/output.js new file mode 100644 index 0000000000..495a40db45 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-named-export-variable-declarations/output.js @@ -0,0 +1,4 @@ +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations-dependents-variables-functions-imports/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations-dependents-variables-functions-imports/input.js new file mode 100644 index 0000000000..e8ece29f29 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations-dependents-variables-functions-imports/input.js @@ -0,0 +1,46 @@ +import keep_me from 'hello' +import { keep_me2 } from 'hello2' +import * as keep_me3 from 'hello3' + +import drop_me from 'bla' +import { drop_me2 } from 'foo' +import { drop_me3, but_not_me } from 'bar' +import * as remove_mua from 'hehe' + +var leave_me_alone = 1 +function dont_bug_me_either() {} + +const inceptionVar = 'hahaa' +var var1 = 1 +let var2 = 2 +const var3 = inceptionVar + remove_mua + +function inception1() { + var2 + drop_me2 +} + +function abc() {} +const b = function () { + var3 + drop_me3 +} +const b2 = function apples() {} +const bla = () => { + inception1 +} + +function getStaticProps() { + abc() + drop_me + b + b2 + bla() + return { props: { var1 } } +} + +export { getStaticProps } + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations-dependents-variables-functions-imports/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations-dependents-variables-functions-imports/output.js new file mode 100644 index 0000000000..6a9308b537 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations-dependents-variables-functions-imports/output.js @@ -0,0 +1,10 @@ +import keep_me from 'hello' +import { keep_me2 } from 'hello2' +import * as keep_me3 from 'hello3' +import { but_not_me } from 'bar' +var leave_me_alone = 1 +function dont_bug_me_either() {} +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations/input.js new file mode 100644 index 0000000000..b03c0f897a --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations/input.js @@ -0,0 +1,9 @@ +function getStaticPaths() { + return [] +} + +export { getStaticPaths } + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations/output.js new file mode 100644 index 0000000000..495a40db45 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-function-declarations/output.js @@ -0,0 +1,4 @@ +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations-safe/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations-safe/input.js new file mode 100644 index 0000000000..04cc2a3bb8 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations-safe/input.js @@ -0,0 +1,10 @@ +const getStaticPaths = () => { + return [] + }, + a = 2 + +export { getStaticPaths } + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations-safe/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations-safe/output.js new file mode 100644 index 0000000000..007e0fd05c --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations-safe/output.js @@ -0,0 +1,5 @@ +const a = 2 +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations/input.js new file mode 100644 index 0000000000..746c042098 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations/input.js @@ -0,0 +1,9 @@ +const getStaticPaths = () => { + return [] +} + +export { getStaticPaths } + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations/output.js new file mode 100644 index 0000000000..495a40db45 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-re-exported-variable-declarations/output.js @@ -0,0 +1,4 @@ +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-separate-named-export-specifiers/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-separate-named-export-specifiers/input.js new file mode 100644 index 0000000000..0472dc9405 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-separate-named-export-specifiers/input.js @@ -0,0 +1,6 @@ +export { getStaticPaths } from './input' +export { a as getStaticProps } from './input' + +export default function Test() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-separate-named-export-specifiers/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-separate-named-export-specifiers/output.js new file mode 100644 index 0000000000..495a40db45 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-remove-separate-named-export-specifiers/output.js @@ -0,0 +1,4 @@ +export var __N_SSG = true +export default function Test() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-babel-style-memoized-function/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-babel-style-memoized-function/input.js new file mode 100644 index 0000000000..cbda4b6977 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-babel-style-memoized-function/input.js @@ -0,0 +1,10 @@ +function fn() { + fn = function () {} + return fn.apply(this, arguments) +} +export function getStaticProps() { + fn +} +export default function Home() { + return
    +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-babel-style-memoized-function/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-babel-style-memoized-function/output.js new file mode 100644 index 0000000000..2f0d20d71d --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-babel-style-memoized-function/output.js @@ -0,0 +1,4 @@ +export var __N_SSG = true +export default function Home() { + return __jsx('div', null) +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports-2/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports-2/input.js new file mode 100644 index 0000000000..cf15ecb2df --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports-2/input.js @@ -0,0 +1,11 @@ +export function getStaticProps() { + return { props: {} } +} + +class Test extends React.Component { + render() { + return
    + } +} + +export default Test diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports-2/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports-2/output.js new file mode 100644 index 0000000000..ff86dad98b --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports-2/output.js @@ -0,0 +1,7 @@ +class Test extends React.Component { + render() { + return __jsx('div', null) + } +} +export var __N_SSG = true +export default Test diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports/input.js new file mode 100644 index 0000000000..13746792d6 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports/input.js @@ -0,0 +1,9 @@ +export function getStaticProps() { + return { props: {} } +} + +export default class Test extends React.Component { + render() { + return
    + } +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports/output.js new file mode 100644 index 0000000000..c3cf72d42b --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-class-exports/output.js @@ -0,0 +1,6 @@ +export var __N_SSG = true +export default class Test extends React.Component { + render() { + return __jsx('div', null) + } +} diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-a-class/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-a-class/input.js new file mode 100644 index 0000000000..0c57a588b0 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-a-class/input.js @@ -0,0 +1,13 @@ +export function getStaticProps() { + return { props: {} } +} + +class El extends React.Component { + render() { + return
    + } +} + +const a = 5 + +export { El as default, a } diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-a-class/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-a-class/output.js new file mode 100644 index 0000000000..9bae7762b3 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-a-class/output.js @@ -0,0 +1,8 @@ +class El extends React.Component { + render() { + return __jsx('div', null) + } +} +const a = 5 +export var __N_SSG = true +export { El as default, a } diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-other-specifiers/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-other-specifiers/input.js new file mode 100644 index 0000000000..d58b2fbdb9 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-other-specifiers/input.js @@ -0,0 +1,11 @@ +export function getStaticProps() { + return { props: {} } +} + +function El() { + return
    +} + +const a = 5 + +export { El as default, a } diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-other-specifiers/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-other-specifiers/output.js new file mode 100644 index 0000000000..97da95c038 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-export-named-as-default-with-other-specifiers/output.js @@ -0,0 +1,6 @@ +function El() { + return __jsx('div', null) +} +const a = 5 +export var __N_SSG = true +export { El as default, a } diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-full-re-export/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-full-re-export/input.js new file mode 100644 index 0000000000..5c0bdae355 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-full-re-export/input.js @@ -0,0 +1 @@ +export { getStaticProps, default } from 'a' diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-full-re-export/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-full-re-export/output.js new file mode 100644 index 0000000000..c1a9ba77d3 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-full-re-export/output.js @@ -0,0 +1,2 @@ +export var __N_SSG = true +export { default } from 'a' diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-named-export-as-default/input.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-named-export-as-default/input.js new file mode 100644 index 0000000000..a0bfe04b87 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-named-export-as-default/input.js @@ -0,0 +1,9 @@ +export function getStaticProps() { + return { props: {} } +} + +function El() { + return
    +} + +export { El as default } diff --git a/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-named-export-as-default/output.js b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-named-export-as-default/output.js new file mode 100644 index 0000000000..0e5f256d73 --- /dev/null +++ b/nextjs/packages/next/build/swc/tests/fixture/ssg/getStaticProps/should-support-named-export-as-default/output.js @@ -0,0 +1,5 @@ +function El() { + return __jsx('div', null) +} +export var __N_SSG = true +export { El as default } diff --git a/nextjs/packages/next/build/utils.ts b/nextjs/packages/next/build/utils.ts new file mode 100644 index 0000000000..e0a8b47b02 --- /dev/null +++ b/nextjs/packages/next/build/utils.ts @@ -0,0 +1,1115 @@ +import '../server/node-polyfill-fetch' +import chalk from 'chalk' +import getGzipSize from 'next/dist/compiled/gzip-size' +import textTable from 'next/dist/compiled/text-table' +import path from 'path' +import { promises as fs } from 'fs' +import { isValidElementType } from 'react-is' +import stripAnsi from 'next/dist/compiled/strip-ansi' +import { + Redirect, + Rewrite, + Header, + CustomRoutes, +} from '../lib/load-custom-routes' +import { + SSG_GET_INITIAL_PROPS_CONFLICT, + SERVER_PROPS_GET_INIT_PROPS_CONFLICT, + SERVER_PROPS_SSG_CONFLICT, +} from '../lib/constants' +import prettyBytes from '../lib/pretty-bytes' +import { recursiveFindPages } from '../lib/recursive-readdir' +import { getRouteMatcher, getRouteRegex } from '../shared/lib/router/utils' +import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic' +import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters' +import { findPageFile } from '../server/lib/find-page-file' +import { GetStaticPaths } from 'next/types' +import { denormalizePagePath } from '../server/normalize-page-path' +import { BuildManifest } from '../server/get-page-files' +import { removePathTrailingSlash } from '../client/normalize-trailing-slash' +import { UnwrapPromise } from '../lib/coalesced-function' +import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' +import * as Log from './output/log' +import { loadComponents } from '../server/load-components' +import { trace } from '../telemetry/trace' +import { setHttpAgentOptions } from '../server/config' +import { NextConfigComplete } from '../server/config-shared' + +const fileGzipStats: { [k: string]: Promise | undefined } = {} +const fsStatGzip = (file: string) => { + const cached = fileGzipStats[file] + if (cached) return cached + return (fileGzipStats[file] = getGzipSize.file(file)) +} + +const fileSize = async (file: string) => (await fs.stat(file)).size + +const fileStats: { [k: string]: Promise | undefined } = {} +const fsStat = (file: string) => { + const cached = fileStats[file] + if (cached) return cached + return (fileStats[file] = fileSize(file)) +} + +export const topLevelFoldersThatMayContainPages = [ + 'pages', + 'src', + 'app', + 'integrations', +] + +export function convertPageFilePathToRoutePath( + filePath: string, + pageExtensions: string[] +) { + return filePath + .replace(/^.*?[\\/]pages[\\/]/, '/') + .replace(/^.*?[\\/]api[\\/]/, '/api/') + .replace(/^.*?[\\/]queries[\\/]/, '/api/rpc/') + .replace(/^.*?[\\/]mutations[\\/]/, '/api/rpc/') + .replace(new RegExp(`\\.+(${pageExtensions.join('|')})$`), '') +} + +const fileExtensionRegex = /\.([a-z]+)$/ + +export function convertPageFilePathToResolverName(filePathFromAppRoot: string) { + return filePathFromAppRoot + .replace(/^.*[\\/](queries|mutations)[\\/]/, '') + .replace(fileExtensionRegex, '') +} + +export function convertPageFilePathToResolverType(filePathFromAppRoot: string) { + return filePathFromAppRoot.match(/[\\/]queries[\\/]/) ? 'query' : 'mutation' +} + +export function getIsPageFile(filePathFromAppRoot: string) { + return ( + /[\\/]pages[\\/]/.test(filePathFromAppRoot) || + /[\\/]api[\\/]/.test(filePathFromAppRoot) || + getIsRpcFile(filePathFromAppRoot) + ) +} + +export function getIsRpcFile(filePathFromAppRoot: string) { + return ( + /[\\/]queries[\\/]/.test(filePathFromAppRoot) || + /[\\/]mutations[\\/]/.test(filePathFromAppRoot) + ) +} + +export function buildPageExtensionRegex(pageExtensions: string[]) { + return new RegExp(`(? { + return recursiveFindPages(directory, buildPageExtensionRegex(pageExtensions)) +} + +export interface PageInfo { + isHybridAmp?: boolean + size: number + totalSize: number + static: boolean + isSsg: boolean + ssgPageRoutes: string[] | null + initialRevalidateSeconds: number | false + pageDuration: number | undefined + ssgPageDurations: number[] | undefined +} + +export async function printTreeView( + list: readonly string[], + pageInfos: Map, + serverless: boolean, + { + distPath, + buildId, + pagesDir, + pageExtensions, + buildManifest, + useStatic404, + gzipSize = true, + }: { + distPath: string + buildId: string + pagesDir: string + pageExtensions: string[] + buildManifest: BuildManifest + useStatic404: boolean + gzipSize?: boolean + } +) { + const getPrettySize = (_size: number): string => { + const size = prettyBytes(_size) + // green for 0-130kb + if (_size < 130 * 1000) return chalk.green(size) + // yellow for 130-170kb + if (_size < 170 * 1000) return chalk.yellow(size) + // red for >= 170kb + return chalk.red.bold(size) + } + + const MIN_DURATION = 300 + const getPrettyDuration = (_duration: number): string => { + const duration = `${_duration} ms` + // green for 300-1000ms + if (_duration < 1000) return chalk.green(duration) + // yellow for 1000-2000ms + if (_duration < 2000) return chalk.yellow(duration) + // red for >= 2000ms + return chalk.red.bold(duration) + } + + const getCleanName = (fileName: string) => + fileName + // Trim off `static/` + .replace(/^static\//, '') + // Re-add `static/` for root files + .replace(/^/, 'static') + // Remove file hash + .replace(/(?:^|[.-])([0-9a-z]{6})[0-9a-z]{14}(?=\.)/, '.$1') + + const messages: [string, string, string][] = [ + ['Page', 'Size', 'First Load JS'].map((entry) => + chalk.underline(entry) + ) as [string, string, string], + ] + + const hasCustomApp = await findPageFile(pagesDir, '/_app', pageExtensions) + + pageInfos.set('/404', { + ...(pageInfos.get('/404') || pageInfos.get('/_error')), + static: useStatic404, + } as any) + + if (!list.includes('/404')) { + list = [...list, '/404'] + } + + const sizeData = await computeFromManifest( + buildManifest, + distPath, + gzipSize, + pageInfos + ) + + const pageList = list + .slice() + .filter( + (e) => + !( + e === '/_document' || + e === '/_error' || + (!hasCustomApp && e === '/_app') + ) + ) + .sort((a, b) => a.localeCompare(b)) + + pageList.forEach((item, i, arr) => { + const symbol = + i === 0 + ? arr.length === 1 + ? '─' + : '┌' + : i === arr.length - 1 + ? '└' + : '├' + + const pageInfo = pageInfos.get(item) + const ampFirst = buildManifest.ampFirstPages.includes(item) + + const totalDuration = + (pageInfo?.pageDuration || 0) + + (pageInfo?.ssgPageDurations?.reduce((a, b) => a + (b || 0), 0) || 0) + + messages.push([ + `${symbol} ${ + item === '/_app' + ? ' ' + : pageInfo?.static + ? '○' + : pageInfo?.isSsg + ? '●' + : 'λ' + } ${ + pageInfo?.initialRevalidateSeconds + ? `${item} (ISR: ${pageInfo?.initialRevalidateSeconds} Seconds)` + : item + }${ + totalDuration > MIN_DURATION + ? ` (${getPrettyDuration(totalDuration)})` + : '' + }`, + pageInfo + ? ampFirst + ? chalk.cyan('AMP') + : pageInfo.size >= 0 + ? prettyBytes(pageInfo.size) + : '' + : '', + pageInfo + ? ampFirst + ? chalk.cyan('AMP') + : pageInfo.size >= 0 + ? getPrettySize(pageInfo.totalSize) + : '' + : '', + ]) + + const uniqueCssFiles = + buildManifest.pages[item]?.filter( + (file) => file.endsWith('.css') && sizeData.uniqueFiles.includes(file) + ) || [] + + if (uniqueCssFiles.length > 0) { + const contSymbol = i === arr.length - 1 ? ' ' : '├' + + uniqueCssFiles.forEach((file, index, { length }) => { + const innerSymbol = index === length - 1 ? '└' : '├' + messages.push([ + `${contSymbol} ${innerSymbol} ${getCleanName(file)}`, + prettyBytes(sizeData.sizeUniqueFiles[file]), + '', + ]) + }) + } + + if (pageInfo?.ssgPageRoutes?.length) { + const totalRoutes = pageInfo.ssgPageRoutes.length + const contSymbol = i === arr.length - 1 ? ' ' : '├' + + let routes: { route: string; duration: number; avgDuration?: number }[] + if ( + pageInfo.ssgPageDurations && + pageInfo.ssgPageDurations.some((d) => d > MIN_DURATION) + ) { + const previewPages = totalRoutes === 8 ? 8 : Math.min(totalRoutes, 7) + const routesWithDuration = pageInfo.ssgPageRoutes + .map((route, idx) => ({ + route, + duration: pageInfo.ssgPageDurations![idx] || 0, + })) + .sort(({ duration: a }, { duration: b }) => + // Sort by duration + // keep too small durations in original order at the end + a <= MIN_DURATION && b <= MIN_DURATION ? 0 : b - a + ) + routes = routesWithDuration.slice(0, previewPages) + const remainingRoutes = routesWithDuration.slice(previewPages) + if (remainingRoutes.length) { + const remaining = remainingRoutes.length + const avgDuration = Math.round( + remainingRoutes.reduce( + (total, { duration }) => total + duration, + 0 + ) / remainingRoutes.length + ) + routes.push({ + route: `[+${remaining} more paths]`, + duration: 0, + avgDuration, + }) + } + } else { + const previewPages = totalRoutes === 4 ? 4 : Math.min(totalRoutes, 3) + routes = pageInfo.ssgPageRoutes + .slice(0, previewPages) + .map((route) => ({ route, duration: 0 })) + if (totalRoutes > previewPages) { + const remaining = totalRoutes - previewPages + routes.push({ route: `[+${remaining} more paths]`, duration: 0 }) + } + } + + routes.forEach(({ route, duration, avgDuration }, index, { length }) => { + const innerSymbol = index === length - 1 ? '└' : '├' + messages.push([ + `${contSymbol} ${innerSymbol} ${route}${ + duration > MIN_DURATION ? ` (${getPrettyDuration(duration)})` : '' + }${ + avgDuration && avgDuration > MIN_DURATION + ? ` (avg ${getPrettyDuration(avgDuration)})` + : '' + }`, + '', + '', + ]) + }) + } + }) + + const sharedFilesSize = sizeData.sizeCommonFiles + const sharedFiles = sizeData.sizeCommonFile + + messages.push([ + '+ First Load JS shared by all', + getPrettySize(sharedFilesSize), + '', + ]) + const sharedFileKeys = Object.keys(sharedFiles) + const sharedCssFiles: string[] = [] + ;[ + ...sharedFileKeys + .filter((file) => { + if (file.endsWith('.css')) { + sharedCssFiles.push(file) + return false + } + return true + }) + .map((e) => e.replace(buildId, '')) + .sort(), + ...sharedCssFiles.map((e) => e.replace(buildId, '')).sort(), + ].forEach((fileName, index, { length }) => { + const innerSymbol = index === length - 1 ? '└' : '├' + + const originalName = fileName.replace('', buildId) + const cleanName = getCleanName(fileName) + + messages.push([ + ` ${innerSymbol} ${cleanName}`, + prettyBytes(sharedFiles[originalName]), + '', + ]) + }) + + console.log( + textTable(messages, { + align: ['l', 'l', 'r'], + stringLength: (str) => stripAnsi(str).length, + }) + ) + + console.log() + console.log( + textTable( + [ + [ + 'λ', + serverless ? '(Lambda)' : '(Server)', + `server-side renders at runtime (uses ${chalk.cyan( + 'getInitialProps' + )} or ${chalk.cyan('getServerSideProps')})`, + ], + [ + '○', + '(Static)', + 'automatically rendered as static HTML (uses no initial props)', + ], + [ + '●', + '(SSG)', + `automatically generated as static HTML + JSON (uses ${chalk.cyan( + 'getStaticProps' + )})`, + ], + [ + '', + '(ISR)', + `incremental static regeneration (uses revalidate in ${chalk.cyan( + 'getStaticProps' + )})`, + ], + ] as [string, string, string][], + { + align: ['l', 'l', 'l'], + stringLength: (str) => stripAnsi(str).length, + } + ) + ) + + console.log() +} + +export function printCustomRoutes({ + redirects, + rewrites, + headers, +}: CustomRoutes) { + const printRoutes = ( + routes: Redirect[] | Rewrite[] | Header[], + type: 'Redirects' | 'Rewrites' | 'Headers' + ) => { + const isRedirects = type === 'Redirects' + const isHeaders = type === 'Headers' + console.log(chalk.underline(type)) + console.log() + + /* + ┌ source + ├ permanent/statusCode + └ destination + */ + const routesStr = (routes as any[]) + .map((route: { source: string }) => { + let routeStr = `┌ source: ${route.source}\n` + + if (!isHeaders) { + const r = route as Rewrite + routeStr += `${isRedirects ? '├' : '└'} destination: ${ + r.destination + }\n` + } + if (isRedirects) { + const r = route as Redirect + routeStr += `└ ${ + r.statusCode + ? `status: ${r.statusCode}` + : `permanent: ${r.permanent}` + }\n` + } + + if (isHeaders) { + const r = route as Header + routeStr += `└ headers:\n` + + for (let i = 0; i < r.headers.length; i++) { + const header = r.headers[i] + const last = i === headers.length - 1 + + routeStr += ` ${last ? '└' : '├'} ${header.key}: ${header.value}\n` + } + } + + return routeStr + }) + .join('\n') + + console.log(routesStr, '\n') + } + + if (redirects.length) { + printRoutes(redirects, 'Redirects') + } + if (headers.length) { + printRoutes(headers, 'Headers') + } + + const combinedRewrites = [ + ...rewrites.beforeFiles, + ...rewrites.afterFiles, + ...rewrites.fallback, + ] + if (combinedRewrites.length) { + printRoutes(combinedRewrites, 'Rewrites') + } +} + +type ComputeManifestShape = { + commonFiles: string[] + uniqueFiles: string[] + sizeUniqueFiles: { [file: string]: number } + sizeCommonFile: { [file: string]: number } + sizeCommonFiles: number +} + +let cachedBuildManifest: BuildManifest | undefined + +let lastCompute: ComputeManifestShape | undefined +let lastComputePageInfo: boolean | undefined + +export async function computeFromManifest( + manifest: BuildManifest, + distPath: string, + gzipSize: boolean = true, + pageInfos?: Map +): Promise { + if ( + Object.is(cachedBuildManifest, manifest) && + lastComputePageInfo === !!pageInfos + ) { + return lastCompute! + } + + let expected = 0 + const files = new Map() + Object.keys(manifest.pages).forEach((key) => { + if (pageInfos) { + const pageInfo = pageInfos.get(key) + // don't include AMP pages since they don't rely on shared bundles + // AMP First pages are not under the pageInfos key + if (pageInfo?.isHybridAmp) { + return + } + } + + ++expected + manifest.pages[key].forEach((file) => { + if (key === '/_app') { + files.set(file, Infinity) + } else if (files.has(file)) { + files.set(file, files.get(file)! + 1) + } else { + files.set(file, 1) + } + }) + }) + + const getSize = gzipSize ? fsStatGzip : fsStat + + const commonFiles = [...files.entries()] + .filter(([, len]) => len === expected || len === Infinity) + .map(([f]) => f) + const uniqueFiles = [...files.entries()] + .filter(([, len]) => len === 1) + .map(([f]) => f) + + let stats: [string, number][] + try { + stats = await Promise.all( + commonFiles.map( + async (f) => + [f, await getSize(path.join(distPath, f))] as [string, number] + ) + ) + } catch (_) { + stats = [] + } + + let uniqueStats: [string, number][] + try { + uniqueStats = await Promise.all( + uniqueFiles.map( + async (f) => + [f, await getSize(path.join(distPath, f))] as [string, number] + ) + ) + } catch (_) { + uniqueStats = [] + } + + lastCompute = { + commonFiles, + uniqueFiles, + sizeUniqueFiles: uniqueStats.reduce( + (obj, n) => Object.assign(obj, { [n[0]]: n[1] }), + {} + ), + sizeCommonFile: stats.reduce( + (obj, n) => Object.assign(obj, { [n[0]]: n[1] }), + {} + ), + sizeCommonFiles: stats.reduce((size, [f, stat]) => { + if (f.endsWith('.css')) return size + return size + stat + }, 0), + } + + cachedBuildManifest = manifest + lastComputePageInfo = !!pageInfos + return lastCompute! +} + +export function difference(main: T[] | Set, sub: T[] | Set): T[] { + const a = new Set(main) + const b = new Set(sub) + return [...a].filter((x) => !b.has(x)) +} + +function intersect(main: T[], sub: T[]): T[] { + const a = new Set(main) + const b = new Set(sub) + return [...new Set([...a].filter((x) => b.has(x)))] +} + +function sum(a: number[]): number { + return a.reduce((size, stat) => size + stat, 0) +} + +export async function getJsPageSizeInKb( + page: string, + distPath: string, + buildManifest: BuildManifest, + gzipSize: boolean = true, + computedManifestData?: ComputeManifestShape +): Promise<[number, number]> { + const data = + computedManifestData || + (await computeFromManifest(buildManifest, distPath, gzipSize)) + + const fnFilterJs = (entry: string) => entry.endsWith('.js') + + const pageFiles = ( + buildManifest.pages[denormalizePagePath(page)] || [] + ).filter(fnFilterJs) + const appFiles = (buildManifest.pages['/_app'] || []).filter(fnFilterJs) + + const fnMapRealPath = (dep: string) => `${distPath}/${dep}` + + const allFilesReal = [...new Set([...pageFiles, ...appFiles])].map( + fnMapRealPath + ) + const selfFilesReal = difference( + intersect(pageFiles, data.uniqueFiles), + data.commonFiles + ).map(fnMapRealPath) + + const getSize = gzipSize ? fsStatGzip : fsStat + + try { + // Doesn't use `Promise.all`, as we'd double compute duplicate files. This + // function is memoized, so the second one will instantly resolve. + const allFilesSize = sum(await Promise.all(allFilesReal.map(getSize))) + const selfFilesSize = sum(await Promise.all(selfFilesReal.map(getSize))) + + return [selfFilesSize, allFilesSize] + } catch (_) {} + return [-1, -1] +} + +export async function buildStaticPaths( + page: string, + getStaticPaths: GetStaticPaths, + locales?: string[], + defaultLocale?: string +): Promise< + Omit>, 'paths'> & { + paths: string[] + encodedPaths: string[] + } +> { + const prerenderPaths = new Set() + const encodedPrerenderPaths = new Set() + const _routeRegex = getRouteRegex(page) + const _routeMatcher = getRouteMatcher(_routeRegex) + + // Get the default list of allowed params. + const _validParamKeys = Object.keys(_routeMatcher(page)) + + const staticPathsResult = await getStaticPaths({ locales, defaultLocale }) + + const expectedReturnVal = + `Expected: { paths: [], fallback: boolean }\n` + + `See here for more info: https://nextjs.org/docs/messages/invalid-getstaticpaths-value` + + if ( + !staticPathsResult || + typeof staticPathsResult !== 'object' || + Array.isArray(staticPathsResult) + ) { + throw new Error( + `Invalid value returned from getStaticPaths in ${page}. Received ${typeof staticPathsResult} ${expectedReturnVal}` + ) + } + + const invalidStaticPathKeys = Object.keys(staticPathsResult).filter( + (key) => !(key === 'paths' || key === 'fallback') + ) + + if (invalidStaticPathKeys.length > 0) { + throw new Error( + `Extra keys returned from getStaticPaths in ${page} (${invalidStaticPathKeys.join( + ', ' + )}) ${expectedReturnVal}` + ) + } + + if ( + !( + typeof staticPathsResult.fallback === 'boolean' || + staticPathsResult.fallback === 'blocking' + ) + ) { + throw new Error( + `The \`fallback\` key must be returned from getStaticPaths in ${page}.\n` + + expectedReturnVal + ) + } + + const toPrerender = staticPathsResult.paths + + if (!Array.isArray(toPrerender)) { + throw new Error( + `Invalid \`paths\` value returned from getStaticPaths in ${page}.\n` + + `\`paths\` must be an array of strings or objects of shape { params: [key: string]: string }` + ) + } + + toPrerender.forEach((entry) => { + // For a string-provided path, we must make sure it matches the dynamic + // route. + if (typeof entry === 'string') { + entry = removePathTrailingSlash(entry) + + const localePathResult = normalizeLocalePath(entry, locales) + let cleanedEntry = entry + + if (localePathResult.detectedLocale) { + cleanedEntry = entry.substr(localePathResult.detectedLocale.length + 1) + } else if (defaultLocale) { + entry = `/${defaultLocale}${entry}` + } + + const result = _routeMatcher(cleanedEntry) + if (!result) { + throw new Error( + `The provided path \`${cleanedEntry}\` does not match the page: \`${page}\`.` + ) + } + + // If leveraging the string paths variant the entry should already be + // encoded so we decode the segments ensuring we only escape path + // delimiters + prerenderPaths.add( + entry + .split('/') + .map((segment) => + escapePathDelimiters(decodeURIComponent(segment), true) + ) + .join('/') + ) + encodedPrerenderPaths.add(entry) + } + // For the object-provided path, we must make sure it specifies all + // required keys. + else { + const invalidKeys = Object.keys(entry).filter( + (key) => key !== 'params' && key !== 'locale' + ) + + if (invalidKeys.length) { + throw new Error( + `Additional keys were returned from \`getStaticPaths\` in page "${page}". ` + + `URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` + + `\n\n\treturn { params: { ${_validParamKeys + .map((k) => `${k}: ...`) + .join(', ')} } }` + + `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n` + ) + } + + const { params = {} } = entry + let builtPage = page + let encodedBuiltPage = page + + _validParamKeys.forEach((validParamKey) => { + const { repeat, optional } = _routeRegex.groups[validParamKey] + let paramValue = params[validParamKey] + if ( + optional && + params.hasOwnProperty(validParamKey) && + (paramValue === null || + paramValue === undefined || + (paramValue as any) === false) + ) { + paramValue = [] + } + if ( + (repeat && !Array.isArray(paramValue)) || + (!repeat && typeof paramValue !== 'string') + ) { + throw new Error( + `A required parameter (${validParamKey}) was not provided as ${ + repeat ? 'an array' : 'a string' + } in getStaticPaths for ${page}` + ) + } + let replaced = `[${repeat ? '...' : ''}${validParamKey}]` + if (optional) { + replaced = `[${replaced}]` + } + builtPage = builtPage + .replace( + replaced, + repeat + ? (paramValue as string[]) + .map((segment) => escapePathDelimiters(segment, true)) + .join('/') + : escapePathDelimiters(paramValue as string, true) + ) + .replace(/(?!^)\/$/, '') + + encodedBuiltPage = encodedBuiltPage + .replace( + replaced, + repeat + ? (paramValue as string[]).map(encodeURIComponent).join('/') + : encodeURIComponent(paramValue as string) + ) + .replace(/(?!^)\/$/, '') + }) + + if (entry.locale && !locales?.includes(entry.locale)) { + throw new Error( + `Invalid locale returned from getStaticPaths for ${page}, the locale ${entry.locale} is not specified in blitz.config.js` + ) + } + const curLocale = entry.locale || defaultLocale || '' + + prerenderPaths.add( + `${curLocale ? `/${curLocale}` : ''}${ + curLocale && builtPage === '/' ? '' : builtPage + }` + ) + encodedPrerenderPaths.add( + `${curLocale ? `/${curLocale}` : ''}${ + curLocale && encodedBuiltPage === '/' ? '' : encodedBuiltPage + }` + ) + } + }) + + return { + paths: [...prerenderPaths], + fallback: staticPathsResult.fallback, + encodedPaths: [...encodedPrerenderPaths], + } +} + +export async function isPageStatic( + page: string, + distDir: string, + serverless: boolean, + runtimeEnvConfig: any, + httpAgentOptions: NextConfigComplete['httpAgentOptions'], + locales?: string[], + defaultLocale?: string, + parentId?: any +): Promise<{ + isStatic?: boolean + isAmpOnly?: boolean + isHybridAmp?: boolean + hasServerProps?: boolean + hasStaticProps?: boolean + prerenderRoutes?: string[] + encodedPrerenderRoutes?: string[] + prerenderFallback?: boolean | 'blocking' + isNextImageImported?: boolean +}> { + const isPageStaticSpan = trace('is-page-static-utils', parentId) + return isPageStaticSpan.traceAsyncFn(async () => { + try { + require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig) + setHttpAgentOptions(httpAgentOptions) + const components = await loadComponents(distDir, page, serverless) + const mod = components.ComponentMod + const Comp = mod.default || mod + + if (!Comp || !isValidElementType(Comp) || typeof Comp === 'string') { + throw new Error('INVALID_DEFAULT_EXPORT') + } + + const hasGetInitialProps = !!(Comp as any).getInitialProps + const hasStaticProps = !!(await mod.getStaticProps) + const hasStaticPaths = !!(await mod.getStaticPaths) + const hasServerProps = !!(await mod.getServerSideProps) + const hasLegacyServerProps = !!(await mod.unstable_getServerProps) + const hasLegacyStaticProps = !!(await mod.unstable_getStaticProps) + const hasLegacyStaticPaths = !!(await mod.unstable_getStaticPaths) + const hasLegacyStaticParams = !!(await mod.unstable_getStaticParams) + + if (hasLegacyStaticParams) { + throw new Error( + `unstable_getStaticParams was replaced with getStaticPaths. Please update your code.` + ) + } + + if (hasLegacyStaticPaths) { + throw new Error( + `unstable_getStaticPaths was replaced with getStaticPaths. Please update your code.` + ) + } + + if (hasLegacyStaticProps) { + throw new Error( + `unstable_getStaticProps was replaced with getStaticProps. Please update your code.` + ) + } + + if (hasLegacyServerProps) { + throw new Error( + `unstable_getServerProps was replaced with getServerSideProps. Please update your code.` + ) + } + + // A page cannot be prerendered _and_ define a data requirement. That's + // contradictory! + if (hasGetInitialProps && hasStaticProps) { + throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT) + } + + if (hasGetInitialProps && hasServerProps) { + throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT) + } + + if (hasStaticProps && hasServerProps) { + throw new Error(SERVER_PROPS_SSG_CONFLICT) + } + + const pageIsDynamic = isDynamicRoute(page) + // A page cannot have static parameters if it is not a dynamic page. + if (hasStaticProps && hasStaticPaths && !pageIsDynamic) { + throw new Error( + `getStaticPaths can only be used with dynamic pages, not '${page}'.` + + `\nLearn more: https://nextjs.org/docs/routing/dynamic-routes` + ) + } + + if (hasStaticProps && pageIsDynamic && !hasStaticPaths) { + throw new Error( + `getStaticPaths is required for dynamic SSG pages and is missing for '${page}'.` + + `\nRead more: https://nextjs.org/docs/messages/invalid-getstaticpaths-value` + ) + } + + let prerenderRoutes: Array | undefined + let encodedPrerenderRoutes: Array | undefined + let prerenderFallback: boolean | 'blocking' | undefined + if (hasStaticProps && hasStaticPaths) { + ;({ + paths: prerenderRoutes, + fallback: prerenderFallback, + encodedPaths: encodedPrerenderRoutes, + } = await buildStaticPaths( + page, + mod.getStaticPaths, + locales, + defaultLocale + )) + } + + const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED + const config = mod.config || {} + return { + isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps, + isHybridAmp: config.amp === 'hybrid', + isAmpOnly: config.amp === true, + prerenderRoutes, + prerenderFallback, + encodedPrerenderRoutes, + hasStaticProps, + hasServerProps, + isNextImageImported, + } + } catch (err) { + if (err.code === 'MODULE_NOT_FOUND') return {} + throw err + } + }) +} + +export async function hasCustomGetInitialProps( + page: string, + distDir: string, + isLikeServerless: boolean, + runtimeEnvConfig: any, + checkingApp: boolean +): Promise { + require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig) + + const components = await loadComponents(distDir, page, isLikeServerless) + let mod = components.ComponentMod + + if (checkingApp) { + mod = (await mod._app) || mod.default || mod + } else { + mod = mod.default || mod + } + mod = await mod + return mod.getInitialProps !== mod.origGetInitialProps +} + +export async function getNamedExports( + page: string, + distDir: string, + isLikeServerless: boolean, + runtimeEnvConfig: any +): Promise> { + require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig) + const components = await loadComponents(distDir, page, isLikeServerless) + let mod = components.ComponentMod + + return Object.keys(mod) +} + +export function detectConflictingPaths( + combinedPages: string[], + ssgPages: Set, + additionalSsgPaths: Map +) { + const conflictingPaths = new Map< + string, + Array<{ + path: string + page: string + }> + >() + + const dynamicSsgPages = [...ssgPages].filter((page) => isDynamicRoute(page)) + + additionalSsgPaths.forEach((paths, pathsPage) => { + paths.forEach((curPath) => { + const lowerPath = curPath.toLowerCase() + let conflictingPage = combinedPages.find( + (page) => page.toLowerCase() === lowerPath + ) + + if (conflictingPage) { + conflictingPaths.set(lowerPath, [ + { path: curPath, page: pathsPage }, + { path: conflictingPage, page: conflictingPage }, + ]) + } else { + let conflictingPath: string | undefined + + conflictingPage = dynamicSsgPages.find((page) => { + if (page === pathsPage) return false + + conflictingPath = additionalSsgPaths + .get(page) + ?.find((compPath) => compPath.toLowerCase() === lowerPath) + return conflictingPath + }) + + if (conflictingPage && conflictingPath) { + conflictingPaths.set(lowerPath, [ + { path: curPath, page: pathsPage }, + { path: conflictingPath, page: conflictingPage }, + ]) + } + } + }) + }) + + if (conflictingPaths.size > 0) { + let conflictingPathsOutput = '' + + conflictingPaths.forEach((pathItems) => { + pathItems.forEach((pathItem, idx) => { + const isDynamic = pathItem.page !== pathItem.path + + if (idx > 0) { + conflictingPathsOutput += 'conflicts with ' + } + + conflictingPathsOutput += `path: "${pathItem.path}"${ + isDynamic ? ` from page: "${pathItem.page}" ` : ' ' + }` + }) + conflictingPathsOutput += '\n' + }) + + Log.error( + 'Conflicting paths returned from getStaticPaths, paths must unique per page.\n' + + 'See more info here: https://nextjs.org/docs/messages/conflicting-ssg-paths\n\n' + + conflictingPathsOutput + ) + process.exit(1) + } +} + +export function getCssFilePaths(buildManifest: BuildManifest): string[] { + const cssFiles = new Set() + Object.values(buildManifest.pages).forEach((files) => { + files.forEach((file) => { + if (file.endsWith('.css')) { + cssFiles.add(file) + } + }) + }) + + return [...cssFiles] +} diff --git a/nextjs/packages/next/build/webpack-config.ts b/nextjs/packages/next/build/webpack-config.ts new file mode 100644 index 0000000000..81da868b33 --- /dev/null +++ b/nextjs/packages/next/build/webpack-config.ts @@ -0,0 +1,1948 @@ +import ReactRefreshWebpackPlugin from '@next/react-refresh-utils/ReactRefreshWebpackPlugin' +import chalk from 'chalk' +import crypto from 'crypto' +import { readFileSync } from 'fs' +import { codeFrameColumns } from 'next/dist/compiled/babel/code-frame' +import semver from 'next/dist/compiled/semver' +import { isWebpack5, webpack } from 'next/dist/compiled/webpack/webpack' +import path, { join as pathJoin, relative as relativePath } from 'path' +import { + DOT_NEXT_ALIAS, + NEXT_PROJECT_ROOT, + NEXT_PROJECT_ROOT_DIST_CLIENT, + PAGES_DIR_ALIAS, +} from '../lib/constants' +import { fileExists } from '../lib/file-exists' +import { getPackageVersion } from '../lib/get-package-version' +import { CustomRoutes } from '../lib/load-custom-routes.js' +import { getTypeScriptConfiguration } from '../lib/typescript/getTypeScriptConfiguration' +import { + CLIENT_STATIC_FILES_RUNTIME_AMP, + CLIENT_STATIC_FILES_RUNTIME_MAIN, + CLIENT_STATIC_FILES_RUNTIME_POLYFILLS, + CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH, + CLIENT_STATIC_FILES_RUNTIME_WEBPACK, + REACT_LOADABLE_MANIFEST, + SERVERLESS_DIRECTORY, + SERVER_DIRECTORY, +} from '../shared/lib/constants' +import { execOnce } from '../shared/lib/utils' +import { NextConfigComplete } from '../server/config-shared' +import { findPageFile } from '../server/lib/find-page-file' +import { WebpackEntrypoints } from './entries' +import * as Log from './output/log' +import { build as buildConfiguration } from './webpack/config' +import { __overrideCssConfiguration } from './webpack/config/blocks/css/overrideCssConfiguration' +import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin' +import BuildStatsPlugin from './webpack/plugins/build-stats-plugin' +import ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin' +import { JsConfigPathsPlugin } from './webpack/plugins/jsconfig-paths-plugin' +import { DropClientPage } from './webpack/plugins/next-drop-client-page-plugin' +import NextJsSsrImportPlugin from './webpack/plugins/nextjs-ssr-import' +import NextJsSSRModuleCachePlugin from './webpack/plugins/nextjs-ssr-module-cache' +import PagesManifestPlugin from './webpack/plugins/pages-manifest-plugin' +import { ProfilingPlugin } from './webpack/plugins/profiling-plugin' +import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-plugin' +import { ServerlessPlugin } from './webpack/plugins/serverless-plugin' +import WebpackConformancePlugin, { + DuplicatePolyfillsConformanceCheck, + GranularChunksConformanceCheck, + MinificationConformanceCheck, + ReactSyncScriptsConformanceCheck, +} from './webpack/plugins/webpack-conformance-plugin' +import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin' +import { regexLikeCss } from './webpack/config/blocks/css' +import { existsSync } from 'fs' +import { getSessionCookiePrefix } from '../server/lib/utils' + +type ExcludesFalse = (x: T | false) => x is T + +const devtoolRevertWarning = execOnce( + (devtool: webpack.Configuration['devtool']) => { + console.warn( + chalk.yellow.bold('Warning: ') + + chalk.bold(`Reverting webpack devtool to '${devtool}'.\n`) + + 'Changing the webpack devtool in development mode will cause severe performance regressions.\n' + + 'Read more: https://nextjs.org/docs/messages/improper-devtool' + ) + } +) + +function parseJsonFile(filePath: string) { + const JSON5 = require('next/dist/compiled/json5') + const contents = readFileSync(filePath, 'utf8') + + // Special case an empty file + if (contents.trim() === '') { + return {} + } + + try { + return JSON5.parse(contents) + } catch (err) { + const codeFrame = codeFrameColumns( + String(contents), + { start: { line: err.lineNumber, column: err.columnNumber } }, + { message: err.message, highlightCode: true } + ) + throw new Error(`Failed to parse "${filePath}":\n${codeFrame}`) + } +} + +function getOptimizedAliases(isServer: boolean): { [pkg: string]: string } { + if (isServer) { + return {} + } + + const stubWindowFetch = path.join(__dirname, 'polyfills', 'fetch', 'index.js') + const stubObjectAssign = path.join(__dirname, 'polyfills', 'object-assign.js') + + const shimAssign = path.join(__dirname, 'polyfills', 'object.assign') + return Object.assign( + {}, + { + unfetch$: stubWindowFetch, + 'isomorphic-unfetch$': stubWindowFetch, + 'whatwg-fetch$': path.join( + __dirname, + 'polyfills', + 'fetch', + 'whatwg-fetch.js' + ), + }, + { + 'object-assign$': stubObjectAssign, + + // Stub Package: object.assign + 'object.assign/auto': path.join(shimAssign, 'auto.js'), + 'object.assign/implementation': path.join( + shimAssign, + 'implementation.js' + ), + 'object.assign$': path.join(shimAssign, 'index.js'), + 'object.assign/polyfill': path.join(shimAssign, 'polyfill.js'), + 'object.assign/shim': path.join(shimAssign, 'shim.js'), + + // Replace: full URL polyfill with platform-based polyfill + url: require.resolve('native-url'), + } + ) +} + +type ClientEntries = { + [key: string]: string | string[] +} + +export function attachReactRefresh( + webpackConfig: webpack.Configuration, + targetLoader: webpack.RuleSetUseItem +) { + let injections = 0 + const reactRefreshLoaderName = '@next/react-refresh-utils/loader' + const reactRefreshLoader = require.resolve(reactRefreshLoaderName) + webpackConfig.module?.rules.forEach((rule) => { + const curr = rule.use + // When the user has configured `defaultLoaders.babel` for a input file: + if (curr === targetLoader) { + ++injections + rule.use = [reactRefreshLoader, curr as webpack.RuleSetUseItem] + } else if ( + Array.isArray(curr) && + curr.some((r) => r === targetLoader) && + // Check if loader already exists: + !curr.some( + (r) => r === reactRefreshLoader || r === reactRefreshLoaderName + ) + ) { + ++injections + const idx = curr.findIndex((r) => r === targetLoader) + // Clone to not mutate user input + rule.use = [...curr] + + // inject / input: [other, babel] output: [other, refresh, babel]: + rule.use.splice(idx, 0, reactRefreshLoader) + } + }) + + if (injections) { + Log.info( + `automatically enabled Fast Refresh for ${injections} custom loader${ + injections > 1 ? 's' : '' + }` + ) + } +} + +const WEBPACK_RESOLVE_OPTIONS = { + // This always uses commonjs resolving, assuming API is identical + // between ESM and CJS in a package + // Otherwise combined ESM+CJS packages will never be external + // as resolving mismatch would lead to opt-out from being external. + dependencyType: 'commonjs', + symlinks: true, +} + +const WEBPACK_ESM_RESOLVE_OPTIONS = { + dependencyType: 'esm', + symlinks: true, +} + +const NODE_RESOLVE_OPTIONS = { + dependencyType: 'commonjs', + modules: ['node_modules'], + alias: false, + fallback: false, + exportsFields: ['exports'], + importsFields: ['imports'], + conditionNames: ['node', 'require', 'module'], + descriptionFiles: ['package.json'], + extensions: ['.js', '.json', '.node'], + enforceExtensions: false, + symlinks: true, + mainFields: ['main'], + mainFiles: ['index'], + roots: [], + fullySpecified: false, + preferRelative: false, + preferAbsolute: false, + restrictions: [], +} + +const NODE_ESM_RESOLVE_OPTIONS = { + ...NODE_RESOLVE_OPTIONS, + dependencyType: 'esm', + conditionNames: ['node', 'import', 'module'], + fullySpecified: true, +} + +export default async function getBaseWebpackConfig( + dir: string, + { + buildId, + config, + dev = false, + isServer = false, + pagesDir, + target = 'server', + reactProductionProfiling = false, + entrypoints, + rewrites, + isDevFallback = false, + }: { + buildId: string + config: NextConfigComplete + dev?: boolean + isServer?: boolean + pagesDir: string + target?: string + reactProductionProfiling?: boolean + entrypoints: WebpackEntrypoints + rewrites: CustomRoutes['rewrites'] + isDevFallback?: boolean + } +): Promise { + const hasRewrites = + rewrites.beforeFiles.length > 0 || + rewrites.afterFiles.length > 0 || + rewrites.fallback.length > 0 + const hasReactRefresh: boolean = dev && !isServer + const reactDomVersion = await getPackageVersion({ + cwd: dir, + name: 'react-dom', + }) + const hasReactExperimental: boolean = + Boolean(reactDomVersion) && reactDomVersion!.includes('experimental') // blitz + const hasReact18: boolean = + (Boolean(reactDomVersion) && + (semver.gte(reactDomVersion!, '18.0.0') || + semver.coerce(reactDomVersion)?.version === '18.0.0')) || + hasReactExperimental // blitz + const hasReactRoot: boolean = config.experimental.reactRoot ?? hasReact18 + // Have to set this suspense env for the actual build, because the webpack + // string replace below only affects the build output, not anything during + // the build like static page optimization + process.env.__BLITZ_SUSPENSE_ENABLED = String(hasReactRoot) + + const babelConfigFile = await [ + '.babelrc', + '.babelrc.json', + '.babelrc.js', + '.babelrc.mjs', + '.babelrc.cjs', + 'babel.config.js', + 'babel.config.json', + 'babel.config.mjs', + 'babel.config.cjs', + ].reduce(async (memo: Promise, filename) => { + const configFilePath = path.join(dir, filename) + return ( + (await memo) || + ((await fileExists(configFilePath)) ? configFilePath : undefined) + ) + }, Promise.resolve(undefined)) + + const distDir = path.join(dir, config.distDir) + + /* ------ Blitz.js ------- */ + const hasDbModule = + existsSync(path.join(dir, 'db/index.js')) || + existsSync(path.join(dir, 'db/index.ts')) + + const sessionCookiePrefix = getSessionCookiePrefix(config) + /* ------ Blitz.js ------- */ + + // Webpack 5 can use the faster babel loader, webpack 5 has built-in caching for loaders + // For webpack 4 the old loader is used as it has external caching + const babelLoader = isWebpack5 + ? require.resolve('./babel/loader/index') + : 'next-babel-loader' + + const useSWCLoader = config.experimental.swcLoader && isWebpack5 + if (useSWCLoader && babelConfigFile) { + Log.warn( + `experimental.swcLoader enabled. The custom Babel configuration will not be used.` + ) + } + const defaultLoaders = { + babel: useSWCLoader + ? { + loader: 'next-swc-loader', + options: { + isServer, + }, + } + : { + loader: babelLoader, + options: { + configFile: babelConfigFile, + isServer, + distDir, + pagesDir, + cwd: dir, + // Webpack 5 has a built-in loader cache + cache: !isWebpack5, + development: dev, + hasReactRefresh, + hasJsxRuntime: true, + }, + }, + // Backwards compat + hotSelfAccept: { + loader: 'noop-loader', + }, + } + + const babelIncludeRegexes: RegExp[] = [ + /next[\\/]dist[\\/]shared[\\/]lib/, + /next[\\/]dist[\\/]client/, + /next[\\/]dist[\\/]pages/, + /[\\/](strip-ansi|ansi-regex)[\\/]/, + ] + + // Support for NODE_PATH + const nodePathList = (process.env.NODE_PATH || '') + .split(process.platform === 'win32' ? ';' : ':') + .filter((p) => !!p) + + const isServerless = target === 'serverless' + const isServerlessTrace = target === 'experimental-serverless-trace' + // Intentionally not using isTargetLikeServerless helper + const isLikeServerless = isServerless || isServerlessTrace + + const outputDir = isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY + const outputPath = path.join(distDir, isServer ? outputDir : '') + const totalPages = Object.keys(entrypoints).length + const clientEntries = !isServer + ? ({ + // Backwards compatibility + 'main.js': [], + ...(dev + ? { + [CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH]: require.resolve( + `@next/react-refresh-utils/runtime` + ), + [CLIENT_STATIC_FILES_RUNTIME_AMP]: + `./` + + relativePath( + dir, + pathJoin(NEXT_PROJECT_ROOT_DIST_CLIENT, 'dev', 'amp-dev') + ).replace(/\\/g, '/'), + } + : {}), + [CLIENT_STATIC_FILES_RUNTIME_MAIN]: + `./` + + path + .relative( + dir, + path.join( + NEXT_PROJECT_ROOT_DIST_CLIENT, + dev ? `next-dev.js` : 'next.js' + ) + ) + .replace(/\\/g, '/'), + [CLIENT_STATIC_FILES_RUNTIME_POLYFILLS]: path.join( + NEXT_PROJECT_ROOT_DIST_CLIENT, + 'polyfills.js' + ), + } as ClientEntries) + : undefined + + let typeScriptPath: string | undefined + try { + typeScriptPath = require.resolve('typescript', { paths: [dir] }) + } catch (_) {} + const tsConfigPath = path.join(dir, 'tsconfig.json') + const useTypeScript = Boolean( + typeScriptPath && (await fileExists(tsConfigPath)) + ) + + let jsConfig + // jsconfig is a subset of tsconfig + if (useTypeScript) { + const ts = (await import(typeScriptPath!)) as typeof import('typescript') + const tsConfig = await getTypeScriptConfiguration(ts, tsConfigPath, true) + jsConfig = { compilerOptions: tsConfig.options } + } + + const jsConfigPath = path.join(dir, 'jsconfig.json') + if (!useTypeScript && (await fileExists(jsConfigPath))) { + jsConfig = parseJsonFile(jsConfigPath) + } + + let resolvedBaseUrl + if (jsConfig?.compilerOptions?.baseUrl) { + resolvedBaseUrl = path.resolve(dir, jsConfig.compilerOptions.baseUrl) + } + + function getReactProfilingInProduction() { + if (reactProductionProfiling) { + return { + 'react-dom$': 'react-dom/profiling', + 'scheduler/tracing': 'scheduler/tracing-profiling', + } + } + } + + const clientResolveRewrites = require.resolve( + '../shared/lib/router/utils/resolve-rewrites' + ) + const clientResolveRewritesNoop = require.resolve( + '../shared/lib/router/utils/resolve-rewrites-noop' + ) + + const resolveConfig = { + // Disable .mjs for node_modules bundling + extensions: isServer + ? [ + '.js', + '.mjs', + ...(useTypeScript ? ['.tsx', '.ts'] : []), + '.jsx', + '.json', + '.wasm', + ] + : [ + '.mjs', + '.js', + ...(useTypeScript ? ['.tsx', '.ts'] : []), + '.jsx', + '.json', + '.wasm', + ], + modules: [ + 'node_modules', + ...nodePathList, // Support for NODE_PATH environment variable + ], + alias: { + next: NEXT_PROJECT_ROOT, + [PAGES_DIR_ALIAS]: pagesDir, + [DOT_NEXT_ALIAS]: distDir, + ...getOptimizedAliases(isServer), + ...getReactProfilingInProduction(), + [clientResolveRewrites]: hasRewrites + ? clientResolveRewrites + : // With webpack 5 an alias can be pointed to false to noop + isWebpack5 + ? false + : clientResolveRewritesNoop, + }, + ...(isWebpack5 && !isServer + ? { + // Full list of old polyfills is accessible here: + // https://github.com/webpack/webpack/blob/2a0536cf510768111a3a6dceeb14cb79b9f59273/lib/ModuleNotFoundError.js#L13-L42 + fallback: { + assert: require.resolve('assert/'), + buffer: require.resolve('buffer/'), + constants: require.resolve('constants-browserify'), + crypto: require.resolve('crypto-browserify'), + domain: require.resolve('domain-browser'), + http: require.resolve('stream-http'), + https: require.resolve('https-browserify'), + os: require.resolve('os-browserify/browser'), + path: require.resolve('path-browserify'), + punycode: require.resolve('punycode'), + process: require.resolve('process/browser'), + // Handled in separate alias + querystring: require.resolve('querystring-es3'), + stream: require.resolve('stream-browserify'), + string_decoder: require.resolve('string_decoder'), + sys: require.resolve('util/'), + timers: require.resolve('timers-browserify'), + tty: require.resolve('tty-browserify'), + // Handled in separate alias + // url: require.resolve('url/'), + util: require.resolve('util/'), + vm: require.resolve('vm-browserify'), + zlib: require.resolve('browserify-zlib'), + }, + } + : undefined), + mainFields: isServer ? ['main', 'module'] : ['browser', 'module', 'main'], + plugins: isWebpack5 + ? // webpack 5+ has the PnP resolver built-in by default: + [] + : [require('pnp-webpack-plugin')], + } + + const terserOptions: any = { + parse: { + ecma: 8, + }, + compress: { + ecma: 5, + warnings: false, + // The following two options are known to break valid JavaScript code + comparisons: false, + inline: 2, // https://github.com/vercel/next.js/issues/7178#issuecomment-493048965 + }, + mangle: { safari10: true }, + output: { + ecma: 5, + safari10: true, + comments: false, + // Fixes usage of Emoji and certain Regex + ascii_only: true, + }, + } + + const isModuleCSS = (module: { type: string }): boolean => { + return ( + // mini-css-extract-plugin + module.type === `css/mini-extract` || + // extract-css-chunks-webpack-plugin (old) + module.type === `css/extract-chunks` || + // extract-css-chunks-webpack-plugin (new) + module.type === `css/extract-css-chunks` + ) + } + + // Contains various versions of the Webpack SplitChunksPlugin used in different build types + const splitChunksConfigs: { + [propName: string]: webpack.Options.SplitChunksOptions | false + } = { + dev: { + cacheGroups: { + default: false, + vendors: false, + }, + }, + prodGranular: { + // Keep main and _app chunks unsplitted in webpack 5 + // as we don't need a separate vendor chunk from that + // and all other chunk depend on them so there is no + // duplication that need to be pulled out. + chunks: isWebpack5 + ? (chunk) => !/^(polyfills|main|pages\/_app)$/.test(chunk.name) + : 'all', + cacheGroups: { + framework: { + chunks: 'all', + name: 'framework', + // This regex ignores nested copies of framework libraries so they're + // bundled with their issuer. + // https://github.com/vercel/next.js/pull/9012 + test: /(? 160000 && + /node_modules[/\\]/.test(module.nameForCondition() || '') + ) + }, + name(module: { + type: string + libIdent?: Function + updateHash: (hash: crypto.Hash) => void + }): string { + const hash = crypto.createHash('sha1') + if (isModuleCSS(module)) { + module.updateHash(hash) + } else { + if (!module.libIdent) { + throw new Error( + `Encountered unknown module type: ${module.type}. Please open an issue.` + ) + } + + hash.update(module.libIdent({ context: dir })) + } + + return hash.digest('hex').substring(0, 8) + }, + priority: 30, + minChunks: 1, + reuseExistingChunk: true, + }, + commons: { + name: 'commons', + minChunks: totalPages, + priority: 20, + }, + ...(isWebpack5 + ? undefined + : { + default: false, + vendors: false, + shared: { + name(module, chunks) { + return ( + crypto + .createHash('sha1') + .update( + chunks.reduce( + (acc: string, chunk: webpack.compilation.Chunk) => { + return acc + chunk.name + }, + '' + ) + ) + .digest('hex') + (isModuleCSS(module) ? '_CSS' : '') + ) + }, + priority: 10, + minChunks: 2, + reuseExistingChunk: true, + }, + }), + }, + maxInitialRequests: 25, + minSize: 20000, + }, + } + + // Select appropriate SplitChunksPlugin config for this build + let splitChunksConfig: webpack.Options.SplitChunksOptions | false + if (dev) { + splitChunksConfig = isWebpack5 ? false : splitChunksConfigs.dev + } else { + splitChunksConfig = splitChunksConfigs.prodGranular + } + + const crossOrigin = config.crossOrigin + + let customAppFile: string | null = await findPageFile( + pagesDir, + '/_app', + config.pageExtensions + ) + if (customAppFile) { + customAppFile = path.resolve(path.join(pagesDir, customAppFile)) + } + + const conformanceConfig = Object.assign( + { + ReactSyncScriptsConformanceCheck: { + enabled: true, + }, + MinificationConformanceCheck: { + enabled: true, + }, + DuplicatePolyfillsConformanceCheck: { + enabled: true, + BlockedAPIToBePolyfilled: Object.assign( + [], + ['fetch'], + config.conformance?.DuplicatePolyfillsConformanceCheck + ?.BlockedAPIToBePolyfilled || [] + ), + }, + GranularChunksConformanceCheck: { + enabled: true, + }, + }, + config.conformance + ) + + const esmExternals = !!config.experimental?.esmExternals + const looseEsmExternals = config.experimental?.esmExternals === 'loose' + + async function handleExternals( + context: string, + request: string, + dependencyType: string, + getResolve: ( + options: any + ) => ( + resolveContext: string, + resolveRequest: string + ) => Promise<[string | null, boolean]> + ) { + // We need to externalize internal requests for files intended to + // not be bundled. + + const isLocal: boolean = + request.startsWith('.') || + // Always check for unix-style path, as webpack sometimes + // normalizes as posix. + path.posix.isAbsolute(request) || + // When on Windows, we also want to check for Windows-specific + // absolute paths. + (process.platform === 'win32' && path.win32.isAbsolute(request)) + + // Relative requires don't need custom resolution, because they + // are relative to requests we've already resolved here. + // Absolute requires (require('/foo')) are extremely uncommon, but + // also have no need for customization as they're already resolved. + if (!isLocal) { + if (/^(?:next$|react(?:$|\/))/.test(request)) { + return `commonjs ${request}` + } + + const notExternalModules = /^(?:private-next-pages\/|next\/(?:dist\/pages\/|(?:app|document|link|image|constants)$)|string-hash$)/ + if (notExternalModules.test(request)) { + return + } + } + + // When in esm externals mode, and using import, we resolve with + // ESM resolving options. + const isEsmRequested = dependencyType === 'esm' + const preferEsm = esmExternals && isEsmRequested + + const resolve = getResolve( + preferEsm ? WEBPACK_ESM_RESOLVE_OPTIONS : WEBPACK_RESOLVE_OPTIONS + ) + + // Resolve the import with the webpack provided context, this + // ensures we're resolving the correct version when multiple + // exist. + let res: string | null + let isEsm: boolean = false + try { + ;[res, isEsm] = await resolve(context, request) + } catch (err) { + res = null + } + + // If resolving fails, and we can use an alternative way + // try the alternative resolving options. + if (!res && (isEsmRequested || looseEsmExternals)) { + const resolveAlternative = getResolve( + preferEsm ? WEBPACK_RESOLVE_OPTIONS : WEBPACK_ESM_RESOLVE_OPTIONS + ) + try { + ;[res, isEsm] = await resolveAlternative(context, request) + } catch (err) { + res = null + } + } + + // If the request cannot be resolved we need to have + // webpack "bundle" it so it surfaces the not found error. + if (!res) { + return + } + + // ESM externals can only be imported (and not required). + // Make an exception in loose mode. + if (!isEsmRequested && isEsm && !looseEsmExternals) { + throw new Error( + `ESM packages (${request}) need to be imported. Use 'import' to reference the package instead. https://nextjs.org/docs/messages/import-esm-externals` + ) + } + + if (isLocal) { + // Makes sure dist/shared and dist/server are not bundled + // we need to process shared/lib/router/router so that + // the DefinePlugin can inject process.env values + const isNextExternal = /next[/\\]dist[/\\](shared|server)[/\\](?!lib[/\\]router[/\\]router)/.test( + res + ) + + if (isNextExternal) { + // Generate Next.js external import + const externalRequest = path.posix.join( + 'next', + 'dist', + path + .relative( + // Root of Next.js package: + path.join(__dirname, '..'), + res + ) + // Windows path normalization + .replace(/\\/g, '/') + ) + return `commonjs ${externalRequest}` + } else { + return + } + } + + // Bundled Node.js code is relocated without its node_modules tree. + // This means we need to make sure its request resolves to the same + // package that'll be available at runtime. If it's not identical, + // we need to bundle the code (even if it _should_ be external). + let baseRes: string | null + let baseIsEsm: boolean + try { + const baseResolve = getResolve( + isEsm ? NODE_ESM_RESOLVE_OPTIONS : NODE_RESOLVE_OPTIONS + ) + ;[baseRes, baseIsEsm] = await baseResolve(dir, request) + } catch (err) { + baseRes = null + baseIsEsm = false + } + + // Same as above: if the package, when required from the root, + // would be different from what the real resolution would use, we + // cannot externalize it. + // if request is pointing to a symlink it could point to the the same file, + // the resolver will resolve symlinks so this is handled + if (baseRes !== res || isEsm !== baseIsEsm) { + return + } + + const externalType = isEsm ? 'module' : 'commonjs' + + if ( + res.match(/next[/\\]dist[/\\]shared[/\\](?!lib[/\\]router[/\\]router)/) + ) { + return `${externalType} ${request}` + } + + // Default pages have to be transpiled + if ( + res.match(/[/\\]next[/\\]dist[/\\]/) || + // This is the @babel/plugin-transform-runtime "helpers: true" option + res.match(/node_modules[/\\]@babel[/\\]runtime[/\\]/) + ) { + return + } + + // Webpack itself has to be compiled because it doesn't always use module relative paths + if ( + res.match(/node_modules[/\\]webpack/) || + res.match(/node_modules[/\\]css-loader/) + ) { + return + } + + // Anything else that is standard JavaScript within `node_modules` + // can be externalized. + if (/node_modules[/\\].*\.c?js$/.test(res)) { + return `${externalType} ${request}` + } + + // Default behavior: bundle the code! + } + + const emacsLockfilePattern = '**/.#*' + + let webpackConfig: webpack.Configuration = { + externals: !isServer + ? // make sure importing "next" is handled gracefully for client + // bundles in case a user imported types and it wasn't removed + // TODO: should we warn/error for this instead? + ['next'] + : !isServerless + ? [ + isWebpack5 + ? ({ + context, + request, + dependencyType, + getResolve, + }: { + context: string + request: string + dependencyType: string + getResolve: ( + options: any + ) => ( + resolveContext: string, + resolveRequest: string, + callback: ( + err?: Error, + result?: string, + resolveData?: { descriptionFileData?: { type?: any } } + ) => void + ) => void + }) => + handleExternals(context, request, dependencyType, (options) => { + const resolveFunction = getResolve(options) + return (resolveContext: string, requestToResolve: string) => + new Promise((resolve, reject) => { + resolveFunction( + resolveContext, + requestToResolve, + (err, result, resolveData) => { + if (err) return reject(err) + if (!result) return resolve([null, false]) + const isEsm = /\.js$/i.test(result) + ? resolveData?.descriptionFileData?.type === + 'module' + : /\.mjs$/i.test(result) + resolve([result, isEsm]) + } + ) + }) + }) + : ( + context: string, + request: string, + callback: (err?: Error, result?: string | undefined) => void + ) => + handleExternals( + context, + request, + 'commonjs', + () => (resolveContext: string, requestToResolve: string) => + new Promise((resolve) => + resolve([ + require.resolve(requestToResolve, { + paths: [resolveContext], + }), + false, + ]) + ) + ).then((result) => callback(undefined, result), callback), + ] + : [ + // When the 'serverless' target is used all node_modules will be compiled into the output bundles + // So that the 'serverless' bundles have 0 runtime dependencies + 'next/dist/compiled/@ampproject/toolbox-optimizer', // except this one + + // Mark this as external if not enabled so it doesn't cause a + // webpack error from being missing + ...(config.experimental.optimizeCss ? [] : ['critters']), + ], + optimization: { + // Webpack 5 uses a new property for the same functionality + ...(isWebpack5 ? { emitOnErrors: !dev } : { noEmitOnErrors: dev }), + checkWasmTypes: false, + nodeEnv: false, + splitChunks: isServer + ? isWebpack5 && !dev + ? ({ + filename: '[name].js', + // allow to split entrypoints + chunks: 'all', + // size of files is not so relevant for server build + // we want to prefer deduplication to load less code + minSize: 1000, + } as any) + : false + : splitChunksConfig, + runtimeChunk: isServer + ? isWebpack5 && !isLikeServerless + ? { name: 'webpack-runtime' } + : undefined + : { name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK }, + minimize: !(dev || isServer), + minimizer: [ + // Minify JavaScript + (compiler: webpack.Compiler) => { + // @ts-ignore No typings yet + const { + TerserPlugin, + } = require('./webpack/plugins/terser-webpack-plugin/src/index.js') + new TerserPlugin({ + cacheDir: path.join(distDir, 'cache', 'next-minifier'), + parallel: config.experimental.cpus, + swcMinify: config.experimental.swcMinify, + terserOptions, + }).apply(compiler) + }, + // Minify CSS + (compiler: webpack.Compiler) => { + const { + CssMinimizerPlugin, + } = require('./webpack/plugins/css-minimizer-plugin') + new CssMinimizerPlugin({ + postcssOptions: { + map: { + // `inline: false` generates the source map in a separate file. + // Otherwise, the CSS file is needlessly large. + inline: false, + // `annotation: false` skips appending the `sourceMappingURL` + // to the end of the CSS file. Webpack already handles this. + annotation: false, + }, + }, + }).apply(compiler) + }, + ], + }, + context: dir, + node: { + setImmediate: false, + }, + // Kept as function to be backwards compatible + // @ts-ignore TODO webpack 5 typings needed + entry: async () => { + return { + ...(clientEntries ? clientEntries : {}), + ...(isServer && hasDbModule ? { 'blitz-db': './db/index' } : {}), + ...entrypoints, + } + }, + watchOptions: { + aggregateTimeout: 5, + ignored: [ + '**/.git/**', + '**/node_modules/**', + '**/.next/**', + // can be removed after https://github.com/paulmillr/chokidar/issues/955 is released + emacsLockfilePattern, + ], + }, + output: { + // we must set publicPath to an empty value to override the default of + // auto which doesn't work in IE11 + publicPath: `${config.assetPrefix || ''}/_next/`, + path: + isServer && isWebpack5 && !dev + ? path.join(outputPath, 'chunks') + : outputPath, + // On the server we don't use hashes + filename: isServer + ? isWebpack5 && !dev + ? '../[name].js' + : '[name].js' + : `static/chunks/${isDevFallback ? 'fallback/' : ''}[name]${ + dev ? '' : isWebpack5 ? '-[contenthash]' : '-[chunkhash]' + }.js`, + library: isServer ? undefined : '_N_E', + libraryTarget: isServer ? 'commonjs2' : 'assign', + hotUpdateChunkFilename: isWebpack5 + ? 'static/webpack/[id].[fullhash].hot-update.js' + : 'static/webpack/[id].[hash].hot-update.js', + hotUpdateMainFilename: isWebpack5 + ? 'static/webpack/[fullhash].[runtime].hot-update.json' + : 'static/webpack/[hash].hot-update.json', + // This saves chunks with the name given via `import()` + chunkFilename: isServer + ? '[name].js' + : `static/chunks/${isDevFallback ? 'fallback/' : ''}${ + dev ? '[name]' : '[name].[contenthash]' + }.js`, + strictModuleExceptionHandling: true, + crossOriginLoading: crossOrigin, + futureEmitAssets: !dev, + webassemblyModuleFilename: 'static/wasm/[modulehash].wasm', + }, + performance: false, + resolve: resolveConfig, + resolveLoader: { + // The loaders Next.js provides + alias: [ + 'emit-file-loader', + 'error-loader', + 'next-babel-loader', + 'next-swc-loader', + 'next-client-pages-loader', + 'next-image-loader', + 'next-serverless-loader', + 'noop-loader', + 'next-style-loader', + ].reduce((alias, loader) => { + // using multiple aliases to replace `resolveLoader.modules` + alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader) + + return alias + }, {} as Record), + modules: [ + 'node_modules', + ...nodePathList, // Support for NODE_PATH environment variable + ], + plugins: isWebpack5 ? [] : [require('pnp-webpack-plugin')], + }, + module: { + rules: [ + ...(isWebpack5 + ? [ + // TODO: FIXME: do NOT webpack 5 support with this + // x-ref: https://github.com/webpack/webpack/issues/11467 + { + test: /\.m?js/, + resolve: { + fullySpecified: false, + }, + } as any, + ] + : []), + { + test: /\.(tsx|ts|js|mjs|jsx)$/, + ...(config.experimental.externalDir + ? // Allowing importing TS/TSX files from outside of the root dir. + {} + : { include: [dir, ...babelIncludeRegexes] }), + exclude: (excludePath: string) => { + if (babelIncludeRegexes.some((r) => r.test(excludePath))) { + return false + } + return /node_modules/.test(excludePath) + }, + use: hasReactRefresh + ? [ + require.resolve('@next/react-refresh-utils/loader'), + defaultLoaders.babel, + ] + : defaultLoaders.babel, + }, + ...(isServer + ? [] + : [ + // Because of packages/core/src/prisma-utils.ts + { + test: /[\\/]npm-which[\\/]/, + use: { loader: require.resolve('null-loader') }, + }, + { + test: /[\\/]cross-spawn[\\/]/, + use: { loader: require.resolve('null-loader') }, + }, + ]), + ...(!config.images.disableStaticImages && isWebpack5 + ? [ + { + test: /\.(png|jpg|jpeg|gif|webp|ico|bmp|svg)$/i, + loader: 'next-image-loader', + issuer: { not: regexLikeCss }, + dependency: { not: ['url'] }, + options: { + isServer, + isDev: dev, + assetPrefix: config.assetPrefix, + }, + }, + ] + : []), + ].filter(Boolean), + }, + plugins: [ + hasReactRefresh && new ReactRefreshWebpackPlugin(webpack), + // Makes sure `Buffer` and `process` are polyfilled in client-side bundles (same behavior as webpack 4) + isWebpack5 && + !isServer && + new webpack.ProvidePlugin({ + Buffer: [require.resolve('buffer'), 'Buffer'], + process: [require.resolve('process')], + }), + // This plugin makes sure `output.filename` is used for entry chunks + !isWebpack5 && new ChunkNamesPlugin(), + new webpack.DefinePlugin({ + ...Object.keys(process.env).reduce( + (prev: { [key: string]: string }, key: string) => { + if ( + key.startsWith('NEXT_PUBLIC_') || + key.startsWith('BLITZ_PUBLIC_') + ) { + prev[`process.env.${key}`] = JSON.stringify(process.env[key]!) + } + return prev + }, + {} + ), + ...Object.keys(config.env).reduce((acc, key) => { + if (/^(?:NODE_.+)|^(?:__.+)$/i.test(key)) { + throw new Error( + `The key "${key}" under "env" in blitz.config.js is not allowed. https://nextjs.org/docs/messages/env-key-not-allowed` + ) + } + + return { + ...acc, + [`process.env.${key}`]: JSON.stringify(config.env[key]), + } + }, {}), + // TODO: enforce `NODE_ENV` on `process.env`, and add a test: + 'process.env.NODE_ENV': JSON.stringify( + dev ? 'development' : 'production' + ), + 'process.env.__NEXT_CROSS_ORIGIN': JSON.stringify(crossOrigin), + 'process.browser': JSON.stringify(!isServer), + 'process.env.__NEXT_TEST_MODE': JSON.stringify( + process.env.__NEXT_TEST_MODE + ), + // This is used in client/dev-error-overlay/hot-dev-client.js to replace the dist directory + ...(dev && !isServer + ? { + 'process.env.__NEXT_DIST_DIR': JSON.stringify(distDir), + } + : {}), + 'process.env.__BLITZ_SUSPENSE_ENABLED': JSON.stringify(hasReactRoot), + 'process.env.__NEXT_TRAILING_SLASH': JSON.stringify( + config.trailingSlash + ), + 'process.env.__NEXT_BUILD_INDICATOR': JSON.stringify( + config.devIndicators.buildActivity + ), + 'process.env.__NEXT_PLUGINS': JSON.stringify( + config.experimental.plugins + ), + 'process.env.__NEXT_STRICT_MODE': JSON.stringify( + config.reactStrictMode + ), + 'process.env.__NEXT_REACT_ROOT': JSON.stringify(hasReactRoot), + 'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify( + config.optimizeFonts && !dev + ), + 'process.env.__NEXT_OPTIMIZE_IMAGES': JSON.stringify( + config.experimental.optimizeImages + ), + 'process.env.__NEXT_OPTIMIZE_CSS': JSON.stringify( + config.experimental.optimizeCss && !dev + ), + 'process.env.__NEXT_SCROLL_RESTORATION': JSON.stringify( + config.experimental.scrollRestoration + ), + 'process.env.__NEXT_IMAGE_OPTS': JSON.stringify({ + deviceSizes: config.images.deviceSizes, + imageSizes: config.images.imageSizes, + path: config.images.path, + loader: config.images.loader, + ...(dev + ? { + // pass domains in development to allow validating on the client + domains: config.images.domains, + } + : {}), + }), + 'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath), + 'process.env.__NEXT_HAS_REWRITES': JSON.stringify(hasRewrites), + 'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n), + 'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(config.i18n?.domains), + 'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId), + ...(isServer + ? { + // Fix bad-actors in the npm ecosystem (e.g. `node-formidable`) + // This is typically found in unmaintained modules from the + // pre-webpack era (common in server-side code) + 'global.GENTLY': JSON.stringify(false), + } + : { + 'process.env.__BLITZ_SESSION_COOKIE_PREFIX': JSON.stringify( + sessionCookiePrefix + ), + }), + // stub process.env with proxy to warn a missing value is + // being accessed in development mode + ...(config.experimental.pageEnv && dev + ? { + 'process.env': ` + new Proxy(${isServer ? 'process.env' : '{}'}, { + get(target, prop) { + if (typeof target[prop] === 'undefined') { + console.warn(\`An environment variable (\${prop}) that was not provided in the environment was accessed.\nSee more info here: https://nextjs.org/docs/messages/missing-env-value\`) + } + return target[prop] + } + }) + `, + } + : {}), + }), + !isServer && + new ReactLoadablePlugin({ + filename: REACT_LOADABLE_MANIFEST, + pagesDir, + }), + !isServer && new DropClientPage(), + // Moment.js is an extremely popular library that bundles large locale files + // by default due to how Webpack interprets its code. This is a practical + // solution that requires the user to opt into importing specific locales. + // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack + config.excludeDefaultMomentLocales && + new webpack.IgnorePlugin({ + resourceRegExp: /^\.\/locale$/, + contextRegExp: /moment$/, + }), + ...(dev + ? (() => { + // Even though require.cache is server only we have to clear assets from both compilations + // This is because the client compilation generates the build manifest that's used on the server side + const { + NextJsRequireCacheHotReloader, + } = require('./webpack/plugins/nextjs-require-cache-hot-reloader') + const devPlugins = [new NextJsRequireCacheHotReloader()] + + if (!isServer) { + devPlugins.push(new webpack.HotModuleReplacementPlugin()) + } + + return devPlugins + })() + : []), + // Webpack 5 no longer requires this plugin in production: + !isWebpack5 && !dev && new webpack.HashedModuleIdsPlugin(), + !dev && + new webpack.IgnorePlugin({ + resourceRegExp: /react-is/, + contextRegExp: /next[\\/]dist[\\/]/, + }), + isServerless && isServer && new ServerlessPlugin(), + isServer && + new PagesManifestPlugin({ serverless: isLikeServerless, dev }), + !isWebpack5 && + target === 'server' && + isServer && + new NextJsSSRModuleCachePlugin({ outputPath }), + isServer && new NextJsSsrImportPlugin(), + !isServer && + new BuildManifestPlugin({ + buildId, + rewrites, + isDevFallback, + }), + !dev && + !isServer && + config.experimental.stats && + new BuildStatsPlugin({ + distDir, + }), + new ProfilingPlugin(), + config.optimizeFonts && + !dev && + isServer && + (function () { + const { + FontStylesheetGatheringPlugin, + } = require('./webpack/plugins/font-stylesheet-gathering-plugin') as { + FontStylesheetGatheringPlugin: typeof import('./webpack/plugins/font-stylesheet-gathering-plugin').FontStylesheetGatheringPlugin + } + return new FontStylesheetGatheringPlugin({ + isLikeServerless, + }) + })(), + config.experimental.conformance && + !isWebpack5 && + !dev && + new WebpackConformancePlugin({ + tests: [ + !isServer && + conformanceConfig.MinificationConformanceCheck.enabled && + new MinificationConformanceCheck(), + conformanceConfig.ReactSyncScriptsConformanceCheck.enabled && + new ReactSyncScriptsConformanceCheck({ + AllowedSources: + conformanceConfig.ReactSyncScriptsConformanceCheck + .allowedSources || [], + }), + !isServer && + conformanceConfig.DuplicatePolyfillsConformanceCheck.enabled && + new DuplicatePolyfillsConformanceCheck({ + BlockedAPIToBePolyfilled: + conformanceConfig.DuplicatePolyfillsConformanceCheck + .BlockedAPIToBePolyfilled, + }), + !isServer && + conformanceConfig.GranularChunksConformanceCheck.enabled && + new GranularChunksConformanceCheck( + splitChunksConfigs.prodGranular + ), + ].filter(Boolean), + }), + new WellKnownErrorsPlugin(), + ].filter((Boolean as any) as ExcludesFalse), + } + + // Support tsconfig and jsconfig baseUrl + if (resolvedBaseUrl) { + webpackConfig.resolve?.modules?.push(resolvedBaseUrl) + } + + if (jsConfig?.compilerOptions?.paths && resolvedBaseUrl) { + webpackConfig.resolve?.plugins?.unshift( + new JsConfigPathsPlugin(jsConfig.compilerOptions.paths, resolvedBaseUrl) + ) + } + + if (isWebpack5) { + // futureEmitAssets is on by default in webpack 5 + delete webpackConfig.output?.futureEmitAssets + + if (isServer && dev) { + // Enable building of client compilation before server compilation in development + // @ts-ignore dependencies exists + webpackConfig.dependencies = ['client'] + } + // webpack 5 no longer polyfills Node.js modules: + if (webpackConfig.node) delete webpackConfig.node.setImmediate + + // Due to bundling of webpack the default values can't be correctly detected + // This restores the webpack defaults + // @ts-ignore webpack 5 + webpackConfig.snapshot = {} + if (process.versions.pnp === '3') { + const match = /^(.+?)[\\/]cache[\\/]jest-worker-npm-[^\\/]+\.zip[\\/]node_modules[\\/]/.exec( + require.resolve('jest-worker') + ) + if (match) { + // @ts-ignore webpack 5 + webpackConfig.snapshot.managedPaths = [ + path.resolve(match[1], 'unplugged'), + ] + } + } else { + const match = /^(.+?[\\/]node_modules)[\\/]/.exec( + require.resolve('jest-worker') + ) + if (match) { + // @ts-ignore webpack 5 + webpackConfig.snapshot.managedPaths = [match[1]] + } + } + if (process.versions.pnp === '1') { + const match = /^(.+?[\\/]v4)[\\/]npm-jest-worker-[^\\/]+-[\da-f]{40}[\\/]node_modules[\\/]/.exec( + require.resolve('jest-worker') + ) + if (match) { + // @ts-ignore webpack 5 + webpackConfig.snapshot.immutablePaths = [match[1]] + } + } else if (process.versions.pnp === '3') { + const match = /^(.+?)[\\/]jest-worker-npm-[^\\/]+\.zip[\\/]node_modules[\\/]/.exec( + require.resolve('jest-worker') + ) + if (match) { + // @ts-ignore webpack 5 + webpackConfig.snapshot.immutablePaths = [match[1]] + } + } + + if (dev) { + if (!webpackConfig.optimization) { + webpackConfig.optimization = {} + } + webpackConfig.optimization.providedExports = false + webpackConfig.optimization.usedExports = false + } + + const configVars = JSON.stringify({ + crossOrigin: config.crossOrigin, + pageExtensions: config.pageExtensions, + trailingSlash: config.trailingSlash, + buildActivity: config.devIndicators.buildActivity, + productionBrowserSourceMaps: !!config.productionBrowserSourceMaps, + plugins: config.experimental.plugins, + reactStrictMode: config.reactStrictMode, + reactMode: config.experimental.reactMode, + optimizeFonts: config.optimizeFonts, + optimizeImages: config.experimental.optimizeImages, + optimizeCss: config.experimental.optimizeCss, + scrollRestoration: config.experimental.scrollRestoration, + basePath: config.basePath, + pageEnv: config.experimental.pageEnv, + excludeDefaultMomentLocales: config.excludeDefaultMomentLocales, + assetPrefix: config.assetPrefix, + disableOptimizedLoading: config.experimental.disableOptimizedLoading, + target, + reactProductionProfiling, + webpack: !!config.webpack, + hasRewrites, + }) + + const cache: any = { + type: 'filesystem', + // Includes: + // - Next.js version + // - blitz.config.js keys that affect compilation + version: `${process.env.__NEXT_VERSION}|${configVars}`, + cacheDirectory: path.join(distDir, 'cache', 'webpack'), + } + + // TODO - can we remove this? + // Adds `blitz.config.js` as a buildDependency when custom webpack config is provided + if (config.webpack && config.configFile) { + cache.buildDependencies = { + config: [config.configFile], + } + } + + webpackConfig.cache = cache + + if (process.env.NEXT_WEBPACK_LOGGING) { + const logInfra = process.env.NEXT_WEBPACK_LOGGING.includes( + 'infrastructure' + ) + const logProfileClient = process.env.NEXT_WEBPACK_LOGGING.includes( + 'profile-client' + ) + const logProfileServer = process.env.NEXT_WEBPACK_LOGGING.includes( + 'profile-server' + ) + const logDefault = !logInfra && !logProfileClient && !logProfileServer + + if (logDefault || logInfra) { + // @ts-ignore TODO: remove ignore when webpack 5 is stable + webpackConfig.infrastructureLogging = { + level: 'verbose', + debug: /FileSystemInfo/, + } + } + + if ( + logDefault || + (logProfileClient && !isServer) || + (logProfileServer && isServer) + ) { + webpackConfig.plugins!.push((compiler: webpack.Compiler) => { + compiler.hooks.done.tap('next-webpack-logging', (stats) => { + console.log( + stats.toString({ + colors: true, + // @ts-ignore TODO: remove ignore when webpack 5 is stable + logging: logDefault ? 'log' : 'verbose', + }) + ) + }) + }) + } + + if ((logProfileClient && !isServer) || (logProfileServer && isServer)) { + webpackConfig.plugins!.push( + new webpack.ProgressPlugin({ + // @ts-ignore TODO: remove ignore when webpack 5 is stable + profile: true, + }) + ) + webpackConfig.profile = true + } + } + } + + webpackConfig = await buildConfiguration(webpackConfig, { + rootDirectory: dir, + customAppFile, + isDevelopment: dev, + isServer, + assetPrefix: config.assetPrefix || '', + sassOptions: config.sassOptions, + productionBrowserSourceMaps: config.productionBrowserSourceMaps, + future: config.future, + isCraCompat: config.experimental.craCompat, + }) + + let originalDevtool = webpackConfig.devtool + if (typeof config.webpack === 'function') { + webpackConfig = config.webpack(webpackConfig, { + dir, + dev, + isServer, + buildId, + config, + defaultLoaders, + totalPages, + webpack, + }) + + if (!webpackConfig) { + throw new Error( + 'Webpack config is undefined. You may have forgot to return properly from within the "webpack" method of your blitz.config.js.\n' + + 'See more info here https://nextjs.org/docs/messages/undefined-webpack-config' + ) + } + + if (dev && originalDevtool !== webpackConfig.devtool) { + webpackConfig.devtool = originalDevtool + devtoolRevertWarning(originalDevtool) + } + + if (typeof (webpackConfig as any).then === 'function') { + console.warn( + '> Promise returned in blitz config. https://nextjs.org/docs/messages/promise-in-next-config' + ) + } + } + + if (!config.images.disableStaticImages && isWebpack5) { + const rules = webpackConfig.module?.rules || [] + const hasCustomSvg = rules.some( + (rule) => + rule.loader !== 'next-image-loader' && + 'test' in rule && + rule.test instanceof RegExp && + rule.test.test('.svg') + ) + const nextImageRule = rules.find( + (rule) => rule.loader === 'next-image-loader' + ) + if (hasCustomSvg && nextImageRule) { + // Exclude svg if the user already defined it in custom + // webpack config such as `@svgr/webpack` plugin or + // the `babel-plugin-inline-react-svg` plugin. + nextImageRule.test = /\.(png|jpg|jpeg|gif|webp|ico|bmp)$/i + } + } + + if ( + config.experimental.craCompat && + webpackConfig.module?.rules && + webpackConfig.plugins + ) { + // CRA prevents loading all locales by default + // https://github.com/facebook/create-react-app/blob/fddce8a9e21bf68f37054586deb0c8636a45f50b/packages/react-scripts/config/webpack.config.js#L721 + webpackConfig.plugins.push( + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) + ) + + // CRA allows importing non-webpack handled files with file-loader + // these need to be the last rule to prevent catching other items + // https://github.com/facebook/create-react-app/blob/fddce8a9e21bf68f37054586deb0c8636a45f50b/packages/react-scripts/config/webpack.config.js#L594 + const fileLoaderExclude = [/\.(js|mjs|jsx|ts|tsx|json)$/] + const fileLoader = isWebpack5 + ? { + exclude: fileLoaderExclude, + issuer: fileLoaderExclude, + type: 'asset/resource', + generator: { + publicPath: '/_next/', + filename: 'static/media/[name].[hash:8].[ext]', + }, + } + : { + loader: require.resolve('next/dist/compiled/file-loader'), + // Exclude `js` files to keep "css" loader working as it injects + // its runtime that would otherwise be processed through "file" loader. + // Also exclude `html` and `json` extensions so they get processed + // by webpacks internal loaders. + exclude: fileLoaderExclude, + issuer: fileLoaderExclude, + options: { + publicPath: '/_next/static/media', + outputPath: 'static/media', + name: '[name].[hash:8].[ext]', + }, + } + + const topRules = [] + const innerRules = [] + + for (const rule of webpackConfig.module.rules) { + if (rule.resolve) { + topRules.push(rule) + } else { + if ( + rule.oneOf && + !(rule.test || rule.exclude || rule.resource || rule.issuer) + ) { + rule.oneOf.forEach((r) => innerRules.push(r)) + } else { + innerRules.push(rule) + } + } + } + + webpackConfig.module.rules = [ + ...(topRules as any), + { + oneOf: [...innerRules, fileLoader], + }, + ] + } + + // Backwards compat with webpack-dev-middleware options object + if (typeof config.webpackDevMiddleware === 'function') { + const options = config.webpackDevMiddleware({ + watchOptions: webpackConfig.watchOptions, + }) + if (options.watchOptions) { + webpackConfig.watchOptions = options.watchOptions + } + } + + function canMatchCss(rule: webpack.RuleSetCondition | undefined): boolean { + if (!rule) { + return false + } + + const fileNames = [ + '/tmp/test.css', + '/tmp/test.scss', + '/tmp/test.sass', + '/tmp/test.less', + '/tmp/test.styl', + ] + + if (rule instanceof RegExp && fileNames.some((input) => rule.test(input))) { + return true + } + + if (typeof rule === 'function') { + if ( + fileNames.some((input) => { + try { + if (rule(input)) { + return true + } + } catch (_) {} + return false + }) + ) { + return true + } + } + + if (Array.isArray(rule) && rule.some(canMatchCss)) { + return true + } + + return false + } + + const hasUserCssConfig = + webpackConfig.module?.rules.some( + (rule) => canMatchCss(rule.test) || canMatchCss(rule.include) + ) ?? false + + if (hasUserCssConfig) { + // only show warning for one build + if (isServer) { + console.warn( + chalk.yellow.bold('Warning: ') + + chalk.bold( + 'Built-in CSS support is being disabled due to custom CSS configuration being detected.\n' + ) + + 'See here for more info: https://nextjs.org/docs/messages/built-in-css-disabled\n' + ) + } + + if (webpackConfig.module?.rules.length) { + // Remove default CSS Loader + webpackConfig.module.rules = webpackConfig.module.rules.filter( + (r) => + !( + typeof r.oneOf?.[0]?.options === 'object' && + r.oneOf[0].options.__next_css_remove === true + ) + ) + } + if (webpackConfig.plugins?.length) { + // Disable CSS Extraction Plugin + webpackConfig.plugins = webpackConfig.plugins.filter( + (p) => (p as any).__next_css_remove !== true + ) + } + if (webpackConfig.optimization?.minimizer?.length) { + // Disable CSS Minifier + webpackConfig.optimization.minimizer = webpackConfig.optimization.minimizer.filter( + (e) => (e as any).__next_css_remove !== true + ) + } + } else if (!config.future.strictPostcssConfiguration) { + await __overrideCssConfiguration(dir, !dev, webpackConfig) + } + + // Inject missing React Refresh loaders so that development mode is fast: + if (hasReactRefresh) { + attachReactRefresh(webpackConfig, defaultLoaders.babel) + } + + // check if using @zeit/next-typescript and show warning + if ( + isServer && + webpackConfig.module && + Array.isArray(webpackConfig.module.rules) + ) { + let foundTsRule = false + + webpackConfig.module.rules = webpackConfig.module.rules.filter( + (rule): boolean => { + if (!(rule.test instanceof RegExp)) return true + if ('noop.ts'.match(rule.test) && !'noop.js'.match(rule.test)) { + // remove if it matches @zeit/next-typescript + foundTsRule = rule.use === defaultLoaders.babel + return !foundTsRule + } + return true + } + ) + + if (foundTsRule) { + console.warn( + '\n@zeit/next-typescript is no longer needed since Blitz.js has built-in support for TypeScript now. Please remove it from your blitz.config.js and your .babelrc\n' + ) + } + } + + // Patch `@zeit/next-sass`, `@zeit/next-less`, `@zeit/next-stylus` for compatibility + if (webpackConfig.module && Array.isArray(webpackConfig.module.rules)) { + ;[].forEach.call( + webpackConfig.module.rules, + function (rule: webpack.RuleSetRule) { + if (!(rule.test instanceof RegExp && Array.isArray(rule.use))) { + return + } + + const isSass = + rule.test.source === '\\.scss$' || rule.test.source === '\\.sass$' + const isLess = rule.test.source === '\\.less$' + const isCss = rule.test.source === '\\.css$' + const isStylus = rule.test.source === '\\.styl$' + + // Check if the rule we're iterating over applies to Sass, Less, or CSS + if (!(isSass || isLess || isCss || isStylus)) { + return + } + + ;[].forEach.call(rule.use, function (use: webpack.RuleSetUseItem) { + if ( + !( + use && + typeof use === 'object' && + // Identify use statements only pertaining to `css-loader` + (use.loader === 'css-loader' || + use.loader === 'css-loader/locals') && + use.options && + typeof use.options === 'object' && + // The `minimize` property is a good heuristic that we need to + // perform this hack. The `minimize` property was only valid on + // old `css-loader` versions. Custom setups (that aren't next-sass, + // next-less or next-stylus) likely have the newer version. + // We still handle this gracefully below. + (Object.prototype.hasOwnProperty.call(use.options, 'minimize') || + Object.prototype.hasOwnProperty.call( + use.options, + 'exportOnlyLocals' + )) + ) + ) { + return + } + + // Try to monkey patch within a try-catch. We shouldn't fail the build + // if we cannot pull this off. + // The user may not even be using the `next-sass` or `next-less` or + // `next-stylus` plugins. + // If it does work, great! + try { + // Resolve the version of `@zeit/next-css` as depended on by the Sass, + // Less or Stylus plugin. + const correctNextCss = require.resolve('@zeit/next-css', { + paths: [ + isCss + ? // Resolve `@zeit/next-css` from the base directory + dir + : // Else, resolve it from the specific plugins + require.resolve( + isSass + ? '@zeit/next-sass' + : isLess + ? '@zeit/next-less' + : isStylus + ? '@zeit/next-stylus' + : 'next' + ), + ], + }) + + // If we found `@zeit/next-css` ... + if (correctNextCss) { + // ... resolve the version of `css-loader` shipped with that + // package instead of whichever was hoisted highest in your + // `node_modules` tree. + const correctCssLoader = require.resolve(use.loader, { + paths: [correctNextCss], + }) + if (correctCssLoader) { + // We saved the user from a failed build! + use.loader = correctCssLoader + } + } + } catch (_) { + // The error is not required to be handled. + } + }) + } + ) + } + + // Backwards compat for `main.js` entry key + // and setup of dependencies between entries + // we can't do that in the initial entry for + // backward-compat reasons + const originalEntry: any = webpackConfig.entry + if (typeof originalEntry !== 'undefined') { + const updatedEntry = async () => { + const entry: WebpackEntrypoints = + typeof originalEntry === 'function' + ? await originalEntry() + : originalEntry + // Server compilation doesn't have main.js + if ( + clientEntries && + Array.isArray(entry['main.js']) && + entry['main.js'].length > 0 + ) { + const originalFile = clientEntries[ + CLIENT_STATIC_FILES_RUNTIME_MAIN + ] as string + entry[CLIENT_STATIC_FILES_RUNTIME_MAIN] = [ + ...entry['main.js'], + originalFile, + ] + } + delete entry['main.js'] + + if (isWebpack5 && !isServer) { + for (const name of Object.keys(entry)) { + if ( + name === 'polyfills' || + name === 'main' || + name === 'amp' || + name === 'react-refresh' + ) + continue + const dependOn = + name.startsWith('pages/') && name !== 'pages/_app' + ? 'pages/_app' + : 'main' + const old = entry[name] + if (typeof old === 'object' && !Array.isArray(old)) { + entry[name] = { + dependOn, + ...old, + } + } else { + entry[name] = { + import: old, + dependOn, + } + } + } + } + + return entry + } + // @ts-ignore webpack 5 typings needed + webpackConfig.entry = updatedEntry + } + + if (!dev) { + // entry is always a function + webpackConfig.entry = await (webpackConfig.entry as webpack.EntryFunc)() + } + + return webpackConfig +} diff --git a/nextjs/packages/next/build/webpack/config/blocks/base.ts b/nextjs/packages/next/build/webpack/config/blocks/base.ts new file mode 100644 index 0000000000..466cafaaea --- /dev/null +++ b/nextjs/packages/next/build/webpack/config/blocks/base.ts @@ -0,0 +1,59 @@ +import isWslBoolean from 'next/dist/compiled/is-wsl' +import curry from 'next/dist/compiled/lodash.curry' +import { webpack, isWebpack5 } from 'next/dist/compiled/webpack/webpack' +import { ConfigurationContext } from '../utils' + +const isWindows = process.platform === 'win32' || isWslBoolean + +export const base = curry(function base( + ctx: ConfigurationContext, + config: webpack.Configuration +) { + config.mode = ctx.isDevelopment ? 'development' : 'production' + config.name = ctx.isServer ? 'server' : 'client' + if (isWebpack5) { + // @ts-ignore TODO webpack 5 typings + config.target = ctx.isServer ? 'node12.17' : ['web', 'es5'] + } else { + config.target = ctx.isServer ? 'node' : 'web' + } + + // Stop compilation early in a production build when an error is encountered. + // This behavior isn't desirable in development due to how the HMR system + // works, but is a good default for production. + config.bail = ctx.isProduction + + // https://webpack.js.org/configuration/devtool/#development + if (ctx.isDevelopment) { + if (process.env.__NEXT_TEST_MODE && !process.env.__NEXT_TEST_WITH_DEVTOOL) { + config.devtool = false + } else if (isWindows) { + // Non-eval based source maps are slow to rebuild, so we only enable + // them for Windows. Unfortunately, eval source maps are flagged as + // suspicious by Windows Defender and block HMR. + config.devtool = 'inline-source-map' + } else { + // `eval-source-map` provides full-fidelity source maps for the + // original source, including columns and original variable names. + // This is desirable so the in-browser debugger can correctly pause + // and show scoped variables with their original names. + config.devtool = 'eval-source-map' + } + } else { + // Enable browser sourcemaps: + if (ctx.productionBrowserSourceMaps && ctx.isClient) { + config.devtool = 'source-map' + } else { + config.devtool = false + } + } + + if (!config.module) { + config.module = { rules: [] } + } + + // TODO: add codemod for "Should not import the named export" with JSON files + config.module.strictExportPresence = !isWebpack5 + + return config +}) diff --git a/nextjs/packages/next/build/webpack/config/blocks/css/index.ts b/nextjs/packages/next/build/webpack/config/blocks/css/index.ts new file mode 100644 index 0000000000..02f44cf5c7 --- /dev/null +++ b/nextjs/packages/next/build/webpack/config/blocks/css/index.ts @@ -0,0 +1,348 @@ +import curry from 'next/dist/compiled/lodash.curry' +import path from 'path' +import { webpack, isWebpack5 } from 'next/dist/compiled/webpack/webpack' +import MiniCssExtractPlugin from '../../../plugins/mini-css-extract-plugin' +import { loader, plugin } from '../../helpers' +import { ConfigurationContext, ConfigurationFn, pipe } from '../../utils' +import { getCssModuleLoader, getGlobalCssLoader } from './loaders' +import { + getCustomDocumentError, + getGlobalImportError, + getGlobalModuleImportError, + getLocalModuleImportError, +} from './messages' +import { getPostCssPlugins } from './plugins' + +// RegExps for all Style Sheet variants +export const regexLikeCss = /\.(css|scss|sass)(\.webpack\[javascript\/auto\])?$/ + +// RegExps for Style Sheets +const regexCssGlobal = /(? file + fns.push( + loader({ + oneOf: [ + { + test: [regexCssGlobal, regexSassGlobal], + use: { + loader: 'error-loader', + options: { + reason: getGlobalImportError( + ctx.customAppFile && + path.relative(ctx.rootDirectory, ctx.customAppFile) + ), + }, + }, + }, + ], + }) + ) + + if (ctx.isClient) { + // Automatically transform references to files (i.e. url()) into URLs + // e.g. url(./logo.svg) + fns.push( + loader({ + oneOf: [ + { + // This should only be applied to CSS files + issuer: regexLikeCss, + // Exclude extensions that webpack handles by default + exclude: [ + /\.(js|mjs|jsx|ts|tsx)$/, + /\.html$/, + /\.json$/, + /\.webpack\[[^\]]+\]$/, + ], + use: { + // `file-loader` always emits a URL reference, where `url-loader` + // might inline the asset as a data URI + loader: require.resolve('next/dist/compiled/file-loader'), + options: { + // Hash the file for immutable cacheability + name: 'static/media/[name].[hash].[ext]', + }, + }, + }, + ], + }) + ) + } + + if (ctx.isClient && ctx.isProduction) { + // Extract CSS as CSS file(s) in the client-side production bundle. + fns.push( + plugin( + // @ts-ignore webpack 5 compat + new MiniCssExtractPlugin({ + experimentalUseImportModule: isWebpack5, + filename: 'static/css/[contenthash].css', + chunkFilename: 'static/css/[contenthash].css', + // Next.js guarantees that CSS order "doesn't matter", due to imposed + // restrictions: + // 1. Global CSS can only be defined in a single entrypoint (_app) + // 2. CSS Modules generate scoped class names by default and cannot + // include Global CSS (:global() selector). + // + // While not a perfect guarantee (e.g. liberal use of `:global()` + // selector), this assumption is required to code-split CSS. + // + // If this warning were to trigger, it'd be unactionable by the user, + // but likely not valid -- so we disable it. + ignoreOrder: true, + }) + ) + ) + } + + const fn = pipe(...fns) + return fn(config) +}) diff --git a/nextjs/packages/next/build/webpack/config/blocks/css/loaders/client.ts b/nextjs/packages/next/build/webpack/config/blocks/css/loaders/client.ts new file mode 100644 index 0000000000..43436a53b2 --- /dev/null +++ b/nextjs/packages/next/build/webpack/config/blocks/css/loaders/client.ts @@ -0,0 +1,40 @@ +import { webpack } from 'next/dist/compiled/webpack/webpack' +import MiniCssExtractPlugin from '../../../../plugins/mini-css-extract-plugin' + +export function getClientStyleLoader({ + isDevelopment, + assetPrefix, +}: { + isDevelopment: boolean + assetPrefix: string +}): webpack.RuleSetUseItem { + return isDevelopment + ? { + loader: 'next-style-loader', + options: { + // By default, style-loader injects CSS into the bottom + // of . This causes ordering problems between dev + // and prod. To fix this, we render a