← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~petermakowski/maas-site-manager:token-create-mock-api-MAASENG-1478 into maas-site-manager:main

 

Peter Makowski has proposed merging ~petermakowski/maas-site-manager:token-create-mock-api-MAASENG-1478 into maas-site-manager:main.

Commit message:
feat: token create mock API MAASENG-1478

Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/438987

- create post token mock api
this includes non-functional wireframe for create token form which will be implemented in a separate merge proposal
-- 
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:token-create-mock-api-MAASENG-1478 into maas-site-manager:main.
diff --git a/frontend/.eslintrc-auto-import.json b/frontend/.eslintrc-auto-import.json
new file mode 100644
index 0000000..fdd362d
--- /dev/null
+++ b/frontend/.eslintrc-auto-import.json
@@ -0,0 +1,42 @@
+{
+  "globals": {
+    "Link": true,
+    "NavLink": true,
+    "Navigate": true,
+    "Outlet": true,
+    "Route": true,
+    "Routes": true,
+    "createRef": true,
+    "forwardRef": true,
+    "lazy": true,
+    "memo": true,
+    "startTransition": true,
+    "useCallback": true,
+    "useContext": true,
+    "useDebugValue": true,
+    "useDeferredValue": true,
+    "useEffect": true,
+    "useHref": true,
+    "useId": true,
+    "useImperativeHandle": true,
+    "useInRouterContext": true,
+    "useInsertionEffect": true,
+    "useLayoutEffect": true,
+    "useLinkClickHandler": true,
+    "useLocation": true,
+    "useMemo": true,
+    "useNavigate": true,
+    "useNavigationType": true,
+    "useOutlet": true,
+    "useOutletContext": true,
+    "useParams": true,
+    "useReducer": true,
+    "useRef": true,
+    "useResolvedPath": true,
+    "useRoutes": true,
+    "useSearchParams": true,
+    "useState": true,
+    "useSyncExternalStore": true,
+    "useTransition": true
+  }
+}
\ No newline at end of file
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
index 5d9e234..9675872 100644
--- a/frontend/.eslintrc.js
+++ b/frontend/.eslintrc.js
@@ -4,6 +4,7 @@ module.exports = {
   root: true,
   plugins: ["prettier", "no-relative-import-paths"],
   extends: [
+    "./.eslintrc-auto-import.json",
     "react-app", // Use the recommended rules from CRA.
     "plugin:import/errors",
     "plugin:import/warnings",
diff --git a/frontend/auto-imports.d.ts b/frontend/auto-imports.d.ts
new file mode 100644
index 0000000..6d3f354
--- /dev/null
+++ b/frontend/auto-imports.d.ts
@@ -0,0 +1,45 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-auto-import
+export {}
+declare global {
+  const Link: typeof import('react-router-dom')['Link']
+  const NavLink: typeof import('react-router-dom')['NavLink']
+  const Navigate: typeof import('react-router-dom')['Navigate']
+  const Outlet: typeof import('react-router-dom')['Outlet']
+  const Route: typeof import('react-router-dom')['Route']
+  const Routes: typeof import('react-router-dom')['Routes']
+  const createRef: typeof import('react')['createRef']
+  const forwardRef: typeof import('react')['forwardRef']
+  const lazy: typeof import('react')['lazy']
+  const memo: typeof import('react')['memo']
+  const startTransition: typeof import('react')['startTransition']
+  const useCallback: typeof import('react')['useCallback']
+  const useContext: typeof import('react')['useContext']
+  const useDebugValue: typeof import('react')['useDebugValue']
+  const useDeferredValue: typeof import('react')['useDeferredValue']
+  const useEffect: typeof import('react')['useEffect']
+  const useHref: typeof import('react-router-dom')['useHref']
+  const useId: typeof import('react')['useId']
+  const useImperativeHandle: typeof import('react')['useImperativeHandle']
+  const useInRouterContext: typeof import('react-router-dom')['useInRouterContext']
+  const useInsertionEffect: typeof import('react')['useInsertionEffect']
+  const useLayoutEffect: typeof import('react')['useLayoutEffect']
+  const useLinkClickHandler: typeof import('react-router-dom')['useLinkClickHandler']
+  const useLocation: typeof import('react-router-dom')['useLocation']
+  const useMemo: typeof import('react')['useMemo']
+  const useNavigate: typeof import('react-router-dom')['useNavigate']
+  const useNavigationType: typeof import('react-router-dom')['useNavigationType']
+  const useOutlet: typeof import('react-router-dom')['useOutlet']
+  const useOutletContext: typeof import('react-router-dom')['useOutletContext']
+  const useParams: typeof import('react-router-dom')['useParams']
+  const useReducer: typeof import('react')['useReducer']
+  const useRef: typeof import('react')['useRef']
+  const useResolvedPath: typeof import('react-router-dom')['useResolvedPath']
+  const useRoutes: typeof import('react-router-dom')['useRoutes']
+  const useSearchParams: typeof import('react-router-dom')['useSearchParams']
+  const useState: typeof import('react')['useState']
+  const useSyncExternalStore: typeof import('react')['useSyncExternalStore']
+  const useTransition: typeof import('react')['useTransition']
+}
diff --git a/frontend/package.json b/frontend/package.json
index 757e451..add2a15 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -66,6 +66,7 @@
     "timezone-mock": "1.3.6",
     "typescript": "4.9.3",
     "unique-names-generator": "4.7.1",
+    "unplugin-auto-import": "0.15.1",
     "vite": "4.1.4",
     "vitest": "0.28.5"
   },
diff --git a/frontend/src/api/handlers.test.ts b/frontend/src/api/handlers.test.ts
new file mode 100644
index 0000000..07c3182
--- /dev/null
+++ b/frontend/src/api/handlers.test.ts
@@ -0,0 +1,25 @@
+import { postTokens } from "./handlers";
+
+import urls from "@/api/urls";
+import { tokenFactory } from "@/mocks/factories";
+import { createMockTokensResolver } from "@/mocks/resolvers";
+import { createMockPostServer } from "@/mocks/server";
+
+const mockServer = createMockPostServer(urls.tokens, createMockTokensResolver());
+
+beforeAll(() => {
+  mockServer.listen();
+});
+afterEach(() => {
+  mockServer.resetHandlers();
+});
+afterAll(() => {
+  mockServer.close();
+});
+
+describe("postTokens handler", () => {
+  it("requires name, amount and expiration time", async () => {
+    // @ts-expect-error
+    await expect(postTokens({})).rejects.toThrowError();
+  });
+});
diff --git a/frontend/src/api/handlers.ts b/frontend/src/api/handlers.ts
index 6bc29f1..a1769fe 100644
--- a/frontend/src/api/handlers.ts
+++ b/frontend/src/api/handlers.ts
@@ -22,3 +22,21 @@ export const getSites = async (params: GetSitesQueryParams, queryText?: string) 
     console.error(error);
   }
 };
