0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor: update title in main flow and report pv for console sign-in (#3651)

This commit is contained in:
Gao Sun 2023-04-03 15:40:56 +08:00 committed by GitHub
parent 1d4662ebc1
commit 0e49e43245
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 163 additions and 51 deletions

View file

@ -9,11 +9,9 @@
"lib"
],
"exports": {
".": {
"import": "./lib/index.js"
},
"./*": {
"import": "./lib/*"
"import": "./lib/*.js",
"types": "./lib/*.d.ts"
}
},
"publishConfig": {

View file

@ -6,7 +6,7 @@ import { findUp } from 'find-up';
dotenv.config({ path: await findUp('.env', {}) });
const { appInsights } = await import('@logto/app-insights/node.js');
const { appInsights } = await import('@logto/app-insights/node');
if (appInsights.setup('logto-cloud')) {
console.debug('Initialized ApplicationInsights');

View file

@ -1,4 +1,4 @@
import { appInsights } from '@logto/app-insights/node.js';
import { appInsights } from '@logto/app-insights/node';
import { tryThat } from '@silverhand/essentials';
import type { BaseContext, NextFunction } from '@withtyped/server';

View file

@ -29,6 +29,7 @@ import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider';
if (appInsightsReact.setup()) {
console.debug('Initialized ApplicationInsights');
}
void initI18n();
function Content() {

View file

@ -1,7 +1,7 @@
import fs from 'node:fs/promises';
import http2 from 'node:http2';
import { appInsights } from '@logto/app-insights/node.js';
import { appInsights } from '@logto/app-insights/node';
import { toTitle, trySafe } from '@silverhand/essentials';
import chalk from 'chalk';
import type Koa from 'koa';

View file

@ -8,7 +8,7 @@ import SystemContext from './tenants/SystemContext.js';
dotenv.config({ path: await findUp('.env', {}) });
const { appInsights } = await import('@logto/app-insights/node.js');
const { appInsights } = await import('@logto/app-insights/node');
if (appInsights.setup('logto')) {
console.debug('Initialized ApplicationInsights');

View file

@ -1,4 +1,4 @@
import { appInsights } from '@logto/app-insights/node.js';
import { appInsights } from '@logto/app-insights/node';
import type { RequestErrorBody } from '@logto/schemas';
import type { Middleware } from 'koa';
import { HttpError } from 'koa';

View file

@ -4,7 +4,13 @@ import { readFileSync } from 'node:fs';
import { userClaims } from '@logto/core-kit';
import type { I18nKey } from '@logto/phrases';
import { CustomClientMetadataKey, demoAppApplicationId } from '@logto/schemas';
import {
CustomClientMetadataKey,
demoAppApplicationId,
logtoCookieKey,
type LogtoUiCookie,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import i18next from 'i18next';
import Provider, { errors, type ResourceServer } from 'oidc-provider';
import snakecaseKeys from 'snakecase-keys';
@ -145,15 +151,23 @@ export default function initOidc(
},
},
interactions: {
url: (ctx, interaction) => {
const isDemoApp = interaction.params.client_id === demoAppApplicationId;
url: (ctx, { params: { client_id: appId }, prompt }) => {
const isDemoApp = appId === demoAppApplicationId;
ctx.cookies.set(
logtoCookieKey,
JSON.stringify({
appId: conditional(Boolean(appId) && String(appId)),
} satisfies LogtoUiCookie),
{ sameSite: 'lax', overwrite: true, httpOnly: false }
);
const appendParameters = (path: string) => {
// `notification` is for showing a text banner on the homepage
return isDemoApp ? path + `?notification=demo_app.notification&no_cache` : path;
};
switch (interaction.prompt.name) {
switch (prompt.name) {
case 'login': {
const isSignUp =
ctx.oidc.params?.[OIDCExtraParametersKey.InteractionMode] === InteractionMode.signUp;
@ -166,7 +180,7 @@ export default function initOidc(
}
default: {
throw new Error(`Prompt not supported: ${interaction.prompt.name}`);
throw new Error(`Prompt not supported: ${prompt.name}`);
}
}
},

View file

@ -60,6 +60,8 @@ const description = {
create_your_account: 'Erstelle dein Konto',
sign_in_to_your_account: 'Melde dich in deinem Konto an',
no_region_code_found: 'Kein Regionencode gefunden',
verify_email: 'Bestätige deine E-Mail-Adresse',
verify_phone: 'Bestätige deine Telefonnummer',
};
export default description;

View file

@ -57,6 +57,8 @@ const description = {
create_your_account: 'Create your account',
sign_in_to_your_account: 'Sign in to your account',
no_region_code_found: 'No region code found',
verify_email: 'Verify your email',
verify_phone: 'Verify your phone number',
};
export default description;

View file

@ -58,6 +58,8 @@ const description = {
create_your_account: 'Cree su cuenta',
sign_in_to_your_account: 'Inicie sesión en su cuenta',
no_region_code_found: 'No se encontró código de región',
verify_email: 'Verificar su correo electrónico',
verify_phone: 'Verificar su número de teléfono',
};
export default description;

View file

@ -60,6 +60,8 @@ const description = {
create_your_account: 'Créer votre compte',
sign_in_to_your_account: 'Connecte-toi à ton compte',
no_region_code_found: 'Aucun code de région trouvé',
verify_email: 'Vérifiez votre e-mail',
verify_phone: 'Vérifiez votre numéro de téléphone',
};
export default description;

View file

@ -56,6 +56,8 @@ const description = {
create_your_account: 'Crea il tuo account',
sign_in_to_your_account: 'Accedi al tuo account',
no_region_code_found: 'Nessun codice di regione trovato',
verify_email: 'Verifica la tua email',
verify_phone: 'Verifica il tuo numero di telefono',
};
export default description;

View file

@ -58,6 +58,8 @@ const description = {
create_your_account: 'アカウントを作成する',
sign_in_to_your_account: 'アカウントにサインインする',
no_region_code_found: '地域コードが見つかりません',
verify_email: 'Eメールを確認する',
verify_phone: '電話番号を確認する',
};
export default description;

View file

@ -53,6 +53,8 @@ const description = {
create_your_account: '계정 생성하기',
sign_in_to_your_account: '계정에 로그인하세요',
no_region_code_found: '지역 코드를 찾을 수 없습니다.',
verify_email: '이메일 인증',
verify_phone: '휴대전화번호 인증',
};
export default description;

View file

@ -1,4 +1,4 @@
const opis = {
const description = {
email: 'adres email',
phone_number: 'numer telefonu',
username: 'nazwa użytkownika',
@ -29,7 +29,7 @@ const opis = {
social_link_email: 'Możesz połączyć kolejny adres email',
social_link_phone: 'Możesz połączyć kolejny numer telefonu',
social_link_email_or_phone: 'Możesz połączyć kolejny adres email lub numer telefonu',
social_bind_with_existing: 'Znaleźliśmy powiązane konto, możesz go połączyć bezpośrednio.',
social_bind_with_existing: 'Znaleźliśmy powiązane konto, możesz je połączyć bezpośrednio.',
reset_password: 'Zresetuj hasło',
reset_password_description:
'Wpisz {{types, lista(type: złączonych;)}} związanego z twoim kontem, a wyślemy ci kod weryfikacyjny do zresetowania hasła.',
@ -56,6 +56,8 @@ const opis = {
create_your_account: 'Utwórz konto',
sign_in_to_your_account: 'Zaloguj się do swojego konta',
no_region_code_found: 'Nie znaleziono kodu regionu',
verify_email: 'Potwierdź swój email',
verify_phone: 'Potwierdź swój numer telefonu',
};
export default opis;
export default description;

View file

@ -55,6 +55,8 @@ const description = {
create_your_account: 'Crie sua conta',
sign_in_to_your_account: 'Faça login na sua conta',
no_region_code_found: 'Não foi possível encontrar o código de região do seu telefone.',
verify_email: 'Verificar e-mail',
verify_phone: 'Verificar número de telefone',
};
export default description;

View file

@ -55,6 +55,8 @@ const description = {
create_your_account: 'Crie a sua conta',
sign_in_to_your_account: 'Inicie sessão na sua conta',
no_region_code_found: 'Não foi possível encontrar o código de região do seu telefone.',
verify_email: 'Verifique o seu email',
verify_phone: 'Verifique o seu número de telefone',
};
export default description;

View file

@ -59,6 +59,8 @@ const description = {
create_your_account: 'Создайте свой аккаунт',
sign_in_to_your_account: 'Войди в свой аккаунт',
no_region_code_found: 'Не удалось определить код региона',
verify_email: 'Подтвердите Ваш электронный адрес',
verify_phone: 'Подтвердите свой номер телефона',
};
export default description;

View file

@ -56,6 +56,8 @@ const description = {
create_your_account: 'Hesabını oluştur',
sign_in_to_your_account: 'Hesabına giriş yap',
no_region_code_found: 'Bölge kodu bulunamadı',
verify_email: 'E-postanızın doğrulanması',
verify_phone: 'Telefon numaranızın doğrulanması',
};
export default description;

View file

@ -49,6 +49,8 @@ const description = {
create_your_account: '注册你的账号',
sign_in_to_your_account: '登录你的账号',
no_region_code_found: '没有找到区域码',
verify_email: '验证你的邮箱',
verify_phone: '验证你的手机号',
};
export default description;

View file

@ -49,6 +49,8 @@ const description = {
create_your_account: '註冊你的帳號',
sign_in_to_your_account: '登錄你的帳號',
no_region_code_found: '沒有找到區域碼',
verify_email: '驗證你的郵箱',
verify_phone: '驗證你的手機號',
};
export default description;

View file

@ -49,6 +49,8 @@ const description = {
create_your_account: '註冊你的帳號',
sign_in_to_your_account: '登錄你的帳號',
no_region_code_found: '沒有找到區域碼',
verify_email: '驗證你的郵箱',
verify_phone: '驗證你的手機號碼',
};
export default description;

View file

@ -0,0 +1 @@
export const logtoCookieKey = '_logto';

View file

@ -1,2 +1,3 @@
export * from './cookie.js';
export * from './system.js';
export * from './oidc.js';

View file

@ -0,0 +1,5 @@
import { z } from 'zod';
export const logtoUiCookieGuard = z.object({ appId: z.string() }).partial();
export type LogtoUiCookie = z.infer<typeof logtoUiCookieGuard>;

View file

@ -16,3 +16,4 @@ export * from './user-assets.js';
export * from './hook.js';
export * from './service-log.js';
export * from './theme.js';
export * from './cookie.js';

View file

@ -9,6 +9,9 @@ const config: Config.InitialOptions = {
'\\.(svg)$': 'jest-transformer-svg',
'\\.(png)$': 'jest-transform-stub',
},
moduleNameMapper: {
'^@logto/app-insights/(.*)$': '<rootDir>/node_modules/@logto/app-insights/$1',
},
}),
// Will update common config soon
transformIgnorePatterns: ['node_modules/(?!(.*(nanoid|jose|ky|@logto|@silverhand))/)'],

View file

@ -17,6 +17,7 @@
"test": "jest"
},
"devDependencies": {
"@logto/app-insights": "workspace:^",
"@logto/connector-kit": "workspace:^",
"@logto/core-kit": "workspace:^",
"@logto/language-kit": "workspace:^",
@ -42,6 +43,7 @@
"@types/jest": "^29.4.0",
"@types/react": "^18.0.31",
"@types/react-dom": "^18.0.0",
"@types/react-helmet": "^6.1.6",
"@types/react-modal": "^3.13.1",
"@types/react-router-dom": "^5.3.2",
"camelcase-keys": "^8.0.0",
@ -56,7 +58,7 @@
"jest-environment-jsdom": "^29.0.0",
"jest-transform-stub": "^2.0.0",
"jest-transformer-svg": "^2.0.0",
"js-base64": "^3.7.2",
"js-base64": "^3.7.5",
"ky": "^0.33.0",
"libphonenumber-js": "^1.9.49",
"lint-staged": "^13.0.0",
@ -68,6 +70,7 @@
"react": "^18.0.0",
"react-device-detect": "^2.2.2",
"react-dom": "^18.0.0",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.34.0",
"react-i18next": "^11.18.3",
"react-modal": "^3.15.1",
@ -77,6 +80,7 @@
"react-top-loading-bar": "^2.3.1",
"stylelint": "^15.0.0",
"superstruct": "^0.16.0",
"tiny-cookie": "^2.4.1",
"ts-jest": "^29.0.5",
"typescript": "^5.0.0",
"use-debounced-loader": "^0.1.1",

View file

@ -1,3 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/lib/react';
import { Route, Routes, BrowserRouter } from 'react-router-dom';
import AppLayout from './Layout/AppLayout';
@ -20,10 +21,15 @@ import SocialLinkAccount from './pages/SocialLinkAccount';
import SocialSignIn from './pages/SocialSignInCallback';
import Springboard from './pages/Springboard';
import VerificationCode from './pages/VerificationCode';
import { shouldTrack } from './utils/cookies';
import { handleSearchParametersData } from './utils/search-parameters';
import './scss/normalized.scss';
if (shouldTrack && appInsightsReact.setup()) {
console.debug('Initialized ApplicationInsights');
}
handleSearchParametersData();
const App = () => {

View file

@ -1,28 +1,15 @@
import classNames from 'classnames';
import { useEffect } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { Outlet } from 'react-router-dom';
import LogtoSignature from '@/components/LogtoSignature';
import usePlatform from '@/hooks/use-platform';
import { layoutClassNames } from '@/utils/consts';
import { parseHtmlTitle } from '@/utils/sign-in-experience';
import CustomContent from './CustomContent';
import * as styles from './index.module.scss';
const AppLayout = () => {
const { isMobile } = usePlatform();
const location = useLocation();
useEffect(() => {
const { pathname } = location;
const title = parseHtmlTitle(pathname);
if (title) {
// eslint-disable-next-line @silverhand/fp/no-mutation
document.title = title;
}
}, [location]);
return (
<div className={styles.viewBox}>

View file

@ -5,6 +5,7 @@ import type { TFuncKey } from 'react-i18next';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import BrandingHeader from '@/components/BrandingHeader';
import PageMeta from '@/components/PageMeta';
import { layoutClassNames } from '@/utils/consts';
import { getBrandingLogoUrl } from '@/utils/logo';
@ -15,7 +16,7 @@ import * as styles from './index.module.scss';
type Props = {
children: ReactNode;
className?: string;
title?: TFuncKey;
title: TFuncKey;
};
const LandingPageLayout = ({ children, className, title }: Props) => {
@ -32,6 +33,7 @@ const LandingPageLayout = ({ children, className, title }: Props) => {
return (
<>
<PageMeta titleKey={title} />
{platform === 'web' && <div className={styles.placeholderTop} />}
<div className={classNames(styles.wrapper, className)}>
<BrandingHeader

View file

@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
import type { TFuncKey } from 'react-i18next';
import NavBar from '@/components/NavBar';
import PageMeta from '@/components/PageMeta';
import usePlatform from '@/hooks/use-platform';
import { InlineNotification } from '../../components/Notification';
@ -9,7 +10,7 @@ import { InlineNotification } from '../../components/Notification';
import * as styles from './index.module.scss';
type Props = {
title?: TFuncKey;
title: TFuncKey;
description?: TFuncKey;
titleProps?: Record<string, unknown>;
descriptionProps?: Record<string, unknown>;
@ -30,13 +31,14 @@ const SecondaryPageLayout = ({
return (
<div className={styles.wrapper}>
<PageMeta titleKey={title} />
<NavBar />
{isMobile && notification && (
<InlineNotification message={notification} className={styles.notification} />
)}
<div className={styles.container}>
<div className={styles.header}>
{title && <div className={styles.title}>{t(title, titleProps)}</div>}
<div className={styles.title}>{t(title, titleProps)}</div>
{description && (
<div className={styles.description}>{t(description, descriptionProps)}</div>
)}

View file

@ -0,0 +1,32 @@
import { appInsightsReact } from '@logto/app-insights/lib/react';
import { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { type TFuncKey, useTranslation } from 'react-i18next';
import { shouldTrack } from '@/utils/cookies';
type Props = {
titleKey: TFuncKey | TFuncKey[];
// eslint-disable-next-line react/boolean-prop-naming
trackPageView?: boolean;
};
const PageMeta = ({ titleKey, trackPageView = true }: Props) => {
const { t } = useTranslation();
const [pageViewTracked, setPageViewTracked] = useState(false);
const keys = typeof titleKey === 'string' ? [titleKey] : titleKey;
const rawTitle = keys.map((key) => t(key, { lng: 'en' })).join(' - ');
const title = keys.map((key) => t(key)).join(' - ');
useEffect(() => {
// Only track once for the same page
if (shouldTrack && trackPageView && !pageViewTracked) {
appInsightsReact.trackPageView?.({ name: [rawTitle, 'SIE'].join(' - ') });
setPageViewTracked(true);
}
}, [pageViewTracked, rawTitle, trackPageView]);
return <Helmet title={title} />;
};
export default PageMeta;

View file

@ -4,7 +4,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Logto</title>
<title></title>
<link rel="preload" href="/api/.well-known/sign-in-exp" as="fetch" crossorigin="anonymous">
<link rel="preload" href="/api/.well-known/phrases" as="fetch" crossorigin="anonymous">
</head>

View file

@ -8,6 +8,7 @@ import PageContext from '@/Providers/PageContextProvider/PageContext';
import EmptyStateDark from '@/assets/icons/empty-state-dark.svg';
import EmptyState from '@/assets/icons/empty-state.svg';
import NavBar from '@/components/NavBar';
import PageMeta from '@/components/PageMeta';
import * as styles from './index.module.scss';
@ -26,6 +27,7 @@ const ErrorPage = ({ title = 'description.not_found', message, rawMessage }: Pro
return (
<StaticPageLayout>
<PageMeta titleKey={title} />
{history.length > 1 && <NavBar />}
<div className={styles.container}>
{theme === Theme.Light ? <EmptyState /> : <EmptyStateDark />}

View file

@ -8,8 +8,6 @@ import { generateState, storeState } from '@/utils/social-connectors';
import SocialCallback from '.';
const origin = 'http://localhost:3000';
jest.mock('i18next', () => ({
...jest.requireActual('i18next'),
language: 'en',

View file

@ -23,7 +23,7 @@ describe('VerificationCode Page', () => {
{ initialEntries: ['/sign-in/verification-code'] }
);
expect(queryByText('action.enter_passcode')).not.toBeNull();
expect(queryByText('description.verify_email')).not.toBeNull();
expect(queryByText('description.enter_passcode')).not.toBeNull();
});

View file

@ -41,9 +41,10 @@ const VerificationCode = () => {
return (
<SecondaryPageLayout
title="action.enter_passcode"
title={`description.verify_${identifier}`}
description="description.enter_passcode"
descriptionProps={{
// TODO: @simeng consider align the phrase key to 'phone'
address: t(`description.${identifier === 'email' ? 'email' : 'phone_number'}`),
target: identifier === 'phone' ? formatPhoneNumberWithCountryCallingCode(value) : value,
}}

View file

@ -0,0 +1,8 @@
import { adminConsoleApplicationId, logtoCookieKey, logtoUiCookieGuard } from '@logto/schemas';
import { trySafe } from '@silverhand/essentials';
import { getCookie } from 'tiny-cookie';
export const logtoCookies =
trySafe(() => logtoUiCookieGuard.parse(getCookie(logtoCookieKey, JSON.parse))) ?? {};
export const shouldTrack = logtoCookies.appId === adminConsoleApplicationId;

View file

@ -1342,6 +1342,9 @@ importers:
packages/ui:
devDependencies:
'@logto/app-insights':
specifier: workspace:^
version: link:../app-insights
'@logto/connector-kit':
specifier: workspace:^
version: link:../toolkit/connector-kit
@ -1417,6 +1420,9 @@ importers:
'@types/react-dom':
specifier: ^18.0.0
version: 18.0.6
'@types/react-helmet':
specifier: ^6.1.6
version: 6.1.6
'@types/react-modal':
specifier: ^3.13.1
version: 3.13.1
@ -1460,8 +1466,8 @@ importers:
specifier: ^2.0.0
version: 2.0.0(jest@29.5.0)(react@18.2.0)
js-base64:
specifier: ^3.7.2
version: 3.7.2
specifier: ^3.7.5
version: 3.7.5
ky:
specifier: ^0.33.0
version: 0.33.0
@ -1495,6 +1501,9 @@ importers:
react-dom:
specifier: ^18.0.0
version: 18.2.0(react@18.2.0)
react-helmet:
specifier: ^6.1.0
version: 6.1.0(react@18.2.0)
react-hook-form:
specifier: ^7.34.0
version: 7.34.0(react@18.2.0)
@ -1522,6 +1531,9 @@ importers:
superstruct:
specifier: ^0.16.0
version: 0.16.0
tiny-cookie:
specifier: ^2.4.1
version: 2.4.1
ts-jest:
specifier: ^29.0.5
version: 29.0.5(@babel/core@7.20.2)(@jest/types@29.3.1)(jest@29.5.0)(typescript@5.0.2)
@ -2716,7 +2728,7 @@ packages:
'@types/node': 18.11.18
jest-message-util: 29.4.1
jest-mock: 29.4.1
jest-util: 29.4.1
jest-util: 29.5.0
dev: true
/@jest/fake-timers@29.5.0:
@ -3025,7 +3037,7 @@ packages:
dependencies:
'@logto/client': 1.1.0
'@silverhand/essentials': 1.3.0
js-base64: 3.7.4
js-base64: 3.7.5
dev: true
/@logto/client@1.1.0:
@ -9639,7 +9651,7 @@ packages:
'@jest/types': 29.5.0
'@types/node': 18.11.18
jest-mock: 29.4.1
jest-util: 29.4.1
jest-util: 29.5.0
dev: true
/jest-environment-node@29.5.0:
@ -9782,7 +9794,7 @@ packages:
dependencies:
'@jest/types': 29.5.0
'@types/node': 18.11.18
jest-util: 29.4.1
jest-util: 29.5.0
dev: true
/jest-mock@29.5.0:
@ -10080,14 +10092,14 @@ packages:
/jose@4.11.1:
resolution: {integrity: sha512-YRv4Tk/Wlug8qicwqFNFVEZSdbROCHRAC6qu/i0dyNKr5JQdoa2pIGoS04lLO/jXQX7Z9omoNewYIVIxqZBd9Q==}
/js-base64@3.7.2:
resolution: {integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==}
dev: true
/js-base64@3.7.4:
resolution: {integrity: sha512-wpM/wi20Tl+3ifTyi0RdDckS4YTD4Lf953mBRrpG8547T7hInHNPEj8+ck4gB8VDcGyeAWFK++Wb/fU1BeavKQ==}
dev: true
/js-base64@3.7.5:
resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==}
dev: true
/js-sdsl@4.3.0:
resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==}
dev: true
@ -11670,7 +11682,7 @@ packages:
dev: true
/object-assign@4.1.1:
resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=}
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
dev: true
@ -14628,6 +14640,10 @@ packages:
resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==}
dev: true
/tiny-cookie@2.4.1:
resolution: {integrity: sha512-h8ueaMyvUd/9ZfRqCfa1t+0tXqfVFhdK8WpLHz8VXMqsiaj3Sqg64AOCH/xevLQGZk0ZV+/75ouITdkvp3taVA==}
dev: true
/tiny-glob@0.2.9:
resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
dependencies: