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

feat(app-insights): add React context provider and hook

and fix AppInsights init issue for frontend projects.
This commit is contained in:
Gao Sun 2023-04-20 18:15:05 +08:00
parent 4331deb6f2
commit 748878ce5b
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
35 changed files with 246 additions and 93 deletions

View file

@ -0,0 +1,7 @@
---
"@logto/app-insights": minor
"@logto/console": patch
"@logto/ui": patch
---
add React context and hook to app-insights, fix init issue for frontend projects

View file

@ -12,11 +12,15 @@
"./*": {
"import": "./lib/*.js",
"types": "./lib/*.d.ts"
},
"./react": {
"import": "./lib/react/index.js",
"types": "./lib/react/index.d.ts"
}
},
"//": "This field is for parcel. Remove after https://github.com/parcel-bundler/parcel/pull/8807 published.",
"alias": {
"./react": "./lib/react.js"
"./react": "./lib/react/index.js"
},
"publishConfig": {
"access": "public"
@ -32,7 +36,9 @@
},
"devDependencies": {
"@silverhand/eslint-config": "3.0.1",
"@silverhand/eslint-config-react": "3.0.1",
"@silverhand/ts-config": "3.0.0",
"@silverhand/ts-config-react": "3.0.0",
"@types/node": "^18.11.18",
"@types/react": "^18.0.31",
"eslint": "^8.34.0",
@ -47,7 +53,7 @@
"node": "^18.12.0"
},
"eslintConfig": {
"extends": "@silverhand"
"extends": "@silverhand/react"
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"dependencies": {

View file

@ -8,9 +8,10 @@ import { type ComponentType } from 'react';
export type SetupConfig = {
connectionString?: string;
clickPlugin?: IClickAnalyticsConfiguration;
cookieDomain?: string;
};
class AppInsightsReact {
export class AppInsightsReact {
protected reactPlugin?: ReactPlugin;
protected clickAnalyticsPlugin?: ClickAnalyticsPlugin;
protected withAITracking?: typeof withAITracking;
@ -48,7 +49,8 @@ class AppInsightsReact {
const { ApplicationInsights } = await import('@microsoft/applicationinsights-web');
// Conditionally load ClickAnalytics plugin
const clickAnalyticsConfig = conditional(typeof config === 'object' && config.clickPlugin);
const configObject = conditional(typeof config === 'object' && config) ?? {};
const { cookieDomain, clickPlugin } = configObject;
const initClickAnalyticsPlugin = async () => {
const { ClickAnalyticsPlugin } = await import(
'@microsoft/applicationinsights-clickanalytics-js'
@ -62,13 +64,12 @@ class AppInsightsReact {
this.reactPlugin = new ReactPlugin();
// Assign ClickAnalytics prop
this.clickAnalyticsPlugin = conditional(
clickAnalyticsConfig && (await initClickAnalyticsPlugin())
);
this.clickAnalyticsPlugin = conditional(clickPlugin && (await initClickAnalyticsPlugin()));
// Init ApplicationInsights instance
this.appInsights = new ApplicationInsights({
config: {
cookieDomain,
connectionString,
enableAutoRouteTracking: false,
extensions: conditionalArray<ITelemetryPlugin>(
@ -77,18 +78,25 @@ class AppInsightsReact {
),
extensionConfig: conditional(
this.clickAnalyticsPlugin && {
[this.clickAnalyticsPlugin.identifier]: clickAnalyticsConfig,
[this.clickAnalyticsPlugin.identifier]: clickPlugin,
}
),
},
});
this.appInsights.addTelemetryInitializer((item) => {
// The key 'ai.cloud.role' is extracted from Node SDK
// @see https://learn.microsoft.com/en-us/azure/azure-monitor/app/nodejs#multiple-roles-for-multi-component-applications
// @see https://github.com/microsoft/ApplicationInsights-JS#example-setting-cloud-role-name
// @see https://github.com/microsoft/ApplicationInsights-node.js/blob/a573e40fc66981c6a3106bdc5b783d1d94f64231/Schema/PublicSchema/ContextTagKeys.bond#L83
// eslint-disable-next-line @silverhand/fp/no-mutation
/* eslint-disable @silverhand/fp/no-mutation */
item.tags = [...(item.tags ?? []), { 'ai.cloud.role': cloudRole }];
// Extract UTM parameters
const searchParams = [...new URLSearchParams(window.location.search).entries()];
item.data = {
...item.data,
...Object.fromEntries(searchParams.filter(([key]) => key.startsWith('utm_'))),
};
/* eslint-enable @silverhand/fp/no-mutation */
});
this.appInsights.loadAppInsights();
@ -112,3 +120,5 @@ class AppInsightsReact {
}
export const appInsightsReact = new AppInsightsReact();
export const withAppInsights = appInsightsReact.withAppInsights.bind(appInsightsReact);

View file

@ -0,0 +1,30 @@
import { type ReactNode, createContext, useMemo, useState } from 'react';
type Context = {
initialized: boolean;
setInitialized: React.Dispatch<React.SetStateAction<boolean>>;
};
export const AppInsightsContext = createContext<Context>({
initialized: false,
setInitialized: () => {
throw new Error('Not implemented');
},
});
type Properties = {
children: ReactNode;
};
export const AppInsightsProvider = ({ children }: Properties) => {
const [initialized, setInitialized] = useState(false);
const context = useMemo<Context>(
() => ({
initialized,
setInitialized,
}),
[initialized]
);
return <AppInsightsContext.Provider value={context}>{children}</AppInsightsContext.Provider>;
};

View file

@ -0,0 +1,4 @@
export { AppInsightsReact, type SetupConfig, withAppInsights } from './AppInsightsReact.js';
export * from './context.js';
export * from './use-app-insights.js';
export * from './utils.js';

View file

@ -0,0 +1,28 @@
import { useCallback, useContext } from 'react';
import { type AppInsightsReact, appInsightsReact } from './AppInsightsReact.js';
import { AppInsightsContext } from './context.js';
export type UseAppInsights = {
initialized: boolean;
setup: typeof appInsightsReact.setup;
appInsights: AppInsightsReact;
};
export const useAppInsights = () => {
const { initialized, setInitialized } = useContext(AppInsightsContext);
const setup = useCallback(
async (...args: Parameters<typeof appInsightsReact.setup>) => {
const result = await appInsightsReact.setup(...args);
if (result) {
console.debug('Initialized ApplicationInsights');
setInitialized(true);
}
},
[setInitialized]
);
return { initialized, setup, appInsights: appInsightsReact };
};

View file

@ -0,0 +1 @@
export const getPrimaryDomain = () => window.location.hostname.split('.').slice(-2).join('.');

View file

@ -1,4 +1,7 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"noEmit": false
},
"include": ["src"]
}

View file

@ -1,5 +1,5 @@
{
"extends": "@silverhand/ts-config/tsconfig.base",
"extends": "@silverhand/ts-config-react/tsconfig.base",
"compilerOptions": {
"outDir": "lib",
"types": ["node"]

View file

@ -1,9 +1,9 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { AppInsightsProvider, getPrimaryDomain, useAppInsights } from '@logto/app-insights/react';
import { UserScope } from '@logto/core-kit';
import { LogtoProvider } from '@logto/react';
import { adminConsoleApplicationId, PredefinedScope } from '@logto/schemas';
import { conditionalArray, deduplicate } from '@silverhand/essentials';
import { useContext } from 'react';
import { useContext, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import 'overlayscrollbars/styles/overlayscrollbars.css';
@ -26,17 +26,15 @@ import AppEndpointsProvider from './contexts/AppEndpointsProvider';
import { AppThemeProvider } from './contexts/AppThemeProvider';
import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider';
// Use `.then()` for better compatibility, update to top-level await some day
// eslint-disable-next-line unicorn/prefer-top-level-await
void appInsightsReact.setup('console').then((success) => {
if (success) {
console.debug('Initialized ApplicationInsights');
}
});
void initI18n();
function Content() {
const { tenants, isSettle, currentTenantId } = useContext(TenantsContext);
const { setup } = useAppInsights();
useEffect(() => {
void setup('console', { cookieDomain: getPrimaryDomain() });
}, [setup]);
const resources = deduplicate(
conditionalArray(
@ -90,9 +88,11 @@ function Content() {
function App() {
return (
<TenantsProvider>
<Content />
</TenantsProvider>
<AppInsightsProvider>
<TenantsProvider>
<Content />
</TenantsProvider>
</AppInsightsProvider>
);
}

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { useAppInsights } from '@logto/app-insights/react';
import type { AdminConsoleKey } from '@logto/phrases';
import { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
@ -14,6 +14,7 @@ type Props = {
function PageMeta({ titleKey, trackPageView = true }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { initialized, appInsights } = useAppInsights();
const [pageViewTracked, setPageViewTracked] = useState(false);
const keys = typeof titleKey === 'string' ? [titleKey] : titleKey;
const rawTitle = keys.map((key) => t(key, { lng: 'en' })).join(' - ');
@ -21,11 +22,11 @@ function PageMeta({ titleKey, trackPageView = true }: Props) {
useEffect(() => {
// Only track once for the same page
if (trackPageView && !pageViewTracked) {
appInsightsReact.trackPageView?.({ name: [rawTitle, mainTitle].join(' - ') });
if (initialized && trackPageView && !pageViewTracked) {
appInsights.trackPageView?.({ name: [rawTitle, mainTitle].join(' - ') });
setPageViewTracked(true);
}
}, [pageViewTracked, rawTitle, trackPageView]);
}, [appInsights, initialized, pageViewTracked, rawTitle, trackPageView]);
return <Helmet title={title} />;
}

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { useAppInsights } from '@logto/app-insights/react';
import { useLogto } from '@logto/react';
import { trySafe } from '@silverhand/essentials';
import { useEffect } from 'react';
@ -10,17 +10,19 @@ class NoIdTokenClaimsError extends Error {
const useTrackUserId = () => {
const { isAuthenticated, getIdTokenClaims } = useLogto();
const {
initialized,
appInsights: { instance },
} = useAppInsights();
useEffect(() => {
const setUserId = async () => {
const { instance: appInsights } = appInsightsReact;
if (!appInsights) {
if (!instance) {
return;
}
if (!isAuthenticated) {
appInsights.clearAuthenticatedUserContext();
instance.clearAuthenticatedUserContext();
return;
}
@ -28,14 +30,16 @@ const useTrackUserId = () => {
const claims = await trySafe(getIdTokenClaims());
if (claims) {
appInsights.setAuthenticatedUserContext(claims.sub, claims.sub, true);
instance.setAuthenticatedUserContext(claims.sub, claims.sub, true);
} else {
appInsights.trackException({ exception: new NoIdTokenClaimsError() });
instance.trackException({ exception: new NoIdTokenClaimsError() });
}
};
void setUserId();
}, [getIdTokenClaims, isAuthenticated]);
if (initialized) {
void setUserId();
}
}, [getIdTokenClaims, initialized, instance, isAuthenticated]);
};
export default useTrackUserId;

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import { conditional } from '@silverhand/essentials';
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
@ -144,4 +144,4 @@ function About() {
);
}
export default appInsightsReact.withAppInsights(About);
export default withAppInsights(About);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import classNames from 'classnames';
import { useContext } from 'react';
import { Trans, useTranslation } from 'react-i18next';
@ -63,4 +63,4 @@ function Congrats() {
);
}
export default appInsightsReact.withAppInsights(Congrats);
export default withAppInsights(Congrats);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { SignInExperience as SignInExperienceType } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import { useCallback, useEffect, useMemo } from 'react';
@ -255,4 +255,4 @@ function SignInExperience() {
);
}
export default appInsightsReact.withAppInsights(SignInExperience);
export default withAppInsights(SignInExperience);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import classNames from 'classnames';
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
@ -111,4 +111,4 @@ function Welcome() {
);
}
export default appInsightsReact.withAppInsights(Welcome);
export default withAppInsights(Welcome);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { Resource } from '@logto/schemas';
import { isManagementApi, Theme } from '@logto/schemas';
import classNames from 'classnames';
@ -148,4 +148,4 @@ function ApiResourceDetails() {
);
}
export default appInsightsReact.withAppInsights(ApiResourceDetails);
export default withAppInsights(ApiResourceDetails);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { Resource } from '@logto/schemas';
import { Theme } from '@logto/schemas';
import { toast } from 'react-hot-toast';
@ -151,4 +151,4 @@ function ApiResources() {
);
}
export default appInsightsReact.withAppInsights(ApiResources);
export default withAppInsights(ApiResources);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { Application, ApplicationResponse, SnakeCaseOidcConfig } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas';
import { useEffect, useState } from 'react';
@ -235,4 +235,4 @@ function ApplicationDetails() {
);
}
export default appInsightsReact.withAppInsights(ApplicationDetails);
export default withAppInsights(ApplicationDetails);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { Application } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
@ -147,4 +147,4 @@ function Applications() {
);
}
export default appInsightsReact.withAppInsights(Applications);
export default withAppInsights(Applications);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { User, Log } from '@logto/schemas';
import { demoAppApplicationId } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
@ -122,4 +122,4 @@ function AuditLogDetails() {
);
}
export default appInsightsReact.withAppInsights(AuditLogDetails);
export default withAppInsights(AuditLogDetails);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import AuditLogTable from '@/components/AuditLogTable';
import CardTitle from '@/components/CardTitle';
@ -17,4 +17,4 @@ function AuditLogs() {
);
}
export default appInsightsReact.withAppInsights(AuditLogs);
export default withAppInsights(AuditLogs);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import { ConnectorType } from '@logto/schemas';
import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas';
import { useEffect, useState } from 'react';
@ -235,4 +235,4 @@ function ConnectorDetails() {
);
}
export default appInsightsReact.withAppInsights(ConnectorDetails);
export default withAppInsights(ConnectorDetails);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { ConnectorFactoryResponse } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
@ -225,4 +225,4 @@ function Connectors() {
);
}
export default appInsightsReact.withAppInsights(Connectors);
export default withAppInsights(Connectors);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import { format } from 'date-fns';
import type { ChangeEventHandler } from 'react';
import { useState } from 'react';
@ -153,4 +153,4 @@ function Dashboard() {
);
}
export default appInsightsReact.withAppInsights(Dashboard);
export default withAppInsights(Dashboard);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@ -99,4 +99,4 @@ function GetStarted() {
);
}
export default appInsightsReact.withAppInsights(GetStarted);
export default withAppInsights(GetStarted);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { ConnectorResponse } from '@logto/schemas';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -99,4 +99,4 @@ function Profile() {
);
}
export default appInsightsReact.withAppInsights(Profile);
export default withAppInsights(Profile);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { Role } from '@logto/schemas';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
@ -135,4 +135,4 @@ function RoleDetails() {
);
}
export default appInsightsReact.withAppInsights(RoleDetails);
export default withAppInsights(RoleDetails);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { RoleResponse } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next';
@ -163,4 +163,4 @@ function Roles() {
);
}
export default appInsightsReact.withAppInsights(Roles);
export default withAppInsights(Roles);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { SignInExperience as SignInExperienceType } from '@logto/schemas';
import classNames from 'classnames';
import type { ReactNode } from 'react';
@ -252,4 +252,4 @@ function SignInExperience() {
);
}
export default appInsightsReact.withAppInsights(SignInExperience);
export default withAppInsights(SignInExperience);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { User } from '@logto/schemas';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
@ -250,4 +250,4 @@ function UserDetails() {
);
}
export default appInsightsReact.withAppInsights(UserDetails);
export default withAppInsights(UserDetails);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { withAppInsights } from '@logto/app-insights/react';
import type { User } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next';
@ -179,4 +179,4 @@ function Users() {
);
}
export default appInsightsReact.withAppInsights(Users);
export default withAppInsights(Users);

View file

@ -1,4 +1,5 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { AppInsightsProvider, getPrimaryDomain, useAppInsights } from '@logto/app-insights/react';
import { useEffect } from 'react';
import { Route, Routes, BrowserRouter } from 'react-router-dom';
import AppLayout from './Layout/AppLayout';
@ -26,19 +27,17 @@ import { handleSearchParametersData } from './utils/search-parameters';
import './scss/normalized.scss';
if (shouldTrack) {
// Use `.then()` for better compatibility, update to top-level await some day
// eslint-disable-next-line unicorn/prefer-top-level-await, promise/prefer-await-to-then
void appInsightsReact.setup('ui').then((success) => {
if (success) {
console.debug('Initialized ApplicationInsights');
}
});
}
handleSearchParametersData();
const App = () => {
const Content = () => {
const { setup } = useAppInsights();
useEffect(() => {
if (shouldTrack) {
void setup('ui', { cookieDomain: getPrimaryDomain() });
}
}, [setup]);
return (
<BrowserRouter>
<PageContextProvider>
@ -99,4 +98,12 @@ const App = () => {
);
};
const App = () => {
return (
<AppInsightsProvider>
<Content />
</AppInsightsProvider>
);
};
export default App;

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react';
import { useAppInsights } from '@logto/app-insights/react';
import { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { type TFuncKey, useTranslation } from 'react-i18next';
@ -13,6 +13,7 @@ type Props = {
const PageMeta = ({ titleKey, trackPageView = true }: Props) => {
const { t } = useTranslation();
const { initialized, appInsights } = useAppInsights();
const [pageViewTracked, setPageViewTracked] = useState(false);
const keys = typeof titleKey === 'string' ? [titleKey] : titleKey;
const rawTitle = keys.map((key) => t(key, { lng: 'en' })).join(' - ');
@ -20,11 +21,11 @@ const PageMeta = ({ titleKey, trackPageView = true }: Props) => {
useEffect(() => {
// Only track once for the same page
if (shouldTrack && trackPageView && !pageViewTracked) {
appInsightsReact.trackPageView?.({ name: [rawTitle, 'SIE'].join(' - ') });
if (shouldTrack && initialized && trackPageView && !pageViewTracked) {
appInsights.trackPageView?.({ name: [rawTitle, 'SIE'].join(' - ') });
setPageViewTracked(true);
}
}, [pageViewTracked, rawTitle, trackPageView]);
}, [appInsights, initialized, pageViewTracked, rawTitle, trackPageView]);
return <Helmet title={title} />;
};

View file

@ -54,9 +54,15 @@ importers:
'@silverhand/eslint-config':
specifier: 3.0.1
version: 3.0.1(eslint@8.34.0)(prettier@2.8.4)(typescript@5.0.2)
'@silverhand/eslint-config-react':
specifier: 3.0.1
version: 3.0.1(eslint@8.34.0)(postcss@8.4.21)(prettier@2.8.4)(stylelint@15.0.0)(typescript@5.0.2)
'@silverhand/ts-config':
specifier: 3.0.0
version: 3.0.0(typescript@5.0.2)
'@silverhand/ts-config-react':
specifier: 3.0.0
version: 3.0.0(typescript@5.0.2)
'@types/node':
specifier: ^18.11.18
version: 18.11.18
@ -8527,6 +8533,28 @@ packages:
- typescript
dev: true
/@silverhand/eslint-config-react@3.0.1(eslint@8.34.0)(postcss@8.4.21)(prettier@2.8.4)(stylelint@15.0.0)(typescript@5.0.2):
resolution: {integrity: sha512-8roPq3t5qgi4pxYh3It2BhS91LVsp740vhNXVn56RTID0ZdW27LMrV2yp5uTT6kVzLdFYis4nIPlOVFCnv+VKA==}
engines: {node: ^18.12.0}
peerDependencies:
stylelint: ^15.0.0
dependencies:
'@silverhand/eslint-config': 3.0.1(eslint@8.34.0)(prettier@2.8.4)(typescript@5.0.2)
eslint-config-xo-react: 0.27.0(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.31.10)(eslint@8.34.0)
eslint-plugin-jsx-a11y: 6.6.1(eslint@8.34.0)
eslint-plugin-react: 7.31.10(eslint@8.34.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.34.0)
stylelint: 15.0.0
stylelint-config-xo-scss: 0.15.0(postcss@8.4.21)(stylelint@15.0.0)
transitivePeerDependencies:
- eslint
- eslint-import-resolver-webpack
- postcss
- prettier
- supports-color
- typescript
dev: true
/@silverhand/eslint-config-react@3.0.1(eslint@8.34.0)(postcss@8.4.6)(prettier@2.8.4)(stylelint@15.0.0)(typescript@5.0.2):
resolution: {integrity: sha512-8roPq3t5qgi4pxYh3It2BhS91LVsp740vhNXVn56RTID0ZdW27LMrV2yp5uTT6kVzLdFYis4nIPlOVFCnv+VKA==}
engines: {node: ^18.12.0}
@ -16914,6 +16942,15 @@ packages:
postcss: 8.4.14
dev: true
/postcss-scss@4.0.5(postcss@8.4.21):
resolution: {integrity: sha512-F7xpB6TrXyqUh3GKdyB4Gkp3QL3DDW1+uI+gxx/oJnUt/qXI4trj5OGlp9rOKdoABGULuqtqeG+3HEVQk4DjmA==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.3.3
dependencies:
postcss: 8.4.21
dev: true
/postcss-scss@4.0.5(postcss@8.4.6):
resolution: {integrity: sha512-F7xpB6TrXyqUh3GKdyB4Gkp3QL3DDW1+uI+gxx/oJnUt/qXI4trj5OGlp9rOKdoABGULuqtqeG+3HEVQk4DjmA==}
engines: {node: '>=12.0'}
@ -18821,6 +18858,20 @@ packages:
- postcss
dev: true
/stylelint-config-xo-scss@0.15.0(postcss@8.4.21)(stylelint@15.0.0):
resolution: {integrity: sha512-X9WD8cDofWFWy3uaKdwwm+DjEvgI/+h7AtlaPagkhNAeOWH/GFQoeciBvNvyJ8tB1p00SoIzCn2IIOIKXCbxYA==}
engines: {node: '>=12'}
peerDependencies:
stylelint: '>=14.5.1 || ^15.0.0'
dependencies:
postcss-scss: 4.0.5(postcss@8.4.21)
stylelint: 15.0.0
stylelint-config-xo: 0.21.1(stylelint@15.0.0)
stylelint-scss: 4.3.0(stylelint@15.0.0)
transitivePeerDependencies:
- postcss
dev: true
/stylelint-config-xo-scss@0.15.0(postcss@8.4.6)(stylelint@15.0.0):
resolution: {integrity: sha512-X9WD8cDofWFWy3uaKdwwm+DjEvgI/+h7AtlaPagkhNAeOWH/GFQoeciBvNvyJ8tB1p00SoIzCn2IIOIKXCbxYA==}
engines: {node: '>=12'}