+
+export type PostTokensData = {
+  amount: number;
+  name: string;
+  expires: string; // <ISO 8601 date string>,
+};
+
+export const postTokens = async (data: PostTokensData) => {
+  if (!data?.amount || !data?.name || !data?.expires) {
+    throw Error("Missing required fields");
+  }
+  try {
+    const response = await api.post(urls.tokens, data);
+    return response.data;
+  } catch (error) {
+    throw error;
+  }
+};
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index f4f18e3..7654746 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -29,3 +29,13 @@ export type PaginatedQueryResult = {
 export type SitesQueryResult = PaginatedQueryResult & {
   items: Site[];
 };
+
+export type Token = {
+  name: string;
+  token: string;
+  expires: string; //<ISO 8601 date string>,
+  created: string; //<ISO 8601 date string>
+};
+export type PostTokensResult = {
+  items: Token[];
+};
diff --git a/frontend/src/api/urls.ts b/frontend/src/api/urls.ts
index b303dd1..5d2fe6e 100644
--- a/frontend/src/api/urls.ts
+++ b/frontend/src/api/urls.ts
@@ -2,6 +2,7 @@ import { getApiUrl } from "./utils";
 
 const urls = {
   sites: getApiUrl("/sites"),
+  tokens: getApiUrl("/tokens"),
 };
 
 export default urls;
diff --git a/frontend/src/components/TokensCreate/TokensCreate.test.tsx b/frontend/src/components/TokensCreate/TokensCreate.test.tsx
new file mode 100644
index 0000000..a3b8abd
--- /dev/null
+++ b/frontend/src/components/TokensCreate/TokensCreate.test.tsx
@@ -0,0 +1,10 @@
+import TokensCreate from "./TokensCreate";
+
+import { render, screen } from "@/test-utils";
+
+describe("TokensCreate", () => {
+  it("renders the form", async () => {
+    render(<TokensCreate />);
+    expect(screen.getByRole("form", { name: /Generate new enrollment tokens/i })).toBeInTheDocument();
+  });
+});
diff --git a/frontend/src/components/TokensCreate/TokensCreate.tsx b/frontend/src/components/TokensCreate/TokensCreate.tsx
new file mode 100644
index 0000000..b2d13df
--- /dev/null
+++ b/frontend/src/components/TokensCreate/TokensCreate.tsx
@@ -0,0 +1,26 @@
+import { useId } from "react";
+
+import { Form, Input, Button } from "@canonical/react-components";
+
+const TokensCreate = () => {
+  const id = useId();
+  return (
+    <div>
+      <Form aria-labelledby={id}>
+        <h3 id={id}>Generate new enrollment tokens</h3>
+        <Input label="Amount of tokens to generate" type="number" />
+        <Input label="Expiration time" type="text" />
+        <p className="u-text--muted">
+          Use this token once to request an enrolment in the specified timeframe. <br />
+          Allowed time units are seconds, minutes, hours, days and weeks.
+        </p>
+        <Button type="button">Cancel</Button>
+        <Button appearance="positive" type="submit">
+          Generate token
+        </Button>
+      </Form>
+    </div>
+  );
+};
+
+export default TokensCreate;
diff --git a/frontend/src/components/TokensCreate/index.ts b/frontend/src/components/TokensCreate/index.ts
new file mode 100644
index 0000000..c42db32
--- /dev/null
+++ b/frontend/src/components/TokensCreate/index.ts
@@ -0,0 +1 @@
+export { default } from "./TokensCreate";
diff --git a/frontend/src/components/TokensList/TokensList.tsx b/frontend/src/components/TokensList/TokensList.tsx
new file mode 100644
index 0000000..4125a07
--- /dev/null
+++ b/frontend/src/components/TokensList/TokensList.tsx
@@ -0,0 +1,28 @@
+import { Button, Col, Row } from "@canonical/react-components";
+import { Outlet, Link } from "react-router-dom";
+
+const TokensList = () => {
+  return (
+    <section>
+      <Row>
+        <Col size={2}>
+          <h2 className="p-heading--4">Tokens</h2>
+        </Col>
+      </Row>
+      <Row>
+        <Col size={12}>
+          <Button>Export</Button>
+          <Button appearance="negative">Delete</Button>
+          <Link to="create">
+            <Button appearance="positive" element="a">
+              Generate tokens
+            </Button>
+          </Link>
+        </Col>
+      </Row>
+      <Outlet />
+    </section>
+  );
+};
+
+export default TokensList;
diff --git a/frontend/src/components/TokensList/index.ts b/frontend/src/components/TokensList/index.ts
new file mode 100644
index 0000000..c19b7b6
--- /dev/null
+++ b/frontend/src/components/TokensList/index.ts
@@ -0,0 +1 @@
+export { default } from "./TokensList";
diff --git a/frontend/src/mocks/factories.ts b/frontend/src/mocks/factories.ts
index 9703559..802c411 100644
--- a/frontend/src/mocks/factories.ts
+++ b/frontend/src/mocks/factories.ts
@@ -1,9 +1,8 @@
+import type { Site, Token } from "api/types";
 import Chance from "chance";
 import { Factory } from "fishery";
 import { uniqueNamesGenerator, adjectives, colors, animals } from "unique-names-generator";
 
-import type { Site } from "api/types";
-
 const connections: Site["connection"][] = ["stable", "lost", "stale", "unstable"];
 
 export const siteFactory = Factory.define<Site>(({ sequence }) => {
@@ -39,3 +38,13 @@ export const siteFactory = Factory.define<Site>(({ sequence }) => {
     },
   };
 });
+
+export const tokenFactory = Factory.define<Token>(({ sequence }) => {
+  const chance = new Chance(`maas-${sequence}`);
+  return {
+    name: `${sequence}`,
+    token: chance.hash({ length: 32 }),
+    expires: new Date(chance.date({ year: 2024 })).toISOString(), //<ISO 8601 date string>,
+    created: new Date(chance.date({ year: 2023 })).toISOString(), //<ISO 8601 date string>
+  };
+});
diff --git a/frontend/src/mocks/resolvers.test.ts b/frontend/src/mocks/resolvers.test.ts
new file mode 100644
index 0000000..f09733e
--- /dev/null
+++ b/frontend/src/mocks/resolvers.test.ts
@@ -0,0 +1,28 @@
+import axios from "axios";
+
+import urls from "@/api/urls";
+import { tokenFactory } from "@/mocks/factories";
+import { createMockTokensResolver } from "@/mocks/resolvers";
+import { createMockPostServer } from "@/mocks/server";
+
+const mockServer = createMockPostServer(urls.tokens, createMockTokensResolver());
+
+beforeAll(() => {
+  mockServer.listen();
+});
+afterEach(() => {
+  mockServer.resetHandlers();
+});
+afterAll(() => {
+  mockServer.close();
+});
+
+describe("mock post tokens server", () => {
+  it("returns list of tokens based on the request data", async () => {
+    const amount = 1;
+    const { name, expires } = tokenFactory.build({ name: "test", expires: "2021-01-01" });
+    const result = await axios.post(urls.tokens, { name, expires, amount });
+    expect(result.data.items).toHaveLength(amount);
+    expect(result.data.items[0]).toEqual(expect.objectContaining({ name, expires }));
+  });
+});
diff --git a/frontend/src/mocks/resolvers.ts b/frontend/src/mocks/resolvers.ts
index b76d7c4..6e6e9f9 100644
--- a/frontend/src/mocks/resolvers.ts
+++ b/frontend/src/mocks/resolvers.ts
@@ -1,8 +1,7 @@
+import type { GetSitesQueryParams, PostTokensData } from "api/handlers";
 import type { RestRequest, restContext, ResponseResolver } from "msw";
 
-import { siteFactory } from "./factories";
-
-import type { GetSitesQueryParams } from "api/handlers";
+import { siteFactory, tokenFactory } from "./factories";
 
 export const sitesList = siteFactory.buildList(155);
 
@@ -22,3 +21,17 @@ export const createMockSitesResolver =
 
     return res(ctx.json(response));
   };
