From 7f952ce163b20175e96196db88bba86631808e4c Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 22 Nov 2024 18:50:13 -0800 Subject: [PATCH] Search suggestions (#85) The motivation for building search suggestions is two-fold: (1) to make the zoekt query language more approachable by presenting all available options to the user, and (2) make it easier for power-users to craft complex queries. The meat-n-potatoes of this change are concentrated in searchBar.tsx and searchSuggestionBox.tsx. The suggestions box works by maintaining a state-machine of "modes". By default, the box is in the refine mode, where suggestions for different prefixes (e.g., repo:, lang:, etc.) are suggested to the user. When one of these prefixes is matched, the state-machine transitions to the corresponding mode (e.g., repository, language, etc.) and surfaces suggestions for that mode (if any). The query is split up into parts by spaces " " (e.g., 'test repo:hello' -> ['test', 'repo:hello']). See splitQuery. The part that has the cursor over it is considered the active part. We evaluate which mode the state machine is in based on the active part. When a suggestion is clicked, we only modify the active part of the query. Three modes are currently missing suggestion data: file (file names), revision (branch / tag names), and symbol (symbol names). In future PRs, we will need to introduce endpoints into the backend to allow the frontend to fetch this data and surface it as suggestions.. --- .github/workflows/test-web.yml | 28 + .vscode/extensions.json | 3 +- .vscode/settings.json | 11 + package.json | 2 +- packages/web/package.json | 8 +- .../app/{ => components}/navigationMenu.tsx | 4 +- .../{ => components}/repositoryCarousel.tsx | 0 .../src/app/components/searchBar/constants.ts | 224 ++++++ .../web/src/app/components/searchBar/index.ts | 2 + .../src/app/components/searchBar/languages.ts | 712 ++++++++++++++++++ .../app/components/searchBar/searchBar.tsx | 292 +++++++ .../searchBar/searchSuggestionsBox.test.tsx | 221 ++++++ .../searchBar/searchSuggestionsBox.tsx | 453 +++++++++++ .../searchBar/zoektLanguageExtension.ts | 25 + .../app/{ => components}/settingsDropdown.tsx | 0 .../src/app/{ => components}/upgradeToast.tsx | 0 packages/web/src/app/page.tsx | 23 +- packages/web/src/app/repos/page.tsx | 2 +- .../search/components/filterPanel/index.tsx | 1 - packages/web/src/app/search/page.tsx | 7 +- packages/web/src/app/searchBar.tsx | 178 ----- packages/web/src/hooks/useClickListener.ts | 24 + packages/web/src/hooks/useTailwind.ts | 2 +- packages/web/src/lib/server/searchService.ts | 2 +- packages/web/src/lib/utils.ts | 5 + packages/web/vitest.config.mts | 10 + yarn.lock | 337 ++++++++- 27 files changed, 2365 insertions(+), 211 deletions(-) create mode 100644 .github/workflows/test-web.yml rename packages/web/src/app/{ => components}/navigationMenu.tsx (96%) rename packages/web/src/app/{ => components}/repositoryCarousel.tsx (100%) create mode 100644 packages/web/src/app/components/searchBar/constants.ts create mode 100644 packages/web/src/app/components/searchBar/index.ts create mode 100644 packages/web/src/app/components/searchBar/languages.ts create mode 100644 packages/web/src/app/components/searchBar/searchBar.tsx create mode 100644 packages/web/src/app/components/searchBar/searchSuggestionsBox.test.tsx create mode 100644 packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx create mode 100644 packages/web/src/app/components/searchBar/zoektLanguageExtension.ts rename packages/web/src/app/{ => components}/settingsDropdown.tsx (100%) rename packages/web/src/app/{ => components}/upgradeToast.tsx (100%) delete mode 100644 packages/web/src/app/searchBar.tsx create mode 100644 packages/web/src/hooks/useClickListener.ts create mode 100644 packages/web/vitest.config.mts diff --git a/.github/workflows/test-web.yml b/.github/workflows/test-web.yml new file mode 100644 index 00000000..108cc277 --- /dev/null +++ b/.github/workflows/test-web.yml @@ -0,0 +1,28 @@ +name: Test Web + +on: + pull_request: + branches: ["main"] + + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: "true" + - name: Use Node.Js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Install + run: yarn install --frozen-lockfile + + - name: Test + run: yarn workspace @sourcebot/web test + diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 897af65d..6347d9d7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "bradlc.vscode-tailwindcss" ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 463b90ca..f753e389 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,16 @@ { "pattern": "./packages/*/" } + ], + // @see : https://cva.style/docs/getting-started/installation#intellisense + "tailwindCSS.experimental.classRegex": [ + [ + "cva\\(([^)]*)\\)", + "[\"'`]([^\"'`]*).*?[\"'`]" + ], + [ + "cx\\(([^)]*)\\)", + "(?:'|\"|`)([^']*)(?:'|\"|`)" + ] ] } \ No newline at end of file diff --git a/package.json b/package.json index 3e0fbd87..a14244a6 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ ], "scripts": { "build": "yarn workspaces run build", - "test": "yarn workspace @sourcebot/backend test", + "test": "yarn workspaces run test", "dev": "npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web", "dev:zoekt": "export PATH=\"$PWD/bin:$PATH\" && zoekt-webserver -index .sourcebot/index -rpc", "dev:backend": "yarn workspace @sourcebot/backend dev:watch", diff --git a/packages/web/package.json b/packages/web/package.json index 4fc540bf..81ae488d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test": "vitest" }, "dependencies": { "@codemirror/commands": "^6.6.0", @@ -77,9 +78,12 @@ "eslint-config-next": "14.2.6", "eslint-plugin-react": "^7.35.0", "eslint-plugin-react-hooks": "^4.6.2", + "jsdom": "^25.0.1", "npm-run-all": "^4.1.5", "postcss": "^8", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "vite-tsconfig-paths": "^5.1.3", + "vitest": "^2.1.5" } } diff --git a/packages/web/src/app/navigationMenu.tsx b/packages/web/src/app/components/navigationMenu.tsx similarity index 96% rename from packages/web/src/app/navigationMenu.tsx rename to packages/web/src/app/components/navigationMenu.tsx index 0beb2d8d..229b7713 100644 --- a/packages/web/src/app/navigationMenu.tsx +++ b/packages/web/src/app/components/navigationMenu.tsx @@ -7,8 +7,8 @@ import { GitHubLogoIcon, DiscordLogoIcon } from "@radix-ui/react-icons"; import { SettingsDropdown } from "./settingsDropdown"; import { Separator } from "@/components/ui/separator"; import Image from "next/image"; -import logoDark from "../../public/sb_logo_dark_small.png"; -import logoLight from "../../public/sb_logo_light_small.png"; +import logoDark from "../../../public/sb_logo_dark_small.png"; +import logoLight from "../../../public/sb_logo_light_small.png"; import { useRouter } from "next/navigation"; const SOURCEBOT_DISCORD_URL = "https://discord.gg/6Fhp27x7Pb"; diff --git a/packages/web/src/app/repositoryCarousel.tsx b/packages/web/src/app/components/repositoryCarousel.tsx similarity index 100% rename from packages/web/src/app/repositoryCarousel.tsx rename to packages/web/src/app/components/repositoryCarousel.tsx diff --git a/packages/web/src/app/components/searchBar/constants.ts b/packages/web/src/app/components/searchBar/constants.ts new file mode 100644 index 00000000..e08a03fe --- /dev/null +++ b/packages/web/src/app/components/searchBar/constants.ts @@ -0,0 +1,224 @@ +import { Suggestion, SuggestionMode } from "./searchSuggestionsBox"; + +/** + * List of search prefixes that can be used while the + * `refine` suggestion mode is active. + */ +enum SearchPrefix { + repo = "repo:", + r = "r:", + lang = "lang:", + file = "file:", + rev = "rev:", + revision = "revision:", + b = "b:", + branch = "branch:", + sym = "sym:", + content = "content:", + archived = "archived:", + case = "case:", + fork = "fork:", + public = "public:" +} + +const negate = (prefix: SearchPrefix) => { + return `-${prefix}`; +} + +type SuggestionModeMapping = { + suggestionMode: SuggestionMode, + prefixes: string[], +} + +/** + * Maps search prefixes to a suggestion mode. When a query starts + * with a prefix, the corresponding suggestion mode is enabled. + * @see [searchSuggestionsBox.tsx](./searchSuggestionsBox.tsx) + */ +export const suggestionModeMappings: SuggestionModeMapping[] = [ + { + suggestionMode: "repo", + prefixes: [ + SearchPrefix.repo, negate(SearchPrefix.repo), + SearchPrefix.r, negate(SearchPrefix.r), + ] + }, + { + suggestionMode: "language", + prefixes: [ + SearchPrefix.lang, negate(SearchPrefix.lang), + ] + }, + { + suggestionMode: "file", + prefixes: [ + SearchPrefix.file, negate(SearchPrefix.file), + ] + }, + { + suggestionMode: "content", + prefixes: [ + SearchPrefix.content, negate(SearchPrefix.content), + ] + }, + { + suggestionMode: "revision", + prefixes: [ + SearchPrefix.rev, negate(SearchPrefix.rev), + SearchPrefix.revision, negate(SearchPrefix.revision), + SearchPrefix.branch, negate(SearchPrefix.branch), + SearchPrefix.b, negate(SearchPrefix.b), + ] + }, + { + suggestionMode: "symbol", + prefixes: [ + SearchPrefix.sym, negate(SearchPrefix.sym), + ] + }, + { + suggestionMode: "archived", + prefixes: [ + SearchPrefix.archived + ] + }, + { + suggestionMode: "case", + prefixes: [ + SearchPrefix.case + ] + }, + { + suggestionMode: "fork", + prefixes: [ + SearchPrefix.fork + ] + }, + { + suggestionMode: "public", + prefixes: [ + SearchPrefix.public + ] + } +]; + +export const refineModeSuggestions: Suggestion[] = [ + { + value: SearchPrefix.repo, + description: "Include only results from the given repository.", + spotlight: true, + }, + { + value: negate(SearchPrefix.repo), + description: "Exclude results from the given repository." + }, + { + value: SearchPrefix.lang, + description: "Include only results from the given language.", + spotlight: true, + }, + { + value: negate(SearchPrefix.lang), + description: "Exclude results from the given language." + }, + { + value: SearchPrefix.file, + description: "Include only results from filepaths matching the given search pattern.", + spotlight: true, + }, + { + value: negate(SearchPrefix.file), + description: "Exclude results from file paths matching the given search pattern." + }, + { + value: SearchPrefix.rev, + description: "Search a given branch or tag instead of the default branch.", + spotlight: true, + }, + { + value: negate(SearchPrefix.rev), + description: "Exclude results from the given branch or tag." + }, + { + value: SearchPrefix.sym, + description: "Include only symbols matching the given search pattern.", + spotlight: true, + }, + { + value: negate(SearchPrefix.sym), + description: "Exclude results from symbols matching the given search pattern." + }, + { + value: SearchPrefix.content, + description: "Include only results from files if their content matches the given search pattern." + }, + { + value: negate(SearchPrefix.content), + description: "Exclude results from files if their content matches the given search pattern." + }, + { + value: SearchPrefix.archived, + description: "Include results from archived repositories.", + }, + { + value: SearchPrefix.case, + description: "Control case-sensitivity of search patterns." + }, + { + value: SearchPrefix.fork, + description: "Include only results from forked repositories." + }, + { + value: SearchPrefix.public, + description: "Filter on repository visibility." + }, +]; + +export const publicModeSuggestions: Suggestion[] = [ + { + value: "yes", + description: "Only include results from public repositories." + }, + { + value: "no", + description: "Only include results from private repositories." + }, +]; + +export const forkModeSuggestions: Suggestion[] = [ + { + value: "yes", + description: "Only include results from forked repositories." + }, + { + value: "no", + description: "Only include results from non-forked repositories." + }, +]; + +export const caseModeSuggestions: Suggestion[] = [ + { + value: "auto", + description: "Search patterns are case-insensitive if all characters are lowercase, and case sensitive otherwise (default)." + }, + { + value: "yes", + description: "Case sensitive search." + }, + { + value: "no", + description: "Case insensitive search." + }, +]; + +export const archivedModeSuggestions: Suggestion[] = [ + { + value: "yes", + description: "Only include results in archived repositories." + }, + { + value: "no", + description: "Only include results in non-archived repositories." + }, +]; + diff --git a/packages/web/src/app/components/searchBar/index.ts b/packages/web/src/app/components/searchBar/index.ts new file mode 100644 index 00000000..835bb099 --- /dev/null +++ b/packages/web/src/app/components/searchBar/index.ts @@ -0,0 +1,2 @@ + +export { SearchBar } from "./searchBar"; \ No newline at end of file diff --git a/packages/web/src/app/components/searchBar/languages.ts b/packages/web/src/app/components/searchBar/languages.ts new file mode 100644 index 00000000..cfa3c49c --- /dev/null +++ b/packages/web/src/app/components/searchBar/languages.ts @@ -0,0 +1,712 @@ + +// From https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml +const languages = [ + "1C Enterprise", + "2-Dimensional Array", + "4D", + "ABAP", + "ABAP CDS", + "ABNF", + "AGS Script", + "AIDL", + "AL", + "AMPL", + "ANTLR", + "API Blueprint", + "APL", + "ASL", + "ASN.1", + "ASP.NET", + "ATS", + "ActionScript", + "Ada", + "Adblock Filter List", + "Adobe Font Metrics", + "Agda", + "Alloy", + "Alpine Abuild", + "Altium Designer", + "AngelScript", + "Ant Build System", + "Antlers", + "ApacheConf", + "Apex", + "Apollo Guidance Computer", + "AppleScript", + "Arc", + "AsciiDoc", + "AspectJ", + "Assembly", + "Astro", + "Asymptote", + "Augeas", + "AutoHotkey", + "AutoIt", + "Avro IDL", + "Awk", + "BASIC", + "Ballerina", + "Batchfile", + "Beef", + "Befunge", + "Berry", + "BibTeX", + "Bicep", + "Bikeshed", + "Bison", + "BitBake", + "Blade", + "BlitzBasic", + "BlitzMax", + "Bluespec", + "Bluespec BH", + "Boo", + "Boogie", + "Brainfuck", + "BrighterScript", + "Brightscript", + "Browserslist", + "C", + "C#", + "C++", + "C-ObjDump", + "C2hs Haskell", + "CAP CDS", + "CIL", + "CLIPS", + "CMake", + "COBOL", + "CODEOWNERS", + "COLLADA", + "CSON", + "CSS", + "CSV", + "CUE", + "CWeb", + "Cabal Config", + "Cadence", + "Cairo", + "CameLIGO", + "Cap'n Proto", + "CartoCSS", + "Ceylon", + "Chapel", + "Charity", + "Checksums", + "ChucK", + "Circom", + "Cirru", + "Clarion", + "Clarity", + "Classic ASP", + "Clean", + "Click", + "Clojure", + "Closure Templates", + "Cloud Firestore Security Rules", + "CoNLL-U", + "CodeQL", + "CoffeeScript", + "ColdFusion", + "ColdFusion CFC", + "Common Lisp", + "Common Workflow Language", + "Component Pascal", + "Cool", + "Coq", + "Cpp-ObjDump", + "Creole", + "Crystal", + "Csound", + "Csound Document", + "Csound Score", + "Cuda", + "Cue Sheet", + "Curry", + "Cycript", + "Cypher", + "Cython", + "D", + "D-ObjDump", + "D2", + "DIGITAL Command Language", + "DM", + "DNS Zone", + "DTrace", + "Dafny", + "Darcs Patch", + "Dart", + "DataWeave", + "Debian Package Control File", + "DenizenScript", + "Dhall", + "Diff", + "DirectX 3D File", + "Dockerfile", + "Dogescript", + "Dotenv", + "Dylan", + "E", + "E-mail", + "EBNF", + "ECL", + "ECLiPSe", + "EJS", + "EQ", + "Eagle", + "Earthly", + "Easybuild", + "Ecere Projects", + "Ecmarkup", + "EditorConfig", + "Edje Data Collection", + "Eiffel", + "Elixir", + "Elm", + "Elvish", + "Elvish Transcript", + "Emacs Lisp", + "EmberScript", + "Erlang", + "Euphoria", + "F#", + "F*", + "FIGlet Font", + "FLUX", + "Factor", + "Fancy", + "Fantom", + "Faust", + "Fennel", + "Filebench WML", + "Filterscript", + "Fluent", + "Formatted", + "Forth", + "Fortran", + "Fortran Free Form", + "FreeBasic", + "FreeMarker", + "Frege", + "Futhark", + "G-code", + "GAML", + "GAMS", + "GAP", + "GCC Machine Description", + "GDB", + "GDScript", + "GEDCOM", + "GLSL", + "GN", + "GSC", + "Game Maker Language", + "Gemfile.lock", + "Gemini", + "Genero", + "Genero Forms", + "Genie", + "Genshi", + "Gentoo Ebuild", + "Gentoo Eclass", + "Gerber Image", + "Gettext Catalog", + "Gherkin", + "Git Attributes", + "Git Config", + "Git Revision List", + "Gleam", + "Glyph", + "Glyph Bitmap Distribution Format", + "Gnuplot", + "Go", + "Go Checksums", + "Go Module", + "Go Workspace", + "Godot Resource", + "Golo", + "Gosu", + "Grace", + "Gradle", + "Gradle Kotlin DSL", + "Grammatical Framework", + "Graph Modeling Language", + "GraphQL", + "Graphviz (DOT)", + "Groovy", + "Groovy Server Pages", + "HAProxy", + "HCL", + "HLSL", + "HOCON", + "HTML", + "HTML+ECR", + "HTML+EEX", + "HTML+ERB", + "HTML+PHP", + "HTML+Razor", + "HTTP", + "HXML", + "Hack", + "Haml", + "Handlebars", + "Harbour", + "Haskell", + "Haxe", + "HiveQL", + "HolyC", + "Hosts File", + "Hy", + "HyPhy", + "IDL", + "IGOR Pro", + "INI", + "IRC log", + "Idris", + "Ignore List", + "ImageJ Macro", + "Imba", + "Inform 7", + "Ink", + "Inno Setup", + "Io", + "Ioke", + "Isabelle", + "Isabelle ROOT", + "J", + "JAR Manifest", + "JCL", + "JFlex", + "JSON", + "JSON with Comments", + "JSON5", + "JSONLD", + "JSONiq", + "Janet", + "Jasmin", + "Java", + "Java Properties", + "Java Server Pages", + "JavaScript", + "JavaScript+ERB", + "Jest Snapshot", + "JetBrains MPS", + "Jinja", + "Jison", + "Jison Lex", + "Jolie", + "Jsonnet", + "Julia", + "Jupyter Notebook", + "Just", + "KRL", + "Kaitai Struct", + "KakouneScript", + "KerboScript", + "KiCad Layout", + "KiCad Legacy Layout", + "KiCad Schematic", + "Kickstart", + "Kit", + "Kotlin", + "Kusto", + "LFE", + "LLVM", + "LOLCODE", + "LSL", + "LTspice Symbol", + "LabVIEW", + "Lark", + "Lasso", + "Latte", + "Lean", + "Less", + "Lex", + "LigoLANG", + "LilyPond", + "Limbo", + "Linker Script", + "Linux Kernel Module", + "Liquid", + "Literate Agda", + "Literate CoffeeScript", + "Literate Haskell", + "LiveScript", + "Logos", + "Logtalk", + "LookML", + "LoomScript", + "Lua", + "M", + "M4", + "M4Sugar", + "MATLAB", + "MAXScript", + "MDX", + "MLIR", + "MQL4", + "MQL5", + "MTML", + "MUF", + "Macaulay2", + "Makefile", + "Mako", + "Markdown", + "Marko", + "Mask", + "Mathematica", + "Maven POM", + "Max", + "Mercury", + "Mermaid", + "Meson", + "Metal", + "Microsoft Developer Studio Project", + "Microsoft Visual Studio Solution", + "MiniD", + "MiniYAML", + "Mint", + "Mirah", + "Modelica", + "Modula-2", + "Modula-3", + "Module Management System", + "Monkey", + "Monkey C", + "Moocode", + "MoonScript", + "Motoko", + "Motorola 68K Assembly", + "Move", + "Muse", + "Mustache", + "Myghty", + "NASL", + "NCL", + "NEON", + "NL", + "NPM Config", + "NSIS", + "NWScript", + "Nasal", + "Nearley", + "Nemerle", + "NetLinx", + "NetLinx+ERB", + "NetLogo", + "NewLisp", + "Nextflow", + "Nginx", + "Nim", + "Ninja", + "Nit", + "Nix", + "Nu", + "NumPy", + "Nunjucks", + "Nushell", + "OASv2-json", + "OASv2-yaml", + "OASv3-json", + "OASv3-yaml", + "OCaml", + "ObjDump", + "Object Data Instance Notation", + "ObjectScript", + "Objective-C", + "Objective-C++", + "Objective-J", + "Odin", + "Omgrofl", + "Opa", + "Opal", + "Open Policy Agent", + "OpenAPI Specification v2", + "OpenAPI Specification v3", + "OpenCL", + "OpenEdge ABL", + "OpenQASM", + "OpenRC runscript", + "OpenSCAD", + "OpenStep Property List", + "OpenType Feature File", + "Option List", + "Org", + "Ox", + "Oxygene", + "Oz", + "P4", + "PDDL", + "PEG.js", + "PHP", + "PLSQL", + "PLpgSQL", + "POV-Ray SDL", + "Pact", + "Pan", + "Papyrus", + "Parrot", + "Parrot Assembly", + "Parrot Internal Representation", + "Pascal", + "Pawn", + "Pep8", + "Perl", + "Pic", + "Pickle", + "PicoLisp", + "PigLatin", + "Pike", + "PlantUML", + "Pod", + "Pod 6", + "PogoScript", + "Polar", + "Pony", + "Portugol", + "PostCSS", + "PostScript", + "PowerBuilder", + "PowerShell", + "Prisma", + "Processing", + "Procfile", + "Proguard", + "Prolog", + "Promela", + "Propeller Spin", + "Protocol Buffer", + "Protocol Buffer Text Format", + "Public Key", + "Pug", + "Puppet", + "Pure Data", + "PureBasic", + "PureScript", + "Pyret", + "Python", + "Python console", + "Python traceback", + "Q#", + "QML", + "QMake", + "Qt Script", + "Quake", + "R", + "RAML", + "RBS", + "RDoc", + "REALbasic", + "REXX", + "RMarkdown", + "RPC", + "RPGLE", + "RPM Spec", + "RUNOFF", + "Racket", + "Ragel", + "Raku", + "Rascal", + "Raw token data", + "ReScript", + "Readline Config", + "Reason", + "ReasonLIGO", + "Rebol", + "Record Jar", + "Red", + "Redcode", + "Redirect Rules", + "Regular Expression", + "Ren'Py", + "RenderScript", + "Rez", + "Rich Text Format", + "Ring", + "Riot", + "RobotFramework", + "Roff", + "Roff Manpage", + "Rouge", + "RouterOS Script", + "Ruby", + "Rust", + "SAS", + "SCSS", + "SELinux Policy", + "SMT", + "SPARQL", + "SQF", + "SQL", + "SQLPL", + "SRecode Template", + "SSH Config", + "STAR", + "STL", + "STON", + "SVG", + "SWIG", + "Sage", + "SaltStack", + "Sass", + "Scala", + "Scaml", + "Scenic", + "Scheme", + "Scilab", + "Self", + "ShaderLab", + "Shell", + "ShellCheck Config", + "ShellSession", + "Shen", + "Sieve", + "Simple File Verification", + "Singularity", + "Slash", + "Slice", + "Slim", + "SmPL", + "Smali", + "Smalltalk", + "Smarty", + "Smithy", + "Snakemake", + "Solidity", + "Soong", + "SourcePawn", + "Spline Font Database", + "Squirrel", + "Stan", + "Standard ML", + "Starlark", + "Stata", + "StringTemplate", + "Stylus", + "SubRip Text", + "SugarSS", + "SuperCollider", + "Svelte", + "Sway", + "Sweave", + "Swift", + "SystemVerilog", + "TI Program", + "TL-Verilog", + "TLA", + "TOML", + "TSQL", + "TSV", + "TSX", + "TXL", + "Talon", + "Tcl", + "Tcsh", + "TeX", + "Tea", + "Terra", + "Texinfo", + "Text", + "TextMate Properties", + "Textile", + "Thrift", + "Turing", + "Turtle", + "Twig", + "Type Language", + "TypeScript", + "Typst", + "Unified Parallel C", + "Unity3D Asset", + "Unix Assembly", + "Uno", + "UnrealScript", + "UrWeb", + "V", + "VBA", + "VBScript", + "VCL", + "VHDL", + "Vala", + "Valve Data Format", + "Velocity Template Language", + "Verilog", + "Vim Help File", + "Vim Script", + "Vim Snippet", + "Visual Basic .NET", + "Visual Basic 6.0", + "Volt", + "Vue", + "Vyper", + "WDL", + "WGSL", + "Wavefront Material", + "Wavefront Object", + "Web Ontology Language", + "WebAssembly", + "WebAssembly Interface Type", + "WebIDL", + "WebVTT", + "Wget Config", + "Whiley", + "Wikitext", + "Win32 Message File", + "Windows Registry Entries", + "Witcher Script", + "Wollok", + "World of Warcraft Addon Data", + "Wren", + "X BitMap", + "X Font Directory Index", + "X PixMap", + "X10", + "XC", + "XCompose", + "XML", + "XML Property List", + "XPages", + "XProc", + "XQuery", + "XS", + "XSLT", + "Xojo", + "Xonsh", + "Xtend", + "YAML", + "YANG", + "YARA", + "YASnippet", + "Yacc", + "Yul", + "ZAP", + "ZIL", + "Zeek", + "ZenScript", + "Zephir", + "Zig", + "Zimpl", + "cURL Config", + "desktop", + "dircolors", + "eC", + "edn", + "fish", + "hoon", + "jq", + "kvlang", + "mIRC Script", + "mcfunction", + "mupad", + "nanorc", + "nesC", + "ooc", + "q", + "reStructuredText", + "robots.txt", + "sed", + "wisp", + "xBase", +] + +export default languages; \ No newline at end of file diff --git a/packages/web/src/app/components/searchBar/searchBar.tsx b/packages/web/src/app/components/searchBar/searchBar.tsx new file mode 100644 index 00000000..7103f06e --- /dev/null +++ b/packages/web/src/app/components/searchBar/searchBar.tsx @@ -0,0 +1,292 @@ +'use client'; + +import { useTailwind } from "@/hooks/useTailwind"; +import { Repository, SearchQueryParams } from "@/lib/types"; +import { cn, createPathWithQueryParams } from "@/lib/utils"; +import { + cursorCharLeft, + cursorCharRight, + cursorDocEnd, + cursorDocStart, + cursorLineBoundaryBackward, + cursorLineBoundaryForward, + deleteCharBackward, + deleteCharForward, + deleteGroupBackward, + deleteGroupForward, + deleteLineBoundaryBackward, + deleteLineBoundaryForward, + history, + historyKeymap, + selectAll, + selectCharLeft, + selectCharRight, + selectDocEnd, + selectDocStart, + selectLineBoundaryBackward, + selectLineBoundaryForward +} from "@codemirror/commands"; +import { tags as t } from '@lezer/highlight'; +import { createTheme } from '@uiw/codemirror-themes'; +import CodeMirror, { Annotation, EditorView, KeyBinding, keymap, ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { cva } from "class-variance-authority"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useHotkeys } from 'react-hotkeys-hook'; +import { SearchSuggestionsBox, Suggestion } from "./searchSuggestionsBox"; +import { useClickListener } from "@/hooks/useClickListener"; +import { getRepos } from "../../api/(client)/client"; +import languages from "./languages"; +import { zoekt } from "./zoektLanguageExtension"; + +interface SearchBarProps { + className?: string; + size?: "default" | "sm"; + defaultQuery?: string; + autoFocus?: boolean; +} + +const searchBarKeymap: readonly KeyBinding[] = ([ + { key: "ArrowLeft", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true }, + { key: "ArrowRight", run: cursorCharRight, shift: selectCharRight, preventDefault: true }, + + { key: "Home", run: cursorLineBoundaryBackward, shift: selectLineBoundaryBackward, preventDefault: true }, + { key: "Mod-Home", run: cursorDocStart, shift: selectDocStart }, + + { key: "End", run: cursorLineBoundaryForward, shift: selectLineBoundaryForward, preventDefault: true }, + { key: "Mod-End", run: cursorDocEnd, shift: selectDocEnd }, + + { key: "Mod-a", run: selectAll }, + + { key: "Backspace", run: deleteCharBackward, shift: deleteCharBackward }, + { key: "Delete", run: deleteCharForward }, + { key: "Mod-Backspace", mac: "Alt-Backspace", run: deleteGroupBackward }, + { key: "Mod-Delete", mac: "Alt-Delete", run: deleteGroupForward }, + { mac: "Mod-Backspace", run: deleteLineBoundaryBackward }, + { mac: "Mod-Delete", run: deleteLineBoundaryForward } +] as KeyBinding[]).concat(historyKeymap); + +const searchBarContainerVariants = cva( + "search-bar-container flex items-center p-0.5 border rounded-md relative", + { + variants: { + size: { + default: "h-10", + sm: "h-8" + } + }, + defaultVariants: { + size: "default", + } + } +); + +export const SearchBar = ({ + className, + size, + defaultQuery, + autoFocus, +}: SearchBarProps) => { + const router = useRouter(); + const tailwind = useTailwind(); + const suggestionBoxRef = useRef(null); + const editorRef = useRef(null); + const [cursorPosition, setCursorPosition] = useState(0); + const [isSuggestionsBoxEnabled, setIsSuggestionsBoxEnabled ] = useState(false); + const [isSuggestionsBoxFocused, setIsSuggestionsBoxFocused] = useState(false); + + const focusEditor = useCallback(() => editorRef.current?.view?.focus(), []); + const focusSuggestionsBox = useCallback(() => suggestionBoxRef.current?.focus(), []); + + const [_query, setQuery] = useState(defaultQuery ?? ""); + const query = useMemo(() => { + // Replace any newlines with spaces to handle + // copy & pasting text with newlines. + return _query.replaceAll(/\n/g, " "); + }, [_query]); + + const [repos, setRepos] = useState([]); + useEffect(() => { + getRepos().then((response) => { + setRepos(response.List.Repos.map(r => r.Repository)); + }); + }, []); + + const suggestionData = useMemo(() => { + const repoSuggestions: Suggestion[] = repos.map((repo) => { + return { + value: repo.Name, + } + }); + + const languageSuggestions: Suggestion[] = languages.map((lang) => { + const spotlight = [ + "Python", + "Java", + "TypeScript", + "Go", + "C++", + "C#" + ].includes(lang); + + return { + value: lang, + spotlight, + }; + }) + + return { + repos: repoSuggestions, + languages: languageSuggestions, + } + }, [repos]); + + const theme = useMemo(() => { + return createTheme({ + theme: 'light', + settings: { + background: tailwind.theme.colors.background, + foreground: tailwind.theme.colors.foreground, + caret: '#AEAFAD', + }, + styles: [ + { + tag: t.keyword, + color: tailwind.theme.colors.highlight, + }, + { + tag: t.paren, + color: tailwind.theme.colors.highlight, + } + ], + }); + }, [tailwind]); + + const extensions = useMemo(() => { + return [ + keymap.of(searchBarKeymap), + history(), + zoekt(), + EditorView.updateListener.of(update => { + if (update.selectionSet) { + const selection = update.state.selection.main; + if (selection.empty) { + setCursorPosition(selection.anchor); + } + } + }) + ]; + }, []); + + // Hotkey to focus the search bar. + useHotkeys('/', (event) => { + event.preventDefault(); + focusEditor(); + setIsSuggestionsBoxEnabled(true); + if (editorRef.current?.view) { + cursorDocEnd({ + state: editorRef.current.view.state, + dispatch: editorRef.current.view.dispatch, + }); + } + }); + + // Collapse the suggestions box if the user clicks outside of the search bar container. + useClickListener('.search-bar-container', (isElementClicked) => { + if (!isElementClicked) { + setIsSuggestionsBoxEnabled(false); + } else { + setIsSuggestionsBoxEnabled(true); + } + }); + + const onSubmit = () => { + const url = createPathWithQueryParams('/search', + [SearchQueryParams.query, query], + ) + router.push(url); + } + + return ( +
{ + if (e.key === 'Enter') { + e.preventDefault(); + setIsSuggestionsBoxEnabled(false); + onSubmit(); + } + + if (e.key === 'Escape') { + e.preventDefault(); + setIsSuggestionsBoxEnabled(false); + } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + setIsSuggestionsBoxEnabled(true); + focusSuggestionsBox(); + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + } + }} + > + { + setQuery(value); + // Whenever the user types, we want to re-enable + // the suggestions box. + setIsSuggestionsBoxEnabled(true); + }} + theme={theme} + basicSetup={false} + extensions={extensions} + indentWithTab={false} + autoFocus={autoFocus ?? false} + /> + { + setQuery(newQuery); + + // Move the cursor to it's new position. + // @note : normally, react-codemirror handles syncing `query` + // and the document state, but this happens on re-render. Since + // we want to move the cursor before the component re-renders, + // we manually update the document state inline. + editorRef.current?.view?.dispatch({ + changes: { from: 0, to: query.length, insert: newQuery }, + annotations: [Annotation.define().of(true)], + }); + + editorRef.current?.view?.dispatch({ + selection: { anchor: newCursorPosition, head: newCursorPosition }, + }); + + // Re-focus the editor since suggestions cause focus to be lost (both click & keyboard) + editorRef.current?.view?.focus(); + }} + isEnabled={isSuggestionsBoxEnabled} + onReturnFocus={() => { + focusEditor(); + }} + isFocused={isSuggestionsBoxFocused} + onFocus={() => { + setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current); + }} + onBlur={() => { + setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current); + }} + cursorPosition={cursorPosition} + data={suggestionData} + /> +
+ ) +} \ No newline at end of file diff --git a/packages/web/src/app/components/searchBar/searchSuggestionsBox.test.tsx b/packages/web/src/app/components/searchBar/searchSuggestionsBox.test.tsx new file mode 100644 index 00000000..da3eab0c --- /dev/null +++ b/packages/web/src/app/components/searchBar/searchSuggestionsBox.test.tsx @@ -0,0 +1,221 @@ +import { expect, test } from 'vitest' +import { completeSuggestion, splitQuery } from './searchSuggestionsBox' + +test('splitQuery returns a single element when the query is empty', () => { + const { queryParts, cursorIndex } = splitQuery('', 0); + expect(cursorIndex).toEqual(0); + expect(queryParts).toEqual(['']); +}); + +test('splitQuery splits on spaces', () => { + const query = String.raw`repo:^github\.com/example/example$ world`; + const { queryParts, cursorIndex } = splitQuery(query, 0); + expect(queryParts).toEqual(query.split(" ")); + expect(cursorIndex).toEqual(0); +}); + +test('splitQuery groups parts that are in the same quote capture group into a single part', () => { + const part1 = 'lang:"1C Enterprise"'; + const part2 = "hello"; + const { queryParts, cursorIndex } = splitQuery(`${part1} ${part2}`, 12); + expect(queryParts).toEqual([part1, part2]); + expect(cursorIndex).toEqual(0); +}); + +test('splitQuery does not support nested quote capture groups', () => { + const { queryParts } = splitQuery('lang:"My language "with quotes"" hello', 0); + expect(queryParts).toEqual(['lang:"My language "with', 'quotes""', 'hello']); +}); + +test('splitQuery groups all parts together when a quote capture group is not closed', () => { + const query = '"hello asdf ok' + const { queryParts, cursorIndex } = splitQuery(query, 0); + expect(queryParts).toEqual([query]); + expect(cursorIndex).toBe(0); +}); + +test('splitQuery correclty locates the cursor index given the cursor position (1)', () => { + const query = 'foo bar "fizz buzz"'; + + const { queryParts: parts1, cursorIndex: index1 } = splitQuery(query, 0); + expect(parts1).toEqual(['foo', 'bar', '"fizz buzz"']); + expect(parts1[index1]).toBe('foo'); + + const { queryParts: parts2, cursorIndex: index2 } = splitQuery(query, 6); + expect(parts2).toEqual(['foo', 'bar', '"fizz buzz"']); + expect(parts2[index2]).toBe('bar'); + + const { queryParts: parts3, cursorIndex: index3 } = splitQuery(query, 15); + expect(parts3).toEqual(['foo', 'bar', '"fizz buzz"']); + expect(parts3[index3]).toBe('"fizz buzz"'); +}); + +test('splitQuery correclty locates the cursor index given the cursor position (2)', () => { + const query = 'a b'; + expect(splitQuery(query, 0).cursorIndex).toBe(0); + expect(splitQuery(query, 1).cursorIndex).toBe(0); + expect(splitQuery(query, 2).cursorIndex).toBe(1); + expect(splitQuery(query, 3).cursorIndex).toBe(1); +}); + +test('splitQuery can handle multiple spaces adjacent', () => { + expect(splitQuery("a b ", 0).queryParts).toEqual(['a', '', '', 'b', '', '']); +}); + +test('splitQuery locates the cursor index to the last query part when the cursor position is at the end of the query', () => { + const query = "as df"; + const cursorPos = query.length; + const { queryParts, cursorIndex } = splitQuery(query, cursorPos); + expect(cursorIndex).toBe(queryParts.length - 1); + expect(queryParts[cursorIndex]).toBe("df"); + expect(queryParts).toEqual(['as', 'df']); +}); + +test('splitQuery sets the cursor index to 0 when the cursor position is out of bounds', () => { + const query = "hello world"; + const cursorPos = query.length + 1; + const { queryParts, cursorIndex } = splitQuery(query, cursorPos); + expect(cursorIndex).toBe(0); + expect(queryParts[cursorIndex]).toBe("hello"); + expect(queryParts).toEqual(['hello', 'world']); +}); + +test('completeSuggestion can complete a empty query', () => { + const suggestionQuery = ``; + const query = ``; + const suggestion = "hello"; + const { newQuery, newCursorPosition } = completeSuggestion({ + query, + suggestionQuery, + suggestion, + trailingSpace: false, + regexEscaped: false, + cursorPosition: 0, + }); + + const expectedNewQuery = String.raw`hello`; + expect(newQuery).toEqual(expectedNewQuery); + expect(newCursorPosition).toBe(newQuery.length); +}); + +test('completeSuggestion can complete with a empty suggestion query', () => { + const suggestionQuery = ``; + const query = `case:`; + const suggestion = "auto"; + const { newQuery, newCursorPosition } = completeSuggestion({ + query, + suggestionQuery, + suggestion, + trailingSpace: false, + regexEscaped: false, + cursorPosition: query.length, + }); + + const expectedNewQuery = `case:auto`; + expect(newQuery).toEqual(expectedNewQuery); + expect(newCursorPosition).toBe(newQuery.length); +}); + +test('completeSuggestion inserts a trailing space when trailingSpace is true and the completion is at the end of the query', () => { + const suggestionQuery = 'a'; + const part1 = String.raw`lang:Go`; + const part2 = String.raw`case:${suggestionQuery}`; + const query = `${part1} ${part2}` + const suggestion = 'auto'; + const cursorPosition = query.length; + + const { newQuery, newCursorPosition } = completeSuggestion({ + query, + suggestionQuery, + suggestion, + trailingSpace: true, + regexEscaped: false, + cursorPosition, + }); + + const expectedPart2 = `case:auto` + const expectedNewQuery = `${part1} ${expectedPart2} `; + expect(newQuery).toEqual(expectedNewQuery); + expect(newCursorPosition).toBe(newQuery.length); +}); + +test('completeSuggestion does not insert a trailing space when trailingSpace is true and the completion is not at the end of the query', () => { + const suggestionQuery = 'G'; + const part1 = String.raw`lang:${suggestionQuery}`; + const part2 = String.raw`case:auto`; + const query = `${part1} ${part2}` + const suggestion = 'Go'; + const cursorPosition = part1.length; + + const { newQuery, newCursorPosition } = completeSuggestion({ + query, + suggestionQuery, + suggestion, + trailingSpace: true, + regexEscaped: false, + cursorPosition, + }); + + const expectedPart1 = `lang:Go` + const expectedNewQuery = `${expectedPart1} ${part2}`; // Notice no trailing space + expect(newQuery).toEqual(expectedNewQuery); + expect(newCursorPosition).toBe(expectedPart1.length); +}); + +test('completeSuggestion wraps suggestions in quotes when the suggestion contains a space and regexEscaped is false', () => { + const suggestionQuery = `m`; + const query = `lang:${suggestionQuery}`; + const suggestion = `my language`; + const { newQuery, newCursorPosition } = completeSuggestion({ + query, + suggestionQuery, + suggestion, + trailingSpace: false, + regexEscaped: false, + cursorPosition: query.length, + }); + + const expectedNewQuery = `lang:"my language"`; + expect(newQuery).toEqual(expectedNewQuery); + expect(newCursorPosition).toBe(newQuery.length); +}); + +test('completeSuggestion completes on query parts that are inbetween other parts', () => { + const part1 = String.raw`repo:^github\.com/sourcebot\x2ddev/sourcebot$`; + const suggestionQuery = 'Type'; + const part2 = String.raw`lang:${suggestionQuery}`; + const part3 = String.raw`case:auto`; + const query = `${part1} ${part2} ${part3}`; + const suggestion = 'TypeScript'; + const cursorPosition = ([part1, part2].join(" ").length); + + const { newQuery, newCursorPosition } = completeSuggestion({ + query, + suggestionQuery, + suggestion, + trailingSpace: false, + regexEscaped: false, + cursorPosition, + }); + + const expectedPart2 = "lang:TypeScript"; + const expectedNewQuery = String.raw`${part1} ${expectedPart2} ${part3}`; + expect(newQuery).toEqual(expectedNewQuery); + expect(newCursorPosition).toBe([part1, expectedPart2].join(" ").length); +}); + +test('completeSuggestions regex escapes suggestions when regexEscaped is true', () => { + const query = "repo:github"; + const { newQuery, newCursorPosition } = completeSuggestion({ + query, + suggestionQuery: "github", + suggestion: "github.com/sourcebot-dev/sourcebot", + trailingSpace: true, + regexEscaped: true, + cursorPosition: query.length, + }); + + const expectedNewQuery = String.raw`repo:^github\.com/sourcebot\x2ddev/sourcebot$ `; + expect(newQuery).toEqual(expectedNewQuery); + expect(newCursorPosition).toBe(newQuery.length); +}); diff --git a/packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx b/packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx new file mode 100644 index 00000000..91fff3aa --- /dev/null +++ b/packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx @@ -0,0 +1,453 @@ +'use client'; + +import { isDefined } from "@/lib/utils"; +import { CommitIcon, MixerVerticalIcon } from "@radix-ui/react-icons"; +import { IconProps } from "@radix-ui/react-icons/dist/types"; +import assert from "assert"; +import clsx from "clsx"; +import escapeStringRegexp from "escape-string-regexp"; +import Fuse from "fuse.js"; +import { forwardRef, Ref, useEffect, useMemo, useState } from "react"; +import { + archivedModeSuggestions, + caseModeSuggestions, + forkModeSuggestions, + publicModeSuggestions, + refineModeSuggestions, + suggestionModeMappings +} from "./constants"; + +type Icon = React.ForwardRefExoticComponent>; + +export type Suggestion = { + value: string; + description?: string; + spotlight?: boolean; +} + +export type SuggestionMode = + "refine" | + "archived" | + "file" | + "language" | + "case" | + "fork" | + "public" | + "revision" | + "symbol" | + "content" | + "repo"; + +interface SearchSuggestionsBoxProps { + query: string; + onCompletion: (newQuery: string, newCursorPosition: number) => void, + isEnabled: boolean; + cursorPosition: number; + isFocused: boolean; + onFocus: () => void; + onBlur: () => void; + onReturnFocus: () => void; + + data: { + repos: Suggestion[]; + languages: Suggestion[]; + } +} + +const SearchSuggestionsBox = forwardRef(({ + query, + onCompletion, + isEnabled, + data, + cursorPosition, + isFocused, + onFocus, + onBlur, + onReturnFocus, +}: SearchSuggestionsBoxProps, ref: Ref) => { + + const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(0); + + const { suggestionQuery, suggestionMode } = useMemo<{ suggestionQuery?: string, suggestionMode?: SuggestionMode }>(() => { + const { queryParts, cursorIndex } = splitQuery(query, cursorPosition); + if (queryParts.length === 0) { + return {}; + } + const part = queryParts[cursorIndex]; + + // Check if the query part starts with one of the + // prefixes. If it does, then we are in the corresponding + // suggestion mode for that prefix. + const suggestionMode = (() => { + for (const mapping of suggestionModeMappings) { + for (const prefix of mapping.prefixes) { + if (part.startsWith(prefix)) { + return mapping.suggestionMode; + } + } + } + })(); + + if (suggestionMode) { + const index = part.indexOf(":"); + return { + suggestionQuery: part.substring(index + 1), + suggestionMode, + } + } + + // Default to the refine suggestion mode + // if there was no match. + return { + suggestionQuery: part, + suggestionMode: "refine", + } + }, [cursorPosition, query]); + + const { suggestions, isHighlightEnabled, Icon, onSuggestionClicked } = useMemo(() => { + if (!isDefined(suggestionQuery) || !isDefined(suggestionMode)) { + return {}; + } + + const createOnSuggestionClickedHandler = (params: { regexEscaped?: boolean, trailingSpace?: boolean } = {}) => { + const { + regexEscaped = false, + trailingSpace = true + } = params; + + const onSuggestionClicked = (suggestion: string) => { + const { newQuery, newCursorPosition } = completeSuggestion({ + query, + cursorPosition, + regexEscaped, + trailingSpace, + suggestion, + suggestionQuery, + }); + + onCompletion(newQuery, newCursorPosition); + } + + return onSuggestionClicked; + } + + const { + threshold = 0.5, + limit = 10, + list, + isHighlightEnabled = false, + isSpotlightEnabled = false, + onSuggestionClicked, + Icon, + } = ((): { + threshold?: number, + limit?: number, + list: Suggestion[], + isHighlightEnabled?: boolean, + isSpotlightEnabled?: boolean, + onSuggestionClicked: (value: string) => void, + Icon?: Icon + } => { + switch (suggestionMode) { + case "public": + return { + list: publicModeSuggestions, + onSuggestionClicked: createOnSuggestionClickedHandler(), + } + case "fork": + return { + list: forkModeSuggestions, + onSuggestionClicked: createOnSuggestionClickedHandler(), + } + case "case": + return { + list: caseModeSuggestions, + onSuggestionClicked: createOnSuggestionClickedHandler(), + } + case "archived": + return { + list: archivedModeSuggestions, + onSuggestionClicked: createOnSuggestionClickedHandler(), + } + case "repo": + return { + list: data.repos, + Icon: CommitIcon, + onSuggestionClicked: createOnSuggestionClickedHandler({ regexEscaped: true }), + } + case "language": { + return { + list: data.languages, + onSuggestionClicked: createOnSuggestionClickedHandler(), + isSpotlightEnabled: true, + } + } + case "refine": + return { + threshold: 0.1, + list: refineModeSuggestions, + isHighlightEnabled: true, + isSpotlightEnabled: true, + Icon: MixerVerticalIcon, + onSuggestionClicked: createOnSuggestionClickedHandler({ trailingSpace: false }), + } + case "file": + case "revision": + case "content": + case "symbol": + return { + list: [], + onSuggestionClicked: createOnSuggestionClickedHandler(), + } + } + })(); + + const fuse = new Fuse(list, { + threshold, + keys: ['value'], + isCaseSensitive: true, + }); + + const suggestions = (() => { + if (suggestionQuery.length === 0) { + // If spotlight is enabled, get the suggestions that are + // flagged to be surfaced. + if (isSpotlightEnabled) { + const spotlightSuggestions = list.filter((suggestion) => suggestion.spotlight); + return spotlightSuggestions; + + // Otherwise, just show the Nth first suggestions. + } else { + return list.slice(0, limit); + } + } + + // Special case: don't show any suggestions if the query + // is the keyword "or". + if (suggestionQuery === "or") { + return []; + } + + return fuse.search(suggestionQuery, { + limit, + }).map(result => result.item); + })(); + + return { + suggestions, + isHighlightEnabled, + Icon, + onSuggestionClicked, + } + + }, [suggestionQuery, suggestionMode, onCompletion, cursorPosition, data.repos, data.languages, query]); + + // When the list of suggestions change, reset the highlight index + useEffect(() => { + setHighlightedSuggestionIndex(0); + }, [suggestions]); + + const suggestionModeText = useMemo(() => { + if (!suggestionMode) { + return ""; + } + switch (suggestionMode) { + case "repo": + return "Repositories"; + case "refine": + return "Refine search" + default: + return ""; + } + }, [suggestionMode]); + + if ( + !isEnabled || + !suggestions || + suggestions.length === 0 + ) { + return null; + } + + return ( +
{ + if (e.key === 'Enter') { + e.stopPropagation(); + const value = suggestions[highlightedSuggestionIndex].value; + onSuggestionClicked(value); + } + + if (e.key === 'ArrowUp') { + e.stopPropagation(); + setHighlightedSuggestionIndex((curIndex) => { + return curIndex <= 0 ? suggestions.length - 1 : curIndex - 1; + }); + } + + if (e.key === 'ArrowDown') { + e.stopPropagation(); + setHighlightedSuggestionIndex((curIndex) => { + return curIndex >= suggestions.length - 1 ? 0 : curIndex + 1; + }); + } + + if (e.key === 'Escape') { + e.stopPropagation(); + onReturnFocus(); + } + }} + onFocus={onFocus} + onBlur={onBlur} + > +

+ {suggestionModeText} +

+ {suggestions.map((result, index) => ( +
{ + onSuggestionClicked(result.value) + }} + > + {Icon && ( + + )} +
+ + {result.value} + + {result.description && ( + + {result.description} + + )} +
+
+ ))} + {isFocused && ( +
+ + Press Enter to select + +
+ )} +
+ ) +}); + +SearchSuggestionsBox.displayName = "SearchSuggestionsBox"; +export { SearchSuggestionsBox }; + +export const splitQuery = (query: string, cursorPos: number) => { + const queryParts = []; + const seperator = " "; + let cursorIndex = 0; + let accumulator = ""; + let isInQuoteCapture = false; + + for (let i = 0; i < query.length; i++) { + if (i === cursorPos) { + cursorIndex = queryParts.length; + } + + if (query[i] === "\"") { + isInQuoteCapture = !isInQuoteCapture; + } + + if (!isInQuoteCapture && query[i] === seperator) { + queryParts.push(accumulator); + accumulator = ""; + continue; + } + + accumulator += query[i]; + } + queryParts.push(accumulator); + + // Edge case: if the cursor is at the end of the query, set the cursor index to the last query part + if (cursorPos === query.length) { + cursorIndex = queryParts.length - 1; + } + + // @note: since we're guaranteed to have at least one query part, we can safely assume that the cursor position + // will be within bounds. + assert(cursorIndex >= 0 && cursorIndex < queryParts.length, "Cursor position is out of bounds"); + + return { + queryParts, + cursorIndex + } +} + +export const completeSuggestion = (params: { + query: string, + suggestionQuery: string, + cursorPosition: number, + suggestion: string, + trailingSpace: boolean, + regexEscaped: boolean, +}) => { + const { + query, + suggestionQuery, + cursorPosition, + suggestion, + trailingSpace, + regexEscaped, + } = params; + + const { queryParts, cursorIndex } = splitQuery(query, cursorPosition); + + const start = queryParts.slice(0, cursorIndex).join(" "); + const end = queryParts.slice(cursorIndex + 1).join(" "); + + let part = queryParts[cursorIndex]; + + // Remove whatever query we have in the suggestion so far (if any). + // For example, if our part is "repo:gith", then we want to remove "gith" + // from the part before we complete the suggestion. + if (suggestionQuery.length > 0) { + part = part.slice(0, -suggestionQuery.length); + } + + if (regexEscaped) { + part = part + `^${escapeStringRegexp(suggestion)}$`; + } else if (suggestion.includes(" ")) { + part = part + `"${suggestion}"`; + } else { + part = part + suggestion; + } + + // Add a trailing space if we are at the end of the query + if (trailingSpace && cursorIndex === queryParts.length - 1) { + part += " "; + } + + let newQuery = [ + ...(start.length > 0 ? [start] : []), + part, + ].join(" "); + const newCursorPosition = newQuery.length; + + newQuery = [ + newQuery, + ...(end.length > 0 ? [end] : []), + ].join(" "); + + return { + newQuery, + newCursorPosition, + } +} \ No newline at end of file diff --git a/packages/web/src/app/components/searchBar/zoektLanguageExtension.ts b/packages/web/src/app/components/searchBar/zoektLanguageExtension.ts new file mode 100644 index 00000000..096d25d6 --- /dev/null +++ b/packages/web/src/app/components/searchBar/zoektLanguageExtension.ts @@ -0,0 +1,25 @@ +import { LanguageSupport, StreamLanguage } from "@codemirror/language"; +import { tags as t } from '@lezer/highlight'; + +const zoektLanguage = StreamLanguage.define({ + token: (stream) => { + if (stream.match(/-?(file|branch|revision|rev|case|repo|lang|content|sym|archived|fork|public):/)) { + return t.keyword.toString(); + } + + if (stream.match(/\bor\b/)) { + return t.keyword.toString(); + } + + if (stream.match(/(\(|\))/)) { + return t.paren.toString(); + } + + stream.next(); + return null; + }, +}); + +export const zoekt = () => { + return new LanguageSupport(zoektLanguage); +} \ No newline at end of file diff --git a/packages/web/src/app/settingsDropdown.tsx b/packages/web/src/app/components/settingsDropdown.tsx similarity index 100% rename from packages/web/src/app/settingsDropdown.tsx rename to packages/web/src/app/components/settingsDropdown.tsx diff --git a/packages/web/src/app/upgradeToast.tsx b/packages/web/src/app/components/upgradeToast.tsx similarity index 100% rename from packages/web/src/app/upgradeToast.tsx rename to packages/web/src/app/components/upgradeToast.tsx diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx index 542d80c7..fff20c43 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -4,12 +4,12 @@ import Image from "next/image"; import { Suspense } from "react"; import logoDark from "../../public/sb_logo_dark_large.png"; import logoLight from "../../public/sb_logo_light_large.png"; -import { NavigationMenu } from "./navigationMenu"; -import { RepositoryCarousel } from "./repositoryCarousel"; -import { SearchBar } from "./searchBar"; +import { NavigationMenu } from "./components/navigationMenu"; +import { RepositoryCarousel } from "./components/repositoryCarousel"; +import { SearchBar } from "./components/searchBar"; import { Separator } from "@/components/ui/separator"; import { SymbolIcon } from "@radix-ui/react-icons"; -import { UpgradeToast } from "./upgradeToast"; +import { UpgradeToast } from "./components/upgradeToast"; export default async function Home() { @@ -18,7 +18,7 @@ export default async function Home() { -
+
-
- -
+
...
}>
-
+ How to search
lang:typescript (by language) - revision:HEAD (by branch or tag) + rev:HEAD (by branch or tag) { - // @todo: Get language icons return { key, displayName: key, diff --git a/packages/web/src/app/search/page.tsx b/packages/web/src/app/search/page.tsx index 7c787213..3dcbe366 100644 --- a/packages/web/src/app/search/page.tsx +++ b/packages/web/src/app/search/page.tsx @@ -18,8 +18,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import logoDark from "../../../public/sb_logo_dark.png"; import logoLight from "../../../public/sb_logo_light.png"; import { search } from "../api/(client)/client"; -import { SearchBar } from "../searchBar"; -import { SettingsDropdown } from "../settingsDropdown"; +import { SearchBar } from "../components/searchBar"; +import { SettingsDropdown } from "../components/settingsDropdown"; import { CodePreviewPanel } from "./components/codePreviewPanel"; import { FilterPanel } from "./components/filterPanel"; import { SearchResultsPanel } from "./components/searchResultsPanel"; @@ -109,7 +109,7 @@ export default function SearchPage() { totalMatchCount: searchResponse.Result.MatchCount, isBranchFilteringEnabled, } - }, [searchResponse, searchQuery]); + }, [searchResponse]); const isMoreResultsButtonVisible = useMemo(() => { return totalMatchCount > maxMatchDisplayCount; @@ -161,6 +161,7 @@ export default function SearchPage() {
{ - if (stream.match(/-?(file|branch|revision|rev|case|repo|lang|content|sym):/)) { - return t.keyword.toString(); - } - - if (stream.match(/\bor\b/)) { - return t.keyword.toString(); - } - - stream.next(); - return null; - }, -}); - -const zoekt = () =>{ - return new LanguageSupport(zoektLanguage); -} - -const extensions = [ - keymap.of(searchBarKeymap), - history(), - zoekt() -]; - -const searchBarVariants = cva( - "flex items-center w-full p-0.5 border rounded-md", - { - variants: { - size: { - default: "h-10", - sm: "h-8" - } - }, - defaultVariants: { - size: "default", - } - } -); - -export const SearchBar = ({ - className, - size, - defaultQuery, - autoFocus, -}: SearchBarProps) => { - const router = useRouter(); - const tailwind = useTailwind(); - - const theme = useMemo(() => { - return createTheme({ - theme: 'light', - settings: { - background: tailwind.theme.colors.background, - foreground: tailwind.theme.colors.foreground, - caret: '#AEAFAD', - }, - styles: [ - { - tag: t.keyword, - color: tailwind.theme.colors.highlight, - }, - ], - }); - }, [tailwind]); - - const [query, setQuery] = useState(defaultQuery ?? ""); - const editorRef = useRef(null); - - useHotkeys('/', (event) => { - event.preventDefault(); - editorRef.current?.view?.focus(); - if (editorRef.current?.view) { - cursorDocEnd({ - state: editorRef.current.view.state, - dispatch: editorRef.current.view.dispatch, - }); - } - }); - - const onSubmit = () => { - const url = createPathWithQueryParams('/search', - [SearchQueryParams.query, query], - ) - router.push(url); - } - - return ( -
{ - if (e.key === 'Enter') { - e.preventDefault(); - onSubmit(); - } - }} - > - { - setQuery(value); - }} - theme={theme} - basicSetup={false} - extensions={extensions} - indentWithTab={false} - autoFocus={autoFocus ?? false} - /> -
- ) -} \ No newline at end of file diff --git a/packages/web/src/hooks/useClickListener.ts b/packages/web/src/hooks/useClickListener.ts new file mode 100644 index 00000000..3df25035 --- /dev/null +++ b/packages/web/src/hooks/useClickListener.ts @@ -0,0 +1,24 @@ +'use client'; + +import { useEffect } from "react"; + +export const useClickListener = (elementSelector: string, onClick: (elementClicked: boolean) => void) => { + useEffect(() => { + const handleClick = (event: MouseEvent) => { + const element = document.querySelector(elementSelector); + + if (element) { + const isElementClicked = element.contains(event.target as Node); + onClick(isElementClicked); + } + }; + + document.addEventListener('click', handleClick); + + return () => { + document.removeEventListener('click', handleClick); + }; + }, [onClick, elementSelector]); + + return null; +} \ No newline at end of file diff --git a/packages/web/src/hooks/useTailwind.ts b/packages/web/src/hooks/useTailwind.ts index 9c72ebec..c6d05eb4 100644 --- a/packages/web/src/hooks/useTailwind.ts +++ b/packages/web/src/hooks/useTailwind.ts @@ -7,7 +7,7 @@ import tailwindConfig from '../../tailwind.config'; export const useTailwind = () => { const tailwind = useMemo(() => { return resolveConfig(tailwindConfig); - }, [tailwindConfig]); + }, []); return tailwind; } \ No newline at end of file diff --git a/packages/web/src/lib/server/searchService.ts b/packages/web/src/lib/server/searchService.ts index fc40dad5..a4a9157b 100644 --- a/packages/web/src/lib/server/searchService.ts +++ b/packages/web/src/lib/server/searchService.ts @@ -1,6 +1,6 @@ import escapeStringRegexp from "escape-string-regexp"; import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment"; -import { listRepositoriesResponseSchema, searchResponseSchema, zoektSearchResponseSchema } from "../schemas"; +import { listRepositoriesResponseSchema, zoektSearchResponseSchema } from "../schemas"; import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types"; import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError"; import { isServiceError } from "../utils"; diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index 3e374af5..e77de129 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -119,3 +119,8 @@ export const base64Decode = (base64: string): string => { const binString = atob(base64); return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString(); } + +// @see: https://stackoverflow.com/a/65959350/23221295 +export const isDefined = (arg: T | null | undefined): arg is T extends null | undefined ? never : T => { + return arg !== null && arg !== undefined; +} \ No newline at end of file diff --git a/packages/web/vitest.config.mts b/packages/web/vitest.config.mts new file mode 100644 index 00000000..af4db3fc --- /dev/null +++ b/packages/web/vitest.config.mts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' +import tsconfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { + environment: 'jsdom', + watch: false, + }, +}) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 6a7c0a90..dee161d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1779,6 +1779,16 @@ chai "^5.1.2" tinyrainbow "^1.2.0" +"@vitest/expect@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.5.tgz#5a6afa6314cae7a61847927bb5bc038212ca7381" + integrity sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q== + dependencies: + "@vitest/spy" "2.1.5" + "@vitest/utils" "2.1.5" + chai "^5.1.2" + tinyrainbow "^1.2.0" + "@vitest/mocker@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.4.tgz#0dc07edb9114f7f080a0181fbcdb16cd4a2d855d" @@ -1788,6 +1798,15 @@ estree-walker "^3.0.3" magic-string "^0.30.12" +"@vitest/mocker@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.5.tgz#54ee50648bc0bb606dfc58e13edfacb8b9208324" + integrity sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ== + dependencies: + "@vitest/spy" "2.1.5" + estree-walker "^3.0.3" + magic-string "^0.30.12" + "@vitest/pretty-format@2.1.4", "@vitest/pretty-format@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.4.tgz#fc31993bdc1ef5a6c1a4aa6844e7ba55658a4f9f" @@ -1795,6 +1814,13 @@ dependencies: tinyrainbow "^1.2.0" +"@vitest/pretty-format@2.1.5", "@vitest/pretty-format@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.5.tgz#bc79b8826d4a63dc04f2a75d2944694039fa50aa" + integrity sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw== + dependencies: + tinyrainbow "^1.2.0" + "@vitest/runner@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.4.tgz#f9346500bdd0be1c926daaac5d683bae87ceda2c" @@ -1803,6 +1829,14 @@ "@vitest/utils" "2.1.4" pathe "^1.1.2" +"@vitest/runner@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.5.tgz#4d5e2ba2dfc0af74e4b0f9f3f8be020559b26ea9" + integrity sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g== + dependencies: + "@vitest/utils" "2.1.5" + pathe "^1.1.2" + "@vitest/snapshot@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.4.tgz#ef8c3f605fbc23a32773256d37d3fdfd9b23d353" @@ -1812,6 +1846,15 @@ magic-string "^0.30.12" pathe "^1.1.2" +"@vitest/snapshot@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.5.tgz#a09a8712547452a84e08b3ec97b270d9cc156b4f" + integrity sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg== + dependencies: + "@vitest/pretty-format" "2.1.5" + magic-string "^0.30.12" + pathe "^1.1.2" + "@vitest/spy@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.4.tgz#4e90f9783437c5841a27c80f8fd84d7289a6100a" @@ -1819,6 +1862,13 @@ dependencies: tinyspy "^3.0.2" +"@vitest/spy@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.5.tgz#f790d1394a5030644217ce73562e92465e83147e" + integrity sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw== + dependencies: + tinyspy "^3.0.2" + "@vitest/utils@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.4.tgz#6d67ac966647a21ce8bc497472ce230de3b64537" @@ -1828,6 +1878,15 @@ loupe "^3.1.2" tinyrainbow "^1.2.0" +"@vitest/utils@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.5.tgz#0e19ce677c870830a1573d33ee86b0d6109e9546" + integrity sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg== + dependencies: + "@vitest/pretty-format" "2.1.5" + loupe "^3.1.2" + tinyrainbow "^1.2.0" + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -1845,6 +1904,13 @@ acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -2342,6 +2408,13 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssstyle@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.1.0.tgz#161faee382af1bafadb6d3867a92a19bcb4aea70" + integrity sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA== + dependencies: + rrweb-cssom "^0.7.1" + csstype@^3.0.2: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" @@ -2352,6 +2425,14 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +data-urls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" + integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== + dependencies: + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + data-view-buffer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" @@ -2379,6 +2460,13 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +debug@4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2386,12 +2474,10 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== - dependencies: - ms "^2.1.3" +decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== deep-eql@^5.0.1: version "5.0.2" @@ -2552,6 +2638,11 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2658,6 +2749,11 @@ es-iterator-helpers@^1.0.19: iterator.prototype "^1.1.2" safe-array-concat "^1.1.2" +es-module-lexer@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + es-object-atoms@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" @@ -3276,6 +3372,11 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -3344,11 +3445,41 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== + dependencies: + whatwg-encoding "^3.1.1" + +http-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-status-codes@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.3.0.tgz#987fefb28c69f92a43aecc77feec2866349a8bfc" integrity sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA== +https-proxy-agent@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -3547,6 +3678,11 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -3669,6 +3805,33 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsdom@^25.0.1: + version "25.0.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.1.tgz#536ec685c288fc8a5773a65f82d8b44badcc73ef" + integrity sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw== + dependencies: + cssstyle "^4.1.0" + data-urls "^5.0.0" + decimal.js "^10.4.3" + form-data "^4.0.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.5" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.12" + parse5 "^7.1.2" + rrweb-cssom "^0.7.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^5.0.0" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.18.0" + xml-name-validator "^5.0.0" + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -4013,6 +4176,11 @@ npm-run-all@^4.1.5: shell-quote "^1.6.1" string.prototype.padend "^3.0.0" +nwsapi@^2.2.12: + version "2.2.13" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.13.tgz#e56b4e98960e7a040e5474536587e599c4ff4655" + integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ== + object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -4148,6 +4316,13 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse5@^7.1.2: + version "7.2.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.2.1.tgz#8928f55915e6125f430cc44309765bf17556a33a" + integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== + dependencies: + entities "^4.5.0" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -4389,7 +4564,7 @@ ps-tree@^1.2.0: dependencies: event-stream "=3.3.4" -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -4612,6 +4787,11 @@ rollup@^4.20.0: "@rollup/rollup-win32-x64-msvc" "4.25.0" fsevents "~2.3.2" +rrweb-cssom@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b" + integrity sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -4653,6 +4833,18 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + scheduler@^0.23.2: version "0.23.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" @@ -4849,7 +5041,7 @@ stackback@0.0.2: resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== -std-env@^3.7.0: +std-env@^3.7.0, std-env@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== @@ -5054,6 +5246,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + tailwind-merge@^2.5.2: version "2.5.3" resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.3.tgz#579546e14ddda24462e0303acd8798c50f5511bb" @@ -5156,6 +5353,18 @@ tinyspy@^3.0.2: resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== +tldts-core@^6.1.63: + version "6.1.63" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.63.tgz#094f2b05faf90cf1e228eda1caef658425c7c912" + integrity sha512-H1XCt54xY+QPbwhTgmxLkepX0MVHu3USfMmejiCOdkMbRcP22Pn2FVF127r/GWXVDmXTRezyF3Ckvhn4Fs6j7Q== + +tldts@^6.1.32: + version "6.1.63" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.63.tgz#81a3898554ba1dbbdc6844ed4e68c574f09fed32" + integrity sha512-YWwhsjyn9sB/1rOkSRYxvkN/wl5LFM1QDv6F2pVR+pb/jFne4EOBxHfkKVWvDIBEAw9iGOwwubHtQTm0WRT5sQ== + dependencies: + tldts-core "^6.1.63" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -5163,6 +5372,20 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tough-cookie@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" + integrity sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q== + dependencies: + tldts "^6.1.32" + +tr46@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec" + integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g== + dependencies: + punycode "^2.3.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -5193,6 +5416,11 @@ tsc-watch@^6.2.0: ps-tree "^1.2.0" string-argv "^0.3.1" +tsconfck@^3.0.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.4.tgz#de01a15334962e2feb526824339b51be26712229" + integrity sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ== + tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -5351,6 +5579,26 @@ vite-node@2.1.4: pathe "^1.1.2" vite "^5.0.0" +vite-node@2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.5.tgz#cf28c637b2ebe65921f3118a165b7cf00a1cdf19" + integrity sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w== + dependencies: + cac "^6.7.14" + debug "^4.3.7" + es-module-lexer "^1.5.4" + pathe "^1.1.2" + vite "^5.0.0" + +vite-tsconfig-paths@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.3.tgz#ffab28a9c2cb171e7685dd5cdcb93b132187cad5" + integrity sha512-0bz+PDlLpGfP2CigeSKL9NFTF1KtXkeHGZSSaGQSuPZH77GhoiQaA8IjYgOaynSuwlDTolSUEU0ErVvju3NURg== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + vite@^5.0.0: version "5.4.11" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5" @@ -5388,11 +5636,44 @@ vitest@^2.1.4: vite-node "2.1.4" why-is-node-running "^2.3.0" +vitest@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.5.tgz#a93b7b84a84650130727baae441354e6df118148" + integrity sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A== + dependencies: + "@vitest/expect" "2.1.5" + "@vitest/mocker" "2.1.5" + "@vitest/pretty-format" "^2.1.5" + "@vitest/runner" "2.1.5" + "@vitest/snapshot" "2.1.5" + "@vitest/spy" "2.1.5" + "@vitest/utils" "2.1.5" + chai "^5.1.2" + debug "^4.3.7" + expect-type "^1.1.0" + magic-string "^0.30.12" + pathe "^1.1.2" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.1" + tinypool "^1.0.1" + tinyrainbow "^1.2.0" + vite "^5.0.0" + vite-node "2.1.5" + why-is-node-running "^2.3.0" + w3c-keyname@^2.2.4: version "2.2.8" resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + web-vitals@^4.0.1: version "4.2.3" resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.3.tgz#270c4baecfbc6ec6fc15da1989e465e5f9b94fb7" @@ -5403,6 +5684,31 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + +whatwg-url@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6" + integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw== + dependencies: + tr46 "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -5537,11 +5843,26 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + xcase@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xcase/-/xcase-2.0.1.tgz#c7fa72caa0f440db78fd5673432038ac984450b9" integrity sha512-UmFXIPU+9Eg3E9m/728Bii0lAIuoc+6nbrNUKaRPJOFp91ih44qqGlWtxMB6kXFrRD6po+86ksHM5XHCfk6iPw== +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + yaml@^2.3.4: version "2.5.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130"