diff --git a/.gitignore b/.gitignore index 45d0606e..0337ddad 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ cypress/screenshots # Dist build dist +lib +lib-next # Dependencies /node_modules diff --git a/cypress/integration/test.spec.ts b/cypress/integration/test.spec.ts index daaaec1e..784b3e0f 100644 --- a/cypress/integration/test.spec.ts +++ b/cypress/integration/test.spec.ts @@ -13,4 +13,29 @@ describe("test", () => { it("renders query parameters", () => { cy.findByText(/query parameters/i).should("exist"); }); + + it("loads when clicking on tabs", () => { + cy.on("uncaught:exception", () => { + // there is an uncaught error trying to load monaco in ci + return false; + }); + + function checkTab(tab: RegExp, heading: RegExp) { + cy.get(".navbar").findByRole("link", { name: tab }).click(); + cy.findByRole("heading", { name: heading, level: 1 }).should("exist"); + } + + checkTab(/issue 21/i, /missing summary/i); + checkTab(/cos/i, /generating an iam token/i); + checkTab(/yaml/i, /hello world/i); + checkTab(/api/i, /recursive/i); + }); + + it("loads a page with authentication", () => { + cy.visit("/cos/list-buckets"); + cy.findByRole("button", { name: /authorize/i }).should("exist"); + + cy.visit("/cos/create-a-bucket"); + cy.findByRole("button", { name: /authorize/i }).should("exist"); + }); }); diff --git a/demo/docusaurus.config.js b/demo/docusaurus.config.js index ddefbdd3..49fb1970 100644 --- a/demo/docusaurus.config.js +++ b/demo/docusaurus.config.js @@ -146,7 +146,9 @@ const config = { theme: lightCodeTheme, darkTheme: darkCodeTheme, }, - proxy: "https://cors-anywhere.herokuapp.com", + api: { + authPersistance: "localStorage", + }, }), }; diff --git a/demo/examples/openapi-cos.json b/demo/examples/openapi-cos.json index 355c4e43..ce5a01fb 100644 --- a/demo/examples/openapi-cos.json +++ b/demo/examples/openapi-cos.json @@ -68,6 +68,10 @@ "schema": { "type": "string" } } ], + "security": [ + { "BearerAuth": [] }, + { "BearerAuth": [], "BasicAuth": [] } + ], "responses": { "200": { "description": "ok" } } } }, @@ -133,7 +137,10 @@ } }, "components": { - "securitySchemes": { "BearerAuth": { "type": "http", "scheme": "bearer" } }, + "securitySchemes": { + "BearerAuth": { "type": "http", "scheme": "bearer" }, + "BasicAuth": { "type": "http", "scheme": "basic" } + }, "schemas": { "AuthForm": { "type": "object", diff --git a/package.json b/package.json index e721bc16..4f02bcff 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,23 @@ "release:changelog": "scripts/changelog.ts", "release:version": "scripts/version.ts", "release:publish": "scripts/publish.ts", - "clean": "rm -rf node_modules build demo/.docusaurus demo/build demo/node_modules && find packages -name node_modules -type d -maxdepth 2 -exec rm -rf {} + && find packages -name dist -type d -maxdepth 2 -exec rm -rf {} +" + "clean": "rm -rf node_modules build demo/.docusaurus demo/build demo/node_modules && find packages -name node_modules -type d -maxdepth 2 -exec rm -rf {} + && find packages -name dist -type d -maxdepth 2 -exec rm -rf {} + && find packages -name lib -type d -maxdepth 2 -exec rm -rf {} + && find packages -name lib-next -type d -maxdepth 2 -exec rm -rf {} +" }, "devDependencies": { + "@babel/cli": "^7.16.0", + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-transform-modules-commonjs": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", "@testing-library/cypress": "^8.0.1", "@types/jest": "^27.0.2", "@typescript-eslint/eslint-plugin": "^4.0.0", "@typescript-eslint/parser": "^4.0.0", "babel-eslint": "^10.0.0", "cypress": "^8.7.0", + "cross-env": "^7.0.3", "eslint": "^7.5.0", "eslint-config-react-app": "^6.0.0", "eslint-plugin-cypress": "^2.12.1", diff --git a/packages/docusaurus-plugin-openapi/src/openapi.ts b/packages/docusaurus-plugin-openapi/src/openapi.ts index 5c263035..a15d9b87 100644 --- a/packages/docusaurus-plugin-openapi/src/openapi.ts +++ b/packages/docusaurus-plugin-openapi/src/openapi.ts @@ -231,10 +231,8 @@ export async function loadOpenapi( item.security = dereffedSpec.security; } - // TODO: we don't want this behavior anymore, but it might break things - // if (i === 0 && ii === 0) { - // item.id = "/"; - // } + // Add security schemes so we know how to handle security. + item.securitySchemes = dereffedSpec.components?.securitySchemes; item.permalink = normalizeUrl([baseUrl, routeBasePath, item.id]); diff --git a/packages/docusaurus-plugin-openapi/src/types.ts b/packages/docusaurus-plugin-openapi/src/types.ts index c243b3e7..345f66a4 100644 --- a/packages/docusaurus-plugin-openapi/src/types.ts +++ b/packages/docusaurus-plugin-openapi/src/types.ts @@ -356,6 +356,13 @@ export interface ApiItem extends OperationObject { next: Page; previous: Page; jsonRequestBodyExample: string; + securitySchemes?: Map< + | ApiKeySecuritySchemeObject + | HttpSecuritySchemeObject + | Oauth2SecuritySchemeObject + | OpenIdConnectSecuritySchemeObject + | ReferenceObject + >; } export interface Page { diff --git a/packages/docusaurus-theme-openapi/babel.config.js b/packages/docusaurus-theme-openapi/babel.config.js new file mode 100644 index 00000000..1f29594a --- /dev/null +++ b/packages/docusaurus-theme-openapi/babel.config.js @@ -0,0 +1,35 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +module.exports = { + env: { + // USED FOR NODE/RUNTIME + // maybe we should differenciate both cases because + // we mostly need to transpile some features so that node does not crash... + lib: { + presets: [ + ["@babel/preset-typescript", { isTSX: true, allExtensions: true }], + ], + // Useful to transpile for older node versions + plugins: [ + "@babel/plugin-transform-modules-commonjs", + "@babel/plugin-proposal-nullish-coalescing-operator", + "@babel/plugin-proposal-optional-chaining", + ], + }, + + // USED FOR JS SWIZZLE + // /lib-next folder is used as source to swizzle JS source code + // This JS code is created from TS source code + // This source code should look clean/human readable to be usable + "lib-next": { + presets: [ + ["@babel/preset-typescript", { isTSX: true, allExtensions: true }], + ], + }, + }, +}; diff --git a/packages/docusaurus-theme-openapi/package.json b/packages/docusaurus-theme-openapi/package.json index a0b5927f..57e996f0 100644 --- a/packages/docusaurus-theme-openapi/package.json +++ b/packages/docusaurus-theme-openapi/package.json @@ -7,13 +7,16 @@ "access": "public" }, "types": "src/theme-openapi.d.ts", - "main": "dist/index.js", + "main": "lib/index.js", "files": [ "/dist" ], "scripts": { - "build": "scripts/build.ts", - "watch": "scripts/build.ts --watch" + "build": "tsc --noEmit && yarn babel:lib && yarn babel:lib-next && yarn format:lib-next", + "watch": "concurrently --names \"lib,lib-next,tsc\" --kill-others \"yarn babel:lib --watch\" \"yarn babel:lib-next --watch\" \"yarn tsc --watch\"", + "babel:lib": "cross-env BABEL_ENV=lib babel src -d lib --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files", + "babel:lib-next": "cross-env BABEL_ENV=lib-next babel src -d lib-next --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files", + "format:lib-next": "prettier --config ../../.prettierrc.json --write \"lib-next/**/*.{js,ts,jsx,tsc}\"" }, "devDependencies": { "@docusaurus/module-type-aliases": "2.0.0-beta.9", diff --git a/packages/docusaurus-theme-openapi/scripts/build.ts b/packages/docusaurus-theme-openapi/scripts/build.ts deleted file mode 100755 index d70233fc..00000000 --- a/packages/docusaurus-theme-openapi/scripts/build.ts +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env ts-node -/* ============================================================================ - * Copyright (c) Cloud Annotations - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * ========================================================================== */ - -import concurrently, { CommandObj } from "concurrently"; - -const watch = process.argv.includes("--watch"); - -const watchFlag = watch ? "--watch" : ""; - -const commands: CommandObj[] = [ - { - command: `tsc --preserveWatchOutput ${watchFlag}`, - name: "tsc", - prefixColor: "yellow", - }, - { - // Copy all files BUT *.ts and *.tsx (which will be turned into JS by TS). - // Notice that we include *.d.ts, they're perfectly valid to copy. - command: `cpx 'src/**/{*.d.ts,!(*.ts|*.tsx)}' dist ${watchFlag}`, - name: "cpx", - prefixColor: "yellow", - }, -]; - -concurrently(commands, { - killOthers: ["failure"], - prefix: "[{time} {name}]", - timestampFormat: "mm:ss.S", -}).catch(() => { - process.exit(1); -}); diff --git a/packages/docusaurus-theme-openapi/src/index.ts b/packages/docusaurus-theme-openapi/src/index.ts index 728ed739..c41590c4 100644 --- a/packages/docusaurus-theme-openapi/src/index.ts +++ b/packages/docusaurus-theme-openapi/src/index.ts @@ -15,7 +15,11 @@ export default function docusaurusThemeOpenAPI(): Plugin { name: "docusaurus-theme-openapi", getThemePath() { - return path.resolve(__dirname, "./theme"); + return path.join(__dirname, "..", "lib-next", "theme"); + }, + + getTypeScriptThemePath() { + return path.resolve(__dirname, "..", "src", "theme"); }, configureWebpack() { diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Authorization/index.js b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Authorization/index.js deleted file mode 100644 index cb9ecd3d..00000000 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Authorization/index.js +++ /dev/null @@ -1,172 +0,0 @@ -/* ============================================================================ - * Copyright (c) Cloud Annotations - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * ========================================================================== */ - -import React, { useState } from "react"; - -import { useSelector } from "react-redux"; - -import { useActions } from "../redux/actions"; -import FloatingButton from "./../FloatingButton"; -import FormItem from "./../FormItem"; -import FormTextInput from "./../FormTextInput"; - -function Authorization() { - const security = useSelector((state) => state.security); - const bearerToken = useSelector((state) => state.bearerToken); - const { setBearerToken, clearSession } = useActions(); - const [editing, setEditing] = useState(false); - const [basicMode, setBasicMode] = useState(true); - const [token, setToken] = useState(null); - const [password, setPassword] = useState(null); - const requiresAuthorization = security?.length > 0; - - if (!requiresAuthorization) { - return null; - } - - if (!!bearerToken) { - return ( -
- -
- ); - } - - if (editing) { - return ( -
- - setBasicMode((mode) => !mode)} - label="Switch Mode" - > -
-            {!basicMode ? (
-              
-                 {
-                    setToken(e.target.value);
-                  }}
-                />
-              
-            ) : (
-              <>
-                
-                   {
-                      setToken(e.target.value);
-                    }}
-                  />
-                
-                
-                   {
-                      setPassword(e.target.value);
-                    }}
-                  />
-                
-              
-            )}
-          
-
-
- ); - } - - return ( -
- -
- ); -} - -export default Authorization; diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Authorization/index.tsx b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Authorization/index.tsx new file mode 100644 index 00000000..34cfc6a8 --- /dev/null +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Authorization/index.tsx @@ -0,0 +1,196 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import React, { useState } from "react"; + +import clsx from "clsx"; +import produce from "immer"; +import { useSelector } from "react-redux"; + +// @ts-ignore +import FormItem from "../FormItem"; +// @ts-ignore +import FormSelect from "../FormSelect"; +// @ts-ignore +import FormTextInput from "../FormTextInput"; +import { useActions } from "../redux/actions"; +import styles from "../styles.module.css"; + +type Props = { + mode: "locked" | "unlocked"; +} & JSX.IntrinsicElements["button"]; + +function LockButton({ mode, children, style, ...rest }: Props) { + return ( + + ); +} + +function Authorization() { + const { setAuth, setSelectedAuthID } = useActions(); + const auth = useSelector((state: any) => state.auth); + const selectedAuthID = useSelector((state: any) => state.selectedAuthID); + const authOptionIDs = useSelector((state: any) => state.authOptionIDs); + const [editing, setEditing] = useState(false); + + const noAuthorization = selectedAuthID === undefined; + + if (noAuthorization) { + return null; + } + + const selectedAuthIndex = authOptionIDs.indexOf(selectedAuthID); + const selectedAuth = auth[selectedAuthIndex] as any[]; + + const values = selectedAuth.flat().flatMap((a) => Object.values(a.data)); + const authenticated = !values.includes(undefined); + + if (editing) { + return ( +
+ {authOptionIDs.length > 1 && ( + + { + setSelectedAuthID(e.target.value); + }} + /> + + )} + {selectedAuth.map((a, i) => { + if (a.type === "http" && a.scheme === "bearer") { + return ( + + { + const newAuth = produce(auth, (draft: any) => { + let value = (e.target.value ?? "").trim(); + value = !value ? undefined : value; + draft[selectedAuthIndex][i].data.token = value; + }); + setAuth(newAuth); + }} + /> + + ); + } + + if (a.type === "http" && a.scheme === "basic") { + return ( + + + { + const newAuth = produce(auth, (draft: any) => { + let value = (e.target.value ?? "").trim(); + value = !value ? undefined : value; + draft[selectedAuthIndex][i].data.username = value; + }); + setAuth(newAuth); + }} + /> + + + { + const newAuth = produce(auth, (draft: any) => { + let value = (e.target.value ?? "").trim(); + value = !value ? undefined : value; + draft[selectedAuthIndex][i].data.password = value; + }); + setAuth(newAuth); + }} + /> + + + ); + } + + return null; + })} + { + setEditing(false); + }} + > + Save + +
+ ); + } + + if (authenticated) { + return ( + { + setEditing(true); + }} + > + Authorized + + ); + } + + return ( + { + setEditing(true); + }} + > + Authorize + + ); +} + +export default Authorization; diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Curl/index.js b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Curl/index.js index 873ad02d..18c34ed2 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Curl/index.js +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Curl/index.js @@ -128,8 +128,9 @@ function Curl() { const accept = useSelector((state) => state.accept); const endpoint = useSelector((state) => state.endpoint); const postman = useSelector((state) => state.postman); - const security = useSelector((state) => state.security); - const bearerToken = useSelector((state) => state.bearerToken); + const auth = useSelector((state) => state.auth); + const selectedAuthID = useSelector((state) => state.selectedAuthID); + const authOptionIDs = useSelector((state) => state.authOptionIDs); const langs = [ ...(siteConfig?.themeConfig?.languageTabs ?? languageSet), @@ -151,8 +152,9 @@ function Curl() { headerParams, body, endpoint, - security, - bearerToken, + auth, + selectedAuthID, + authOptionIDs, }); codegen.convert( @@ -183,8 +185,9 @@ function Curl() { postman, queryParams, endpoint, - security, - bearerToken, + auth, + selectedAuthID, + authOptionIDs, ]); const ref = useRef(null); diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Execute/index.js b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Execute/index.js index 235611fa..7a8a266d 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Execute/index.js +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Execute/index.js @@ -7,7 +7,6 @@ import React from "react"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import { useSelector } from "react-redux"; import buildPostmanRequest from "./../buildPostmanRequest"; @@ -26,9 +25,6 @@ function isRequestComplete(params) { } function Execute() { - const { siteConfig } = useDocusaurusContext(); - const { proxy } = siteConfig.themeConfig; - const postman = useSelector((state) => state.postman); const pathParams = useSelector((state) => state.params.path); @@ -39,8 +35,10 @@ function Execute() { const body = useSelector((state) => state.body); const accept = useSelector((state) => state.accept); const endpoint = useSelector((state) => state.endpoint); - const security = useSelector((state) => state.security); - const bearerToken = useSelector((state) => state.bearerToken); + const auth = useSelector((state) => state.auth); + const selectedAuthID = useSelector((state) => state.selectedAuthID); + const authOptionIDs = useSelector((state) => state.authOptionIDs); + const proxy = useSelector((state) => state.options.proxy); const params = useSelector((state) => state.params); const finishedRequest = isRequestComplete(params); @@ -56,8 +54,9 @@ function Execute() { headerParams, body, endpoint, - security, - bearerToken, + auth, + selectedAuthID, + authOptionIDs, }); return ( @@ -67,8 +66,12 @@ function Execute() { disabled={!finishedRequest} onClick={async () => { setResponse("loading..."); - const res = await makeRequest(postmanRequest, proxy, body); - setResponse(res); + try { + const res = await makeRequest(postmanRequest, proxy, body); + setResponse(res); + } catch (e) { + setResponse(e.message ?? "Error fetching."); + } }} > Execute diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Execute/makeRequest.js b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Execute/makeRequest.js index 0054835f..b5ef500a 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Execute/makeRequest.js +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/Execute/makeRequest.js @@ -154,18 +154,16 @@ async function makeRequest(request, proxy, _body) { body: myBody, }; + let finalUrl = request.url.toString(); if (proxy) { // Ensure the proxy ends with a slash. let normalizedProxy = proxy.replace(/\/$/, "") + "/"; - return await fetch( - normalizedProxy + request.url.toString(), - requestOptions - ).then((response) => response.text()); + finalUrl = normalizedProxy + request.url.toString(); } - return await fetch(request.url.toString(), requestOptions).then((response) => - response.text() - ); + return await fetch(finalUrl, requestOptions).then((response) => { + return response.text(); + }); } export default makeRequest; diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.js b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.js index 730c60c6..1920f505 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.js +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/buildPostmanRequest.js @@ -164,8 +164,9 @@ function buildPostmanRequest( headerParams, body, endpoint, - security, - bearerToken, + auth, + selectedAuthID, + authOptionIDs, } ) { const clonedPostman = cloneDeep(postman); @@ -192,11 +193,39 @@ function buildPostmanRequest( const cookie = buildCookie(cookieParams); let otherHeaders = []; - if (bearerToken && security?.length > 0) { - otherHeaders.push({ - key: "Authorization", - value: bearerToken, - }); + + let selectedAuth = []; + if (selectedAuthID !== undefined) { + const selectedAuthIndex = authOptionIDs.indexOf(selectedAuthID); + selectedAuth = auth[selectedAuthIndex]; + } + + for (const a of selectedAuth) { + // Bearer Auth + if (a.type === "http" && a.scheme === "bearer") { + const { token } = a.data; + if (token === undefined) { + continue; + } + otherHeaders.push({ + key: "Authorization", + value: `Bearer ${token}`, + }); + continue; + } + + // Basic Auth + if (a.type === "http" && a.scheme === "basic") { + const { username, password } = a.data; + if (username === undefined || password === undefined) { + continue; + } + otherHeaders.push({ + key: "Authorization", + value: `Basic ${window.btoa(`${username}:${password}`)}`, + }); + continue; + } } setHeaders( diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/index.js b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/index.js index 9cc3df20..8e1f10e0 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/index.js +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/index.js @@ -7,6 +7,7 @@ import React from "react"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import sdk from "postman-collection"; import { Provider } from "react-redux"; import { createStore } from "redux"; @@ -26,10 +27,13 @@ import Response from "./Response"; import styles from "./styles.module.css"; function ApiDemoPanel({ item }) { + const { siteConfig } = useDocusaurusContext(); + const { api: options } = siteConfig.themeConfig; + item.postman = new sdk.Request(item.postman); const store = createStore( reducer, - init(item), + init(item, options), composeWithDevTools({ name: `${item.method} ${item.path}` })() ); diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/actions.js b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/actions.ts similarity index 70% rename from packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/actions.js rename to packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/actions.ts index fb199385..2db785ee 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/actions.js +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/actions.ts @@ -16,18 +16,18 @@ export const types = { setContentType: "SET_CONTENT_TYPE", setEndpoint: "SET_ENDPOINT", setEndpointValue: "SET_ENDPOINT_VALUE", - setBearerToken: "SET_BEARER_TOKEN", - clearSession: "CLEAR_SESSION", + setAuth: "SET_AUTH", + setSelectedAuthID: "SET_SELECTED_AUTH_ID", }; export function useActions() { const dispatch = useDispatch(); - function updateParam(param) { + function updateParam(param: any) { dispatch({ type: types.updateParam, param }); } - function setResponse(response) { + function setResponse(response: any) { dispatch({ type: types.setResponse, response }); } @@ -35,36 +35,36 @@ export function useActions() { dispatch({ type: types.setResponse, response: undefined }); } - function setBody(body) { + function setBody(body: any) { dispatch({ type: types.setBody, body }); } - function setForm(body) { + function setForm(body: any) { dispatch({ type: types.setForm, body }); } - function setAccept(accept) { + function setAccept(accept: any) { dispatch({ type: types.setAccept, accept }); } - function setContentType(contentType) { + function setContentType(contentType: any) { dispatch({ type: types.setContentType, contentType }); } - function setEndpoint(endpoint) { + function setEndpoint(endpoint: any) { dispatch({ type: types.setEndpoint, endpoint }); } - function setEndpointValue(key, value) { + function setEndpointValue(key: any, value: any) { dispatch({ type: types.setEndpointValue, key, value }); } - function setBearerToken(bearerToken) { - dispatch({ type: types.setBearerToken, bearerToken }); + function setAuth(auth: any) { + dispatch({ type: types.setAuth, auth }); } - function clearSession() { - dispatch({ type: types.clearSession }); + function setSelectedAuthID(selectedAuthID: string) { + dispatch({ type: types.setSelectedAuthID, selectedAuthID }); } return { @@ -77,7 +77,7 @@ export function useActions() { clearResponse, setBody, setForm, - setBearerToken, - clearSession, + setAuth, + setSelectedAuthID, }; } diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/auth-types.ts b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/auth-types.ts new file mode 100644 index 00000000..d4c35881 --- /dev/null +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/auth-types.ts @@ -0,0 +1,28 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +export interface Security { + key: string; + scopes: string[]; + type: string; + [key: string]: any; +} + +export function getAuthDataKeys(security: Security) { + // Bearer Auth + if (security.type === "http" && security.scheme === "bearer") { + return ["token"]; + } + + // Basic Auth + if (security.type === "http" && security.scheme === "basic") { + return ["username", "password"]; + } + + // none + return []; +} diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/init.js b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/init.js deleted file mode 100644 index 1c88c559..00000000 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/init.js +++ /dev/null @@ -1,81 +0,0 @@ -/* ============================================================================ - * Copyright (c) Cloud Annotations - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * ========================================================================== */ - -function init({ - path, - method, - parameters = [], - requestBody = {}, - responses = {}, - "x-code-samples": codeSamples = [], - postman, - jsonRequestBodyExample, - servers, - security, -}) { - const { content = {} } = requestBody; - - const contentTypeArray = Object.keys(content); - - const acceptArray = Array.from( - new Set( - Object.values(responses) - .map((response) => Object.keys(response.content || {})) - .flat() - ) - ); - - let params = { - path: [], - query: [], - header: [], - cookie: [], - }; - - parameters.forEach((param) => { - params[param.in].push({ - ...param, - name: param.name, - value: undefined, - description: param.description, - type: param.in, - required: param.required, - schema: param.schema, - }); - }); - - if (!servers) { - servers = []; - } - - let bearerToken = sessionStorage.getItem("bearerToken"); - if (!bearerToken) { - bearerToken = undefined; - } - - return { - jsonRequestBodyExample: jsonRequestBodyExample, - requestBodyMetadata: requestBody, // TODO: no... - acceptOptions: acceptArray, - contentTypeOptions: contentTypeArray, - path: path, - method: method, - params: params, - contentType: contentTypeArray[0], - codeSamples: codeSamples, - accept: acceptArray[0], - body: undefined, - response: undefined, - postman: postman, - servers: servers, - endpoint: servers[0], - security: security, - bearerToken: bearerToken, - }; -} - -export default init; diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/init.ts b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/init.ts new file mode 100644 index 00000000..b66a684e --- /dev/null +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/init.ts @@ -0,0 +1,113 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import { ThemeConfig } from "../../../types"; +import { loadAuth, loadSelectedAuth } from "./persistance"; + +function init( + { + path, + method, + parameters = [], + requestBody = {}, + responses = {}, + "x-code-samples": codeSamples = [], + postman, + jsonRequestBodyExample, + servers, + security, + securitySchemes, + }: any, + options: ThemeConfig["api"] = {} +) { + const { content = {} } = requestBody; + + const contentTypeArray = Object.keys(content); + + const acceptArray = Array.from( + new Set( + Object.values(responses) + .map((response: any) => Object.keys(response.content ?? {})) + .flat() + ) + ); + + let params: any = { + path: [], + query: [], + header: [], + cookie: [], + }; + + parameters.forEach((param: any) => { + params[param.in].push({ + ...param, + name: param.name, + value: undefined, + description: param.description, + type: param.in, + required: param.required, + schema: param.schema, + }); + }); + + if (!servers) { + servers = []; + } + + const auth = loadAuth({ + securitySchemes, + security: security ?? [], + persistance: options.authPersistance, + }); + + function createOptionIDs(auth: any): string[] { + return auth + .map((a: { key: string }[]) => + a.reduce((acc, cur) => { + if (acc === undefined) { + return cur.key; + } + return `${acc} and ${cur.key}`; + }, undefined as string | undefined) + ) + .filter(Boolean); + } + const authOptionIDs = createOptionIDs(auth); + const _uniqueAuthKey = authOptionIDs.join("&"); + + const selectedAuthID = + loadSelectedAuth({ + key: _uniqueAuthKey, + persistance: options.authPersistance, + }) ?? authOptionIDs[0]; + + return { + jsonRequestBodyExample, + requestBodyMetadata: requestBody, // TODO: no... + acceptOptions: acceptArray, + contentTypeOptions: contentTypeArray, + path, + method, + params, + contentType: contentTypeArray[0], + codeSamples, + accept: acceptArray[0], + body: undefined, + response: undefined, + postman, + servers, + endpoint: servers[0], + auth, + selectedAuthID, + authOptionIDs, + _uniqueAuthKey, + options, + }; +} + +export default init; diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/persistance.ts b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/persistance.ts new file mode 100644 index 00000000..d233fe35 --- /dev/null +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/persistance.ts @@ -0,0 +1,205 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import { getAuthDataKeys, Security } from "./auth-types"; + +type Persistance = false | "localStorage" | "sessionStorage" | undefined; + +/** + * OpenAPI security array has an and/or relationship: + * + * // or ... + * "security": [ + * { + * "A": [] + * }, + * { + * "B": [] + * }, + * { + * "C": [] + * } + * ] + * + * // and ... + * "security": [ + * { + * "A": [], + * "B": [], + * "C": [] + * } + * ] + * + * This function merges the schemes into an array: + * + * security: [["A"], ["B"], ["C"]] + * + * security: [["A", "B", "C"]] + */ +function mergeSecurity({ + securitySchemes, + security, +}: { + securitySchemes: any; + security: any; +}): Security[][] { + return security.map( + (item: any) => + Object.entries(item) + .map(([key, value]) => { + if (!securitySchemes[key]) { + return undefined; + } + return { + ...securitySchemes[key], + key, + scopes: value, + }; + }) + .filter(Boolean) // throw out any schemes that can't be found. + ); +} + +function hydrateObject( + keys: string[], + { prefix, persistance }: { prefix: string; persistance: Persistance } +) { + if (persistance === false) { + return {}; + } + + if (persistance === "sessionStorage") { + return keys.reduce((acc, key) => { + return { + ...acc, + [key]: sessionStorage.getItem(`${prefix}-${key}`) ?? undefined, + }; + }, {}); + } + + // Default to localStorage + return keys.reduce((acc, key) => { + return { + ...acc, + [key]: localStorage.getItem(`${prefix}-${key}`) ?? undefined, + }; + }, {}); +} + +function storeObject( + obj: { [key: string]: string }, + { prefix, persistance }: { prefix: string; persistance: Persistance } +) { + if (persistance === false) { + return; + } + + if (persistance === "sessionStorage") { + for (const [key, value] of Object.entries(obj)) { + if (value === undefined) { + sessionStorage.removeItem(`${prefix}-${key}`); + } else { + sessionStorage.setItem(`${prefix}-${key}`, value); + } + } + return; + } + + // Default to localStorage + for (const [key, value] of Object.entries(obj)) { + if (value === undefined) { + localStorage.removeItem(`${prefix}-${key}`); + } else { + localStorage.setItem(`${prefix}-${key}`, value); + } + } +} + +function uniqueKey(relationship: Security[], security: Security) { + return relationship.map((r) => r.key).join("+") + ":" + security.key; +} + +export function loadAuth({ + securitySchemes, + security, + persistance, +}: { + securitySchemes: any; + security: any; + persistance: Persistance; +}) { + const securities = mergeSecurity({ securitySchemes, security }); + + return securities.map((relationship) => + relationship.map((security) => { + const data = hydrateObject(getAuthDataKeys(security), { + prefix: uniqueKey(relationship, security), + persistance, + }); + return { ...security, data }; + }) + ); +} + +export function persistAuth({ + security: securities, + persistance, +}: { + security: Security[][]; + persistance: Persistance; +}) { + if (persistance === false) { + return; + } + + for (const relationship of securities) { + for (const security of relationship) { + storeObject(security.data, { + prefix: uniqueKey(relationship, security), + persistance, + }); + } + } +} + +export function persistSelectedAuth({ + key, + selectedAuthID, + persistance, +}: { + key: string; + selectedAuthID: string; + persistance: Persistance; +}) { + if (persistance === false) { + return; + } + + if (persistance === "sessionStorage") { + sessionStorage.setItem(key, selectedAuthID); + } + + localStorage.setItem(key, selectedAuthID); +} + +export function loadSelectedAuth({ + key, + persistance, +}: { + key: string; + persistance: Persistance; +}): string | undefined { + if (persistance === false) { + return undefined; + } + + if (persistance === "sessionStorage") { + return sessionStorage.getItem(key) ?? undefined; + } + + return localStorage.getItem(key) ?? undefined; +} diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/reducer.js b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/reducer.ts similarity index 64% rename from packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/reducer.js rename to packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/reducer.ts index aa5228f9..b4599f15 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/reducer.js +++ b/packages/docusaurus-theme-openapi/src/theme/ApiDemoPanel/redux/reducer.ts @@ -8,13 +8,14 @@ import produce from "immer"; import { types } from "./actions"; +import { persistAuth, persistSelectedAuth } from "./persistance"; const reducer = produce((draft, action) => { switch (action.type) { case types.updateParam: { draft.params[action.param.type][ draft.params[action.param.type].findIndex( - (param) => param.name === action.param.name + (param: any) => param.name === action.param.name ) ] = action.param; break; @@ -39,7 +40,9 @@ const reducer = produce((draft, action) => { break; } case types.setEndpoint: { - draft.endpoint = draft.servers.find((s) => s.url === action.endpoint); + draft.endpoint = draft.servers.find( + (s: any) => s.url === action.endpoint + ); break; } case types.setEndpointValue: { @@ -50,14 +53,23 @@ const reducer = produce((draft, action) => { draft.contentType = action.contentType; break; } - case types.setBearerToken: { - sessionStorage.setItem("bearerToken", action.bearerToken); - draft.bearerToken = action.bearerToken; + case types.setAuth: { + // TODO: This is a side effect and shouldn't be done here. + persistAuth({ + security: action.auth, + persistance: draft.options.authPersistance, + }); + draft.auth = action.auth; break; } - case types.clearSession: { - sessionStorage.clear(); - draft.bearerToken = undefined; + case types.setSelectedAuthID: { + // TODO: This is a side effect and shouldn't be done here. + persistSelectedAuth({ + key: draft._uniqueAuthKey, + selectedAuthID: action.selectedAuthID, + persistance: draft.options.authPersistance, + }); + draft.selectedAuthID = action.selectedAuthID; break; } default: diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiItem/index.tsx b/packages/docusaurus-theme-openapi/src/theme/ApiItem/index.tsx index 78cfa275..29d82200 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiItem/index.tsx +++ b/packages/docusaurus-theme-openapi/src/theme/ApiItem/index.tsx @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ +import React from "react"; + import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment"; import type { Props } from "@theme/ApiItem"; import DocPaginator from "@theme/DocPaginator"; diff --git a/packages/docusaurus-theme-openapi/src/theme/ApiPage/index.tsx b/packages/docusaurus-theme-openapi/src/theme/ApiPage/index.tsx index 7f0d3127..c5e87af6 100644 --- a/packages/docusaurus-theme-openapi/src/theme/ApiPage/index.tsx +++ b/packages/docusaurus-theme-openapi/src/theme/ApiPage/index.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { ReactNode, useState, useCallback } from "react"; +import React, { ReactNode, useState, useCallback } from "react"; import renderRoutes from "@docusaurus/renderRoutes"; import { matchPath } from "@docusaurus/router"; diff --git a/packages/docusaurus-theme-openapi/src/types.ts b/packages/docusaurus-theme-openapi/src/types.ts index 22c8d1d0..bb3ef550 100644 --- a/packages/docusaurus-theme-openapi/src/types.ts +++ b/packages/docusaurus-theme-openapi/src/types.ts @@ -6,5 +6,8 @@ * ========================================================================== */ export interface ThemeConfig { - proxy: string; + api?: { + proxy?: string; + authPersistance?: false | "localStorage" | "sessionStorage"; + }; } diff --git a/packages/docusaurus-theme-openapi/tsconfig.json b/packages/docusaurus-theme-openapi/tsconfig.json index 3677b4b9..a86882b9 100644 --- a/packages/docusaurus-theme-openapi/tsconfig.json +++ b/packages/docusaurus-theme-openapi/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "lib": ["es2017", "es2019.array", "dom"], - "outDir": "dist" + "module": "esnext", + "outDir": "dist", + "jsx": "react" }, "include": ["src"] } diff --git a/yarn.lock b/yarn.lock index b0adbeaa..748cc5d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -125,6 +125,22 @@ "@algolia/logger-common" "4.11.0" "@algolia/requester-common" "4.11.0" +"@babel/cli@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.16.0.tgz#a729b7a48eb80b49f48a339529fc4129fd7bcef3" + integrity sha512-WLrM42vKX/4atIoQB+eb0ovUof53UUvecb4qGjU2PDDWRiZr50ZpiV8NpcLo7iSxeGYrRG0Mqembsa+UrTAV6Q== + dependencies: + commander "^4.0.1" + convert-source-map "^1.1.0" + fs-readdir-recursive "^1.1.0" + glob "^7.0.0" + make-dir "^2.1.0" + slash "^2.0.0" + source-map "^0.5.0" + optionalDependencies: + "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3" + chokidar "^3.4.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -166,7 +182,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.12.16", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.7.5": +"@babel/core@^7.1.0", "@babel/core@^7.12.16", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.7.5": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4" integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ== @@ -187,6 +203,15 @@ semver "^6.3.0" source-map "^0.5.0" +"@babel/eslint-parser@^7.16.3": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.16.3.tgz#2a6b1702f3f5aea48e00cea5a5bcc241c437e459" + integrity sha512-iB4ElZT0jAt7PKVaeVulOECdGe6UnmA/O0P9jlF5g5GBOwDVbna8AXhHRu4s27xQf6OkveyA8iTDv1jHdDejgQ== + dependencies: + eslint-scope "^5.1.1" + eslint-visitor-keys "^2.1.0" + semver "^6.3.0" + "@babel/generator@^7.12.15", "@babel/generator@^7.12.5", "@babel/generator@^7.16.0", "@babel/generator@^7.7.2": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" @@ -1111,7 +1136,7 @@ "@babel/plugin-transform-react-jsx-development" "^7.16.0" "@babel/plugin-transform-react-pure-annotations" "^7.16.0" -"@babel/preset-typescript@^7.12.16": +"@babel/preset-typescript@^7.12.16", "@babel/preset-typescript@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.0.tgz#b0b4f105b855fb3d631ec036cdc9d1ffd1fa5eac" integrity sha512-txegdrZYgO9DlPbv+9QOVpMnKbOtezsLHWsnsRF4AjbSIsVaujrq1qg8HK0mxQpWv0jnejt0yEoW1uWpvbrDTg== @@ -2577,6 +2602,11 @@ "@monaco-editor/loader" "^1.2.0" prop-types "^15.7.2" +"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": + version "2.1.8-no-fsevents.3" + resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" + integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -4749,7 +4779,7 @@ chokidar@^1.6.0: optionalDependencies: fsevents "^1.0.0" -chokidar@^3.4.2, chokidar@^3.5.2: +chokidar@^3.4.0, chokidar@^3.4.2, chokidar@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== @@ -5027,6 +5057,11 @@ commander@2.20.3, commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^4.0.1, commander@~4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + commander@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" @@ -5042,11 +5077,6 @@ commander@^8.1.0, commander@^8.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commander@~4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -5259,7 +5289,7 @@ conventional-recommended-bump@^6.1.0: meow "^8.0.0" q "^1.5.1" -convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: +convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== @@ -5381,6 +5411,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-fetch@^3.0.4: version "3.1.4" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" @@ -5388,7 +5425,7 @@ cross-fetch@^3.0.4: dependencies: node-fetch "2.6.1" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -6453,7 +6490,7 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -eslint-visitor-keys@^2.0.0: +eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== @@ -7189,6 +7226,11 @@ fs-monkey@1.0.3: resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== +fs-readdir-recursive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" + integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -13225,6 +13267,11 @@ sitemap@^7.0.0: arg "^5.0.0" sax "^1.2.4" +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"