+
+type TokensResponseResolver = ResponseResolver<RestRequest<PostTokensData>, typeof restContext>;
+export const createMockTokensResolver = (): TokensResponseResolver => async (req, res, ctx) => {
+  let items;
+  const { amount, name, expires } = await req.json();
+  if (amount && name && expires) {
+    items = Array(amount).fill(tokenFactory.build({ name, expires }));
+  }
+  const response = {
+    items,
+  };
+
+  return res(ctx.json(response));
+};
diff --git a/frontend/src/mocks/server.ts b/frontend/src/mocks/server.ts
index 71d89f6..ebe2d52 100644
--- a/frontend/src/mocks/server.ts
+++ b/frontend/src/mocks/server.ts
@@ -8,7 +8,10 @@ import urls from "@/api/urls";
 
 const createMockGetServer = (endpoint: string, resolver: ReturnType<typeof createMockSitesResolver>) =>
   setupServer(rest.get(endpoint, resolver));
+const createMockPostServer = (endpoint: string, resolver: ReturnType<typeof createMockSitesResolver>) =>
+  setupServer(rest.post(endpoint, resolver));
 
 const mockSitesServer = createMockGetServer(urls.sites, createMockSitesResolver());
+const mockPostTokensServer = createMockPostServer(urls.tokens, createMockSitesResolver());
 
-export { createMockGetServer, mockSitesServer };
+export { createMockGetServer, createMockPostServer, mockSitesServer, mockPostTokensServer };
diff --git a/frontend/src/pages/tokens.tsx b/frontend/src/pages/tokens.tsx
deleted file mode 100644
index c9452aa..0000000
--- a/frontend/src/pages/tokens.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-const Tokens: React.FC = () => (
-  <section>
-    <h2>Tokens</h2>
-  </section>
-);
-
-export default Tokens;
diff --git a/frontend/src/pages/tokens/create.tsx b/frontend/src/pages/tokens/create.tsx
new file mode 100644
index 0000000..103a4a6
--- /dev/null
+++ b/frontend/src/pages/tokens/create.tsx
@@ -0,0 +1,5 @@
+import TokensCreate from "@/components/TokensCreate";
+
+const Create: React.FC = () => <TokensCreate />;
+
+export default Create;
diff --git a/frontend/src/pages/tokens/index.ts b/frontend/src/pages/tokens/index.ts
new file mode 100644
index 0000000..6ea397c
--- /dev/null
+++ b/frontend/src/pages/tokens/index.ts
@@ -0,0 +1 @@
+export { default } from "./tokens";
diff --git a/frontend/src/pages/tokens/tokens.tsx b/frontend/src/pages/tokens/tokens.tsx
new file mode 100644
index 0000000..9b1b53e
--- /dev/null
+++ b/frontend/src/pages/tokens/tokens.tsx
@@ -0,0 +1,5 @@
+import TokensList from "@/components/TokensList";
+
+const Tokens: React.FC = () => <TokensList />;
+
+export default Tokens;
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index 0ff2e9a..5009017 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -1,7 +1,9 @@
 import { createRoutesFromElements, Route, redirect } from "react-router-dom";
 
-import MainLayout from "./components/MainLayout";
-import SitesList from "./pages/sites";
+import MainLayout from "@/components/MainLayout";
+import SitesList from "@/pages/sites";
+import TokensCreate from "@/pages/tokens/create";
+import Tokens from "@/pages/tokens/tokens";
 
 export const routes = createRoutesFromElements(
   <Route element={<MainLayout />} path="/">
@@ -14,7 +16,9 @@ export const routes = createRoutesFromElements(
     <Route path="logout" />
     <Route element={<SitesList />} path="sites" />
     <Route path="requests" />
-    <Route path="tokens" />
+    <Route element={<Tokens />} path="tokens">
+      <Route element={<TokensCreate />} path="create" />
+    </Route>
     <Route path="users" />
   </Route>,
 );
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 02a04a1..8a536d7 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -1,5 +1,6 @@
 import { defineConfig } from "vite";
 import react from "@vitejs/plugin-react-swc";
+import AutoImport from "unplugin-auto-import/vite";
 import dotenv from "dotenv";
 import * as path from "path";
 
@@ -7,7 +8,16 @@ dotenv.config({ path: "../.env" });
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [react()],
+  plugins: [
+    react(),
+    AutoImport({
+      imports: ["react", "react-router-dom"],
+      dts: true,
+      eslintrc: {
+        enabled: true, // <-- this
+      },
+    }),
+  ],
   server: { port: Number(process.env.VITE_UI_PORT) },
   resolve: {
     alias: { "@": path.resolve(__dirname, "src") },
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 5f07629..567d215 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -15,6 +15,11 @@
     "@jridgewell/gen-mapping" "^0.1.0"
     "@jridgewell/trace-mapping" "^0.3.9"
 
+"@antfu/utils@^0.7.2":
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.7.2.tgz#3bb6f37a6b188056fe9e2f363b6aa735ed65d7ca";
+  integrity sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g==
+
 "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6":
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a";
@@ -1289,7 +1294,7 @@
     "@jridgewell/gen-mapping" "^0.3.0"
     "@jridgewell/trace-mapping" "^0.3.9"
 
-"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
+"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13":
   version "1.4.14"
   resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24";
   integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
@@ -1419,6 +1424,15 @@
   dependencies:
     web-streams-polyfill "^3.1.1"
 
+"@rollup/pluginutils@^5.0.2":
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33";
+  integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==
+  dependencies:
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^2.3.1"
+
 "@rushstack/eslint-patch@^1.1.0":
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728";
@@ -1617,6 +1631,11 @@
   dependencies:
     "@types/ms" "*"
 
+"@types/estree@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2";
+  integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
+
 "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44";
@@ -2357,6 +2376,13 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
+brace-expansion@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae";
+  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  dependencies:
+    balanced-match "^1.0.0"
+
 braces@^3.0.2, braces@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107";
@@ -2494,7 +2520,7 @@ check-error@^1.0.2:
   resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82";
   integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
 
-"chokidar@>=3.0.0 <4.0.0", chokidar@^3.3.0, chokidar@^3.4.2:
+"chokidar@>=3.0.0 <4.0.0", chokidar@^3.3.0, chokidar@^3.4.2, chokidar@^3.5.3:
   version "3.5.3"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd";
   integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@@ -3063,6 +3089,11 @@ escape-string-regexp@^4.0.0:
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34";
   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
 
+escape-string-regexp@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8";
+  integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
+
 escodegen@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd";
@@ -3361,6 +3392,11 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123";
   integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
 
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac";
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
 esutils@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64";
@@ -3414,7 +3450,7 @@ fast-diff@^1.1.2:
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03";
   integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
 
-fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9:
+fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.7, fast-glob@^3.2.9:
   version "3.2.12"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80";
   integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
@@ -4416,7 +4452,7 @@ lines-and-columns@^1.1.6:
   resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632";
   integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
 
-local-pkg@^0.4.2:
+local-pkg@^0.4.2, local-pkg@^0.4.3:
   version "0.4.3"
   resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963";
   integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==
@@ -4496,6 +4532,13 @@ lz-string@^1.4.4:
   resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26";
   integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==
 
+magic-string@^0.30.0:
+  version "0.30.0"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529";
+  integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.4.13"
+
 map-obj@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d";
@@ -4566,6 +4609,13 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
   dependencies:
     brace-expansion "^1.1.7"
 
+minimatch@^7.4.2:
+  version "7.4.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.2.tgz#157e847d79ca671054253b840656720cb733f10f";
+  integrity sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==
+  dependencies:
+    brace-expansion "^2.0.1"
+
 minimist-options@4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619";
@@ -4590,6 +4640,16 @@ mlly@^1.0.0, mlly@^1.1.0:
     pkg-types "^1.0.1"
     ufo "^1.0.1"
 
+mlly@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.2.0.tgz#f0f6c2fc8d2d12ea6907cd869066689b5031b613";
+  integrity sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==
+  dependencies:
+    acorn "^8.8.2"
+    pathe "^1.1.0"
+    pkg-types "^1.0.2"
+    ufo "^1.1.1"
+
 mockdate@3.0.5:
   version "3.0.5"
   resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.5.tgz#789be686deb3149e7df2b663d2bc4392bc3284fb";
@@ -4998,6 +5058,15 @@ pkg-types@^1.0.1:
     mlly "^1.0.0"
     pathe "^1.0.0"
 
+pkg-types@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.2.tgz#c233efc5210a781e160e0cafd60c0d0510a4b12e";
+  integrity sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==
+  dependencies:
+    jsonc-parser "^3.2.0"
+    mlly "^1.1.1"
+    pathe "^1.1.0"
+
 playwright-core@1.31.1:
   version "1.31.1"
   resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.31.1.tgz#4deeebbb8fb73b512593fe24bea206d8fd85ff7f";
@@ -5496,6 +5565,11 @@ scheduler@^0.23.0:
   dependencies:
     loose-envify "^1.1.0"
 
+scule@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/scule/-/scule-1.0.0.tgz#895e6f4ba887e78d8b9b4111e23ae84fef82376d";
+  integrity sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==
+
 "semver@2 || 3 || 4 || 5":
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7";
@@ -5752,7 +5826,7 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006";
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
-strip-literal@^1.0.0:
+strip-literal@^1.0.0, strip-literal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.0.1.tgz#0115a332710c849b4e46497891fb8d585e404bd2";
   integrity sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==
@@ -5995,6 +6069,11 @@ ufo@^1.0.1:
   resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.0.1.tgz#64ed43b530706bda2e4892f911f568cf4cf67d29";
   integrity sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==
 
+ufo@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.1.tgz#e70265e7152f3aba425bd013d150b2cdf4056d7c";
+  integrity sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==
+
 unbox-primitive@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e";
@@ -6028,6 +6107,23 @@ unicode-property-aliases-ecmascript@^2.0.0:
   resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd";
   integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
 
+unimport@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/unimport/-/unimport-3.0.2.tgz#c7016df38775e03c4bc7682b0194c98236e308db";
+  integrity sha512-OQ0hShpcerS1PSsISsyn/NV2dGe5xfdUn4p5nwOodq0iqq5xxYQrTidHqlFGjxIliPDtDJp80OeySzyPTjYHmA==
+  dependencies:
+    "@rollup/pluginutils" "^5.0.2"
+    escape-string-regexp "^5.0.0"
+    fast-glob "^3.2.12"
+    local-pkg "^0.4.3"
+    magic-string "^0.30.0"
+    mlly "^1.1.1"
+    pathe "^1.1.0"
+    pkg-types "^1.0.2"
+    scule "^1.0.0"
+    strip-literal "^1.0.1"
+    unplugin "^1.1.0"
+
 unique-names-generator@4.7.1:
   version "4.7.1"
   resolved "https://registry.yarnpkg.com/unique-names-generator/-/unique-names-generator-4.7.1.tgz#966407b12ba97f618928f77322cfac8c80df5597";
@@ -6043,6 +6139,29 @@ universalify@^2.0.0:
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717";
   integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
 
+unplugin-auto-import@0.15.1:
+  version "0.15.1"
+  resolved "https://registry.yarnpkg.com/unplugin-auto-import/-/unplugin-auto-import-0.15.1.tgz#c7f1c01ad8676231c3711ca7bbf56dd5339586a2";
+  integrity sha512-xLS+BfVNy00Y3IkqBmEd0IThvjx8kSGIgSuf/1kETttiENK8sHrDA+poKkQxRCPTKYH4yWM6txGQANPTzwpUWQ==
+  dependencies:
+    "@antfu/utils" "^0.7.2"
+    "@rollup/pluginutils" "^5.0.2"
+    local-pkg "^0.4.3"
+    magic-string "^0.30.0"
+    minimatch "^7.4.2"
+    unimport "^3.0.2"
+    unplugin "^1.1.0"
+
+unplugin@^1.1.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.3.1.tgz#7af993ba8695d17d61b0845718380caf6af5109f";
+  integrity sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==
+  dependencies:
+    acorn "^8.8.2"
+    chokidar "^3.5.3"
+    webpack-sources "^3.2.3"
+    webpack-virtual-modules "^0.5.0"
+
 update-browserslist-db@^1.0.10:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3";
@@ -6251,6 +6370,16 @@ webidl-conversions@^7.0.0:
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a";
   integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
 
+webpack-sources@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde";
+  integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
+webpack-virtual-modules@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c";
+  integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==
+
 whatwg-encoding@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53";

Follow ups