mirror of
https://github.com/QingWei-Li/notea.git
synced 2024-11-22 18:20:37 +03:00
feat: Add rudimentary debugging utilities
/debug: Added a /debug page that's only accessible if a fatal error in configuration has occurred. settings: The settings page now includes a debugging section with similar content to the debug page. This still needs styling but is otherwise OK.
This commit is contained in:
parent
2431c96409
commit
615041a927
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,6 +23,7 @@ node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/logs
|
||||
|
||||
# local env files
|
||||
.env
|
||||
|
@ -26,7 +26,7 @@ COPY --from=builder /app/node_modules ./node_modules
|
||||
VOLUME /app/config
|
||||
# VOLUME /app/data
|
||||
|
||||
ENV CONFIG_FILE=/app/config/notea.yml
|
||||
ENV CONFIG_FILE=/app/config/notea.yml LOG_DIRECTORY=/app/logs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
74
components/debug/debug-info-copy-button.tsx
Normal file
74
components/debug/debug-info-copy-button.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { Button } from '@material-ui/core';
|
||||
import useI18n from 'libs/web/hooks/use-i18n';
|
||||
import { FC } from 'react';
|
||||
import { logLevelToString, DebugInformation } from 'libs/shared/debugging';
|
||||
|
||||
export const DebugInfoCopyButton: FC<{
|
||||
debugInfo: DebugInformation;
|
||||
}> = ({ debugInfo }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
function generateDebugInfo(): string {
|
||||
let data =
|
||||
'Notea debug information' +
|
||||
'\nTime ' +
|
||||
new Date(Date.now()).toISOString() +
|
||||
'\n\n';
|
||||
function ensureNewline() {
|
||||
if (!data.endsWith('\n') && data.length >= 1) {
|
||||
data += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (debugInfo.issues.length > 0) {
|
||||
ensureNewline();
|
||||
data += 'Configuration errors: ';
|
||||
let i = 1;
|
||||
const prefixLength = debugInfo.issues.length.toString().length;
|
||||
for (const issue of debugInfo.issues) {
|
||||
const prefix = i.toString().padStart(prefixLength, ' ') + ': ';
|
||||
const empty = ' '.repeat(prefixLength + 2);
|
||||
|
||||
data += prefix + issue.name;
|
||||
data += empty + '// ' + issue.cause;
|
||||
data += empty + issue.description;
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
data += 'No detected configuration errors.';
|
||||
}
|
||||
|
||||
if (debugInfo.logs.length > 0) {
|
||||
ensureNewline();
|
||||
for (const log of debugInfo.logs) {
|
||||
data += `[${new Date(log.time).toISOString()} ${log.name}] ${logLevelToString(log.level)}: ${log.msg}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function copyDebugInfo() {
|
||||
const text = generateDebugInfo();
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
// nothing
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(
|
||||
'Error when trying to copy debugging information to clipboard: %O',
|
||||
e
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="contained" onClick={copyDebugInfo}>
|
||||
{t('Copy debugging information')}
|
||||
</Button>
|
||||
);
|
||||
};
|
17
components/debug/issue-list.tsx
Normal file
17
components/debug/issue-list.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { FC } from 'react';
|
||||
import { Issue } from 'libs/shared/debugging';
|
||||
import { Issue as IssueDisplay } from './issue';
|
||||
|
||||
export const IssueList: FC<{
|
||||
issues: Array<Issue>;
|
||||
}> = ({ issues }) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{issues.map((v, i) => {
|
||||
return (
|
||||
<IssueDisplay key={i} issue={v} id={`issue-${i}`}/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
165
components/debug/issue.tsx
Normal file
165
components/debug/issue.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import { FC } from 'react';
|
||||
import {
|
||||
getNameFromRecommendation,
|
||||
getNameFromSeverity,
|
||||
Issue as IssueInfo,
|
||||
IssueFix,
|
||||
IssueSeverity
|
||||
} from 'libs/shared/debugging';
|
||||
import {
|
||||
Accordion as MuiAccordion,
|
||||
AccordionDetails as MuiAccordionDetails,
|
||||
AccordionSummary as MuiAccordionSummary,
|
||||
withStyles
|
||||
} from '@material-ui/core';
|
||||
import { ChevronDownIcon } from '@heroicons/react/outline';
|
||||
import useI18n from 'libs/web/hooks/use-i18n';
|
||||
|
||||
const Accordion = withStyles({
|
||||
root: {
|
||||
boxShadow: 'none',
|
||||
'&:not(:last-child)': {
|
||||
borderBottom: 0,
|
||||
},
|
||||
'&:before': {
|
||||
display: 'none',
|
||||
},
|
||||
'&$expanded': {
|
||||
margin: 'auto 0',
|
||||
},
|
||||
},
|
||||
expanded: {
|
||||
},
|
||||
})(MuiAccordion);
|
||||
|
||||
const AccordionSummary = withStyles({
|
||||
root: {
|
||||
backgroundColor: 'rgba(0, 0, 0, .03)',
|
||||
borderBottom: '1px solid rgba(0, 0, 0, .125)',
|
||||
borderTopRightRadius: 'inherit',
|
||||
borderBottomRightRadius: 'inherit',
|
||||
marginBottom: -1,
|
||||
minHeight: 56,
|
||||
'&$expanded': {
|
||||
minHeight: 56,
|
||||
borderBottomRightRadius: '0'
|
||||
},
|
||||
},
|
||||
content: {
|
||||
'&$expanded': {
|
||||
margin: '12px 0',
|
||||
}
|
||||
},
|
||||
expanded: {},
|
||||
})(MuiAccordionSummary);
|
||||
|
||||
const AccordionDetails = withStyles((theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}))(MuiAccordionDetails);
|
||||
|
||||
interface FixProps {
|
||||
id: string;
|
||||
fix: IssueFix;
|
||||
}
|
||||
const Fix: FC<FixProps> = ({ id, fix }) => {
|
||||
const i18n = useI18n();
|
||||
const { t } = i18n;
|
||||
const steps = fix.steps ?? [];
|
||||
return (
|
||||
<Accordion key={id} className={"bg-gray-300"}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ChevronDownIcon width=".8em"/>}
|
||||
aria-controls={`${id}-details`}
|
||||
id={`${id}-summary`}
|
||||
>
|
||||
<div className={"flex flex-col"}>
|
||||
{fix.recommendation === 0 && (
|
||||
<span className={"text-xs uppercase"}>{getNameFromRecommendation(fix.recommendation, i18n)}</span>
|
||||
)}
|
||||
<span className={"font-bold"}>{fix.description}</span>
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={"rounded-[inherit]"}>
|
||||
{steps.length > 0 ? (
|
||||
<ol className="list-decimal list-inside">
|
||||
{steps.map((step, i) => {
|
||||
const stepId = `${id}-step-${i}`;
|
||||
return (
|
||||
<li key={stepId}>{step}</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
) : (
|
||||
<span>{t('No steps were provided by Notea to perform this fix.')}</span>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
interface IssueProps {
|
||||
issue: IssueInfo;
|
||||
id: string;
|
||||
}
|
||||
export const Issue: FC<IssueProps> = function (props) {
|
||||
const { issue, id } = props;
|
||||
const i18n = useI18n();
|
||||
const { t } = i18n;
|
||||
|
||||
let borderColour: string;
|
||||
switch (issue.severity) {
|
||||
case IssueSeverity.SUGGESTION:
|
||||
borderColour = "border-gray-500";
|
||||
break;
|
||||
case IssueSeverity.WARNING:
|
||||
borderColour = "border-yellow-100";
|
||||
break;
|
||||
case IssueSeverity.ERROR:
|
||||
borderColour = "border-red-500";
|
||||
break;
|
||||
case IssueSeverity.FATAL_ERROR:
|
||||
borderColour = "border-red-900";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion className={`border-l-4 ${borderColour} bg-gray-200`}>
|
||||
<AccordionSummary
|
||||
className={"bg-gray-100"}
|
||||
expandIcon={<ChevronDownIcon width=".8em"/>}
|
||||
aria-controls={`${id}-details`}
|
||||
id={`${id}-summary`}
|
||||
>
|
||||
<div className={"flex flex-col bg-transparent"}>
|
||||
<span className={"text-xs uppercase"}>{getNameFromSeverity(issue.severity, i18n)}</span>
|
||||
<span className={"font-bold"}>{issue.name}</span>
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={"flex flex-col"}>
|
||||
<span>{issue.description ?? t('No description was provided for this issue.')}</span>
|
||||
|
||||
{issue.fixes.length > 0 ? (
|
||||
<div className={"mt-1 flex flex-col"}>
|
||||
<span className={"font-bold"}>{t('Potential fixes')}</span>
|
||||
<div>
|
||||
{issue.fixes.map((fix, i) => {
|
||||
const fixId = `${id}-fix-${i}`;
|
||||
return (
|
||||
<Fix
|
||||
key={fixId}
|
||||
id={fixId}
|
||||
fix={fix}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span>{t('No fixes are known by Notea for this issue.')}</span>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
27
components/debug/logs.tsx
Normal file
27
components/debug/logs.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { FC } from 'react';
|
||||
import { logLevelToString, LogLike } from 'libs/shared/debugging';
|
||||
|
||||
interface LogsProps {
|
||||
logs: Array<LogLike>
|
||||
}
|
||||
|
||||
export const Logs: FC<LogsProps> = (props) => {
|
||||
return (
|
||||
<div className={"flex flex-col space-y-1"}>
|
||||
{props.logs.length > 0 ? props.logs.map((log, i) => {
|
||||
return (
|
||||
<div className={"flex flex-col border-l pl-2"} key={i}>
|
||||
<span className={"text-sm uppercase"}>
|
||||
{logLevelToString(log.level)} at {new Date(log.time ?? 0).toLocaleString()} from <b>{log.name}</b>
|
||||
</span>
|
||||
<span className={"font-mono"}>
|
||||
{log.msg}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}) : (
|
||||
<span>No logs.</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
20
components/settings/debugging.tsx
Normal file
20
components/settings/debugging.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { IssueList } from 'components/debug/issue-list';
|
||||
import type { FC } from 'react';
|
||||
import { DebugInfoCopyButton } from 'components/debug/debug-info-copy-button';
|
||||
import { DebugInformation, Issue } from 'libs/shared/debugging';
|
||||
|
||||
export const Debugging: FC<{
|
||||
debugInfo: DebugInformation;
|
||||
}> = (props) => {
|
||||
const issues: Array<Issue> = [...props.debugInfo.issues].sort((a, b) => b.severity - a.severity);
|
||||
return (
|
||||
<div className="my-2">
|
||||
<IssueList
|
||||
issues={issues}
|
||||
/>
|
||||
<div className={'flex flex-row my-2'}>
|
||||
<DebugInfoCopyButton debugInfo={props.debugInfo} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -8,6 +8,8 @@ import { ImportOrExport } from './import-or-export';
|
||||
import { SnippetInjection } from './snippet-injection';
|
||||
import useI18n from 'libs/web/hooks/use-i18n';
|
||||
import { SettingsHeader } from './settings-header';
|
||||
import { Debugging } from 'components/settings/debugging';
|
||||
import { DebugInformation } from 'libs/shared/debugging';
|
||||
|
||||
export const defaultFieldConfig: TextFieldProps = {
|
||||
fullWidth: true,
|
||||
@ -26,7 +28,9 @@ const HR = () => {
|
||||
return <hr className="my-10 border-gray-200" />;
|
||||
};
|
||||
|
||||
export const SettingsContainer: FC = () => {
|
||||
export const SettingsContainer: FC<{
|
||||
debugInfo: DebugInformation
|
||||
}> = (props) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
@ -36,6 +40,7 @@ export const SettingsContainer: FC = () => {
|
||||
<Language></Language>
|
||||
<Theme></Theme>
|
||||
<EditorWidth></EditorWidth>
|
||||
|
||||
<HR />
|
||||
<SettingsHeader
|
||||
id="import-and-export"
|
||||
@ -44,11 +49,21 @@ export const SettingsContainer: FC = () => {
|
||||
'Import a zip file containing markdown files to this location, or export all pages from this location.'
|
||||
)}
|
||||
></SettingsHeader>
|
||||
|
||||
<ImportOrExport></ImportOrExport>
|
||||
|
||||
<HR />
|
||||
<SettingsHeader id="sharing" title={t('Sharing')}></SettingsHeader>
|
||||
<SnippetInjection></SnippetInjection>
|
||||
|
||||
<HR />
|
||||
<SettingsHeader
|
||||
id="debug"
|
||||
title={t('Debugging')}
|
||||
description={t(
|
||||
'Provides information about your Notea instance that can be helpful when trying to fix problems.'
|
||||
)}
|
||||
/>
|
||||
<Debugging debugInfo={props.debugInfo} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,10 @@
|
||||
import yaml from 'js-yaml';
|
||||
import * as env from 'libs/shared/env';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { isProbablyError } from 'libs/shared/util';
|
||||
import { createLogger, Issue, IssueCategory, IssueFixRecommendation, IssueSeverity } from 'libs/server/debugging';
|
||||
|
||||
const logger = createLogger("config");
|
||||
|
||||
export type BasicUser = { username: string; password: string };
|
||||
type BasicMultiUserConfiguration = {
|
||||
@ -45,23 +49,106 @@ export interface Configuration {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let loaded: Configuration | undefined = undefined;
|
||||
|
||||
export function loadConfig() {
|
||||
enum ErrTitle {
|
||||
CONFIG_FILE_READ_FAIL = 'Failed to load configuration file',
|
||||
INVALID_AUTH_CONFIG = 'Invalid authorisation configuration',
|
||||
CONFIG_FILE_PARSE_FAIL = 'Could not parse configuration file',
|
||||
}
|
||||
|
||||
export function loadConfigAndListErrors(): {
|
||||
config?: Configuration;
|
||||
errors: Array<Issue>;
|
||||
} {
|
||||
logger.debug("Loading configuration from scratch (loadConfigAndListErrors)");
|
||||
// TODO: More config errors
|
||||
const configFile = env.getEnvRaw('CONFIG_FILE', false) ?? './notea.yml';
|
||||
const errors: Array<Issue> = [];
|
||||
|
||||
let baseConfig: Configuration = {} as Configuration;
|
||||
if (existsSync(configFile)) {
|
||||
const data = readFileSync(configFile, 'utf-8');
|
||||
let data;
|
||||
try {
|
||||
data = readFileSync(configFile, 'utf-8');
|
||||
} catch (e) {
|
||||
let cause;
|
||||
if (isProbablyError(e)) {
|
||||
cause = e.message;
|
||||
}
|
||||
errors.push({
|
||||
name: ErrTitle.CONFIG_FILE_READ_FAIL,
|
||||
description: "The configuration file couldn't be read.",
|
||||
cause,
|
||||
severity: IssueSeverity.WARNING,
|
||||
category: IssueCategory.CONFIG,
|
||||
fixes: [
|
||||
{
|
||||
description: "Make sure Notea has read access to the configuration file",
|
||||
recommendation: IssueFixRecommendation.NEUTRAL,
|
||||
},
|
||||
{
|
||||
description: 'Make sure no other programme is using the configuration file',
|
||||
recommendation: IssueFixRecommendation.NEUTRAL
|
||||
}
|
||||
],
|
||||
});
|
||||
}
|
||||
if (data) {
|
||||
try {
|
||||
baseConfig = yaml.load(data) as Configuration;
|
||||
} catch (e) {
|
||||
let cause;
|
||||
if (isProbablyError(e)) {
|
||||
cause = e.message;
|
||||
}
|
||||
errors.push({
|
||||
name: ErrTitle.CONFIG_FILE_PARSE_FAIL,
|
||||
description:
|
||||
'The configuration file could not be parsed, probably due to a syntax error.',
|
||||
severity: IssueSeverity.WARNING,
|
||||
category: IssueCategory.CONFIG,
|
||||
cause,
|
||||
fixes: [
|
||||
{
|
||||
description: 'Check your configuration file for syntax errors.',
|
||||
recommendation: IssueFixRecommendation.RECOMMENDED
|
||||
}
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const disablePassword = env.parseBool(env.getEnvRaw('DISABLE_PASSWORD', false), false);
|
||||
const disablePassword = env.parseBool(
|
||||
env.getEnvRaw('DISABLE_PASSWORD', false),
|
||||
false
|
||||
);
|
||||
|
||||
let auth: AuthConfiguration;
|
||||
let auth: AuthConfiguration = { type: 'none' };
|
||||
if (!disablePassword) {
|
||||
const envPassword = env.getEnvRaw('PASSWORD', false);
|
||||
if (baseConfig.auth === undefined) {
|
||||
if (envPassword === undefined) {
|
||||
throw new Error('Authentication undefined');
|
||||
errors.push({
|
||||
name: ErrTitle.INVALID_AUTH_CONFIG,
|
||||
description:
|
||||
'Neither the configuration file, the PASSWORD environment variable, nor the DISABLE_PASSWORD environment variable was set.',
|
||||
severity: IssueSeverity.FATAL_ERROR,
|
||||
category: IssueCategory.CONFIG,
|
||||
fixes: [
|
||||
{
|
||||
description: 'Include an auth section the configuration file',
|
||||
recommendation: IssueFixRecommendation.RECOMMENDED,
|
||||
},
|
||||
{
|
||||
description: 'Set the password using the environment variable',
|
||||
recommendation: IssueFixRecommendation.NEUTRAL,
|
||||
},
|
||||
{
|
||||
description: 'Disable authentication',
|
||||
recommendation: IssueFixRecommendation.NOT_ADVISED
|
||||
}
|
||||
],
|
||||
});
|
||||
} else {
|
||||
auth = {
|
||||
type: 'basic',
|
||||
@ -71,14 +158,39 @@ export function loadConfig() {
|
||||
} else {
|
||||
auth = baseConfig.auth;
|
||||
if (envPassword !== undefined) {
|
||||
throw new Error(
|
||||
'Cannot specify PASSWORD when auth config section is present'
|
||||
);
|
||||
errors.push({
|
||||
name: ErrTitle.INVALID_AUTH_CONFIG,
|
||||
description:
|
||||
'The PASSWORD environment variable cannot be set when the file configuration contains an auth section.',
|
||||
category: IssueCategory.CONFIG,
|
||||
severity: IssueSeverity.FATAL_ERROR,
|
||||
fixes: [
|
||||
{
|
||||
description: "Don't set the PASSWORD environment variable prior to running Notea.",
|
||||
recommendation: IssueFixRecommendation.RECOMMENDED
|
||||
},
|
||||
{
|
||||
description: 'Remove the auth section from your file configuration.',
|
||||
recommendation: IssueFixRecommendation.NEUTRAL
|
||||
}
|
||||
],
|
||||
});
|
||||
}
|
||||
if (auth.type === 'basic') {
|
||||
if (auth.users) {
|
||||
// TEMPORARILY;
|
||||
throw new Error('Multiple users are not yet supported');
|
||||
errors.push({
|
||||
name: ErrTitle.INVALID_AUTH_CONFIG,
|
||||
description: 'Multiple users are not yet supported',
|
||||
severity: IssueSeverity.FATAL_ERROR,
|
||||
category: IssueCategory.CONFIG,
|
||||
fixes: [
|
||||
{
|
||||
description: "Change to a single-user configuration.",
|
||||
recommendation: IssueFixRecommendation.RECOMMENDED
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/*for (const user of auth.users) {
|
||||
user.username = user.username.toString();
|
||||
@ -132,7 +244,10 @@ export function loadConfig() {
|
||||
'STORE_PREFIX',
|
||||
false,
|
||||
) ?? store.prefix ?? '';
|
||||
store.proxyAttachments = env.parseBool(env.getEnvRaw('DIRECT_RESPONSE_ATTACHMENT', false), store.proxyAttachments ?? false);
|
||||
store.proxyAttachments = env.parseBool(
|
||||
env.getEnvRaw('DIRECT_RESPONSE_ATTACHMENT', false),
|
||||
store.proxyAttachments ?? false
|
||||
);
|
||||
}
|
||||
|
||||
let server: ServerConfiguration;
|
||||
@ -142,20 +257,50 @@ export function loadConfig() {
|
||||
server = baseConfig.server;
|
||||
}
|
||||
{
|
||||
server.useSecureCookies = env.parseBool(env.getEnvRaw('COOKIE_SECURE', false), server.useSecureCookies ?? process.env.NODE_ENV === 'production');
|
||||
server.useSecureCookies = env.parseBool(
|
||||
env.getEnvRaw('COOKIE_SECURE', false),
|
||||
server.useSecureCookies ?? process.env.NODE_ENV === 'production'
|
||||
);
|
||||
server.baseUrl = env.getEnvRaw('BASE_URL', false) ?? server.baseUrl;
|
||||
}
|
||||
|
||||
loaded = {
|
||||
return {
|
||||
config: {
|
||||
auth,
|
||||
store,
|
||||
server
|
||||
server,
|
||||
},
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
const MAX_ERRORS = 2;
|
||||
export function loadConfig() {
|
||||
const result = loadConfigAndListErrors();
|
||||
|
||||
if (!result.config) {
|
||||
const { errors } = result;
|
||||
let name = errors
|
||||
.slice(0, MAX_ERRORS)
|
||||
.map((v) => v.name)
|
||||
.join(', ');
|
||||
if (errors.length > MAX_ERRORS) {
|
||||
const rest = errors.length - MAX_ERRORS;
|
||||
name += ' and ' + rest + ' other error' + (rest > 1 ? 's' : '');
|
||||
}
|
||||
throw new Error(name);
|
||||
}
|
||||
|
||||
loaded = result.config;
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
export function config(): Configuration {
|
||||
if (!loaded) {
|
||||
logger.debug("Loading configuration");
|
||||
loadConfig();
|
||||
logger.debug("Successfully loaded configuration");
|
||||
}
|
||||
|
||||
return loaded as Configuration;
|
||||
|
@ -18,6 +18,7 @@ import { Settings } from 'libs/shared/settings';
|
||||
import { TreeModel } from 'libs/shared/tree';
|
||||
import { UserAgentType } from 'libs/shared/ua';
|
||||
import { useStore } from './middlewares/store';
|
||||
import { DebugInformation } from 'libs/server/debugging';
|
||||
|
||||
export interface ServerState {
|
||||
store: StoreProvider;
|
||||
@ -36,6 +37,7 @@ export interface ServerProps {
|
||||
tree?: TreeModel;
|
||||
ua?: UserAgentType;
|
||||
disablePassword: boolean;
|
||||
debugInformation?: DebugInformation;
|
||||
}
|
||||
|
||||
export type ApiRequest = NextApiRequest & {
|
||||
|
68
libs/server/debugging.ts
Normal file
68
libs/server/debugging.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { loadConfigAndListErrors } from 'libs/server/config';
|
||||
import pino from 'pino';
|
||||
import pinoPretty from 'pino-pretty';
|
||||
import Logger = pino.Logger;
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { DebugInformation, Issue } from 'libs/shared/debugging';
|
||||
|
||||
export * from 'libs/shared/debugging'; // here's a lil' lesson in trickery
|
||||
|
||||
const runtimeIssues: Array<Issue> = [];
|
||||
export function reportRuntimeIssue(issue: Issue) {
|
||||
runtimeIssues.push({
|
||||
...issue,
|
||||
isRuntime: true
|
||||
});
|
||||
}
|
||||
|
||||
export function findIssues(): Array<Issue> {
|
||||
const issues: Array<Issue> = [];
|
||||
|
||||
const cfg = loadConfigAndListErrors();
|
||||
issues.push(...cfg.errors);
|
||||
|
||||
issues.push(...runtimeIssues);
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
export function collectDebugInformation(): DebugInformation {
|
||||
const issues = findIssues();
|
||||
return {
|
||||
issues,
|
||||
logs: []
|
||||
};
|
||||
}
|
||||
|
||||
function getLogFile(name: string) {
|
||||
const dir = path.resolve(process.cwd(), process.env.LOG_DIRECTORY ?? 'logs');
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
|
||||
return path.resolve(dir, `${name}.log`);
|
||||
}
|
||||
|
||||
const loggerTransport: Parameters<typeof pino.multistream>[0] = [
|
||||
{
|
||||
stream: fs.createWriteStream(getLogFile('debug'), { flags: 'a' }),
|
||||
level: "debug"
|
||||
},
|
||||
{
|
||||
stream: pinoPretty(),
|
||||
level: "info"
|
||||
}
|
||||
];
|
||||
|
||||
const multistream = pino.multistream(loggerTransport);
|
||||
|
||||
export function createLogger(name: string): Logger {
|
||||
return pino({
|
||||
name,
|
||||
level: "trace",
|
||||
}, multistream);
|
||||
}
|
14
libs/server/middlewares/misconfiguration.ts
Normal file
14
libs/server/middlewares/misconfiguration.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { SSRMiddleware } from '../connect';
|
||||
import { collectDebugInformation } from 'libs/server/debugging';
|
||||
|
||||
export const applyMisconfiguration: SSRMiddleware = async (req, _res, next) => {
|
||||
// const IS_DEMO = getEnv<boolean>('IS_DEMO', false);
|
||||
|
||||
const collected = collectDebugInformation();
|
||||
req.props = {
|
||||
...req.props,
|
||||
debugInformation: collected
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
@ -1,19 +1,31 @@
|
||||
import { DEFAULT_SETTINGS } from 'libs/shared/settings';
|
||||
import { getSettings } from 'pages/api/settings';
|
||||
import { SSRMiddleware } from '../connect';
|
||||
import { Redirect } from 'next';
|
||||
|
||||
export const applySettings: SSRMiddleware = async (req, _res, next) => {
|
||||
const settings = await getSettings(req.state.store);
|
||||
let settings, redirect: Redirect = req.redirect;
|
||||
try {
|
||||
settings = await getSettings(req.state.store);
|
||||
} catch (e) {
|
||||
redirect = {
|
||||
permanent: false,
|
||||
destination: '/debug'
|
||||
};
|
||||
}
|
||||
let lngDict = {};
|
||||
|
||||
if (settings) {
|
||||
// import language dict
|
||||
if (settings.locale && settings.locale !== DEFAULT_SETTINGS.locale) {
|
||||
lngDict = (await import(`locales/${settings.locale}.json`)).default;
|
||||
}
|
||||
}
|
||||
|
||||
req.props = {
|
||||
...req.props,
|
||||
...{ settings, lngDict },
|
||||
...{ settings, lngDict }
|
||||
};
|
||||
req.redirect = redirect;
|
||||
next();
|
||||
};
|
||||
|
89
libs/shared/debugging.ts
Normal file
89
libs/shared/debugging.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { ContextProps as I18nContextProps } from 'libs/web/utils/i18n-provider';
|
||||
import pino from 'pino';
|
||||
|
||||
export enum IssueCategory {
|
||||
CONFIG = "config",
|
||||
MISC = "misc",
|
||||
STORE = "store",
|
||||
}
|
||||
export enum IssueSeverity {
|
||||
/**
|
||||
* Suggestions are issues that suggest things the user can do to improve
|
||||
* their experience with Notea.
|
||||
*/
|
||||
SUGGESTION = 0,
|
||||
/**
|
||||
* Warnings are issues that aren't severe enough to cause major issues by
|
||||
* themselves, but that *could* cause worse issues.
|
||||
*/
|
||||
WARNING = 1,
|
||||
/**
|
||||
* Errors are issues that are severe enough to cause major issues by themselves.
|
||||
* They don't necessarily cause the entire instance to stop working though.
|
||||
*/
|
||||
ERROR = 2,
|
||||
/**
|
||||
* Fatal errors are issues that must be resolved before Notea starts working.
|
||||
*/
|
||||
FATAL_ERROR = 3
|
||||
}
|
||||
export function getNameFromSeverity(severity: IssueSeverity, { t }: I18nContextProps) {
|
||||
switch (severity) {
|
||||
case IssueSeverity.SUGGESTION:
|
||||
return t('Suggestion');
|
||||
case IssueSeverity.WARNING:
|
||||
return t('Warning');
|
||||
case IssueSeverity.ERROR:
|
||||
return t('Error');
|
||||
case IssueSeverity.FATAL_ERROR:
|
||||
return t('Fatal error');
|
||||
}
|
||||
}
|
||||
|
||||
export enum IssueFixRecommendation {
|
||||
NEUTRAL,
|
||||
RECOMMENDED,
|
||||
NOT_ADVISED
|
||||
}
|
||||
export function getNameFromRecommendation(recommendation: IssueFixRecommendation, { t }: I18nContextProps) {
|
||||
switch (recommendation) {
|
||||
case IssueFixRecommendation.NEUTRAL:
|
||||
return t('Neutral');
|
||||
case IssueFixRecommendation.RECOMMENDED:
|
||||
return t('Recommended');
|
||||
case IssueFixRecommendation.NOT_ADVISED:
|
||||
return t('Not advised');
|
||||
}
|
||||
}
|
||||
|
||||
export interface IssueFix {
|
||||
recommendation: IssueFixRecommendation;
|
||||
description: string;
|
||||
steps?: Array<string>;
|
||||
}
|
||||
|
||||
export interface Issue {
|
||||
name: string;
|
||||
description?: string;
|
||||
category: IssueCategory;
|
||||
severity: IssueSeverity;
|
||||
fixes: Array<IssueFix>;
|
||||
cause?: string;
|
||||
isRuntime?: boolean;
|
||||
}
|
||||
|
||||
export interface LogLike {
|
||||
name: string;
|
||||
msg: string;
|
||||
pid: number;
|
||||
time: number;
|
||||
level: number;
|
||||
}
|
||||
export interface DebugInformation {
|
||||
issues: Array<Issue>;
|
||||
logs: Array<LogLike>; // TODO: Logging
|
||||
}
|
||||
|
||||
export function logLevelToString(level: number) {
|
||||
return pino.levels.labels[level];
|
||||
}
|
6
libs/shared/util.ts
Normal file
6
libs/shared/util.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export function isProbablyError(e: any): e is Error {
|
||||
if (!e) return false;
|
||||
if (e instanceof Error) return true;
|
||||
|
||||
return e && e.stack && e.message;
|
||||
}
|
@ -10,7 +10,7 @@ const i18n = rosetta<Record<string, string>>();
|
||||
export const defaultLanguage = DEFAULT_SETTINGS.locale;
|
||||
export const languages = values(Locale);
|
||||
|
||||
interface ContextProps {
|
||||
export interface ContextProps {
|
||||
activeLocale: Locale;
|
||||
t: Rosetta<Record<string, string>>['t'];
|
||||
locale: (l: Locale, dict: Record<string, string>) => void;
|
||||
|
@ -63,6 +63,8 @@
|
||||
"next-themes": "^0.0.14",
|
||||
"notistack": "^1.0.7",
|
||||
"outline-icons": "^1.27.0",
|
||||
"pino": "^8.7.0",
|
||||
"pino-pretty": "^9.1.1",
|
||||
"prosemirror-inputrules": "^1.1.3",
|
||||
"pupa": "^2.1.1",
|
||||
"qss": "^2.0.3",
|
||||
|
@ -6,15 +6,32 @@ import { formatSettings, Settings } from 'libs/shared/settings';
|
||||
import { isEqual } from 'lodash';
|
||||
import { tryJSON } from 'libs/shared/str';
|
||||
import { StoreProvider } from 'libs/server/store';
|
||||
import { IssueCategory, IssueFixRecommendation, IssueSeverity, reportRuntimeIssue } from 'libs/server/debugging';
|
||||
|
||||
export async function getSettings(store: StoreProvider): Promise<Settings> {
|
||||
const settingsPath = getPathSettings();
|
||||
let settings;
|
||||
try {
|
||||
if (await store.hasObject(settingsPath)) {
|
||||
settings = tryJSON<Settings>(
|
||||
await store.getObject(settingsPath)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
reportRuntimeIssue({
|
||||
category: IssueCategory.STORE,
|
||||
severity: IssueSeverity.FATAL_ERROR,
|
||||
name: "Could not get settings",
|
||||
cause: String(e),
|
||||
fixes: [
|
||||
{
|
||||
description: "Make sure Notea can connect to the store.",
|
||||
recommendation: IssueFixRecommendation.RECOMMENDED
|
||||
}
|
||||
]
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
const formatted = formatSettings(settings || {});
|
||||
|
||||
if (!settings || !isEqual(settings, formatted)) {
|
||||
|
91
pages/debug.tsx
Normal file
91
pages/debug.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { ServerProps, ssr, SSRContext } from 'libs/server/connect';
|
||||
import { applyMisconfiguration } from 'libs/server/middlewares/misconfiguration';
|
||||
import pkg from 'package.json';
|
||||
import { IssueList } from 'components/debug/issue-list';
|
||||
import { DebugInfoCopyButton } from 'components/debug/debug-info-copy-button';
|
||||
import { DebugInformation, IssueSeverity } from 'libs/shared/debugging';
|
||||
import { Logs } from 'components/debug/logs';
|
||||
|
||||
export function DebugPage({ debugInformation }: ServerProps) {
|
||||
if (!debugInformation) throw new Error('Missing debug information');
|
||||
|
||||
const issues = debugInformation.issues;
|
||||
const logs = debugInformation.logs ?? [];
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
<main className="flex flex-col space-y-4 mx-auto my-auto">
|
||||
<h1 className="text-4xl font-bold my-auto mx-auto">Backup debugging page</h1>
|
||||
<div className="flex flex-row my-auto mx-auto">
|
||||
<DebugInfoCopyButton debugInfo={debugInformation} />
|
||||
</div>
|
||||
<div className="flex flex-row space-x-5 my-auto">
|
||||
{issues.length > 0 && (
|
||||
<div className={"flex flex-col"}>
|
||||
<h2 className={"text-2xl"}>Issues</h2>
|
||||
<IssueList
|
||||
issues={debugInformation.issues}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{logs.length > 0 && (
|
||||
<div>
|
||||
<h2 className={"text-2xl"}>Logs</h2>
|
||||
<Logs logs={logs}/>
|
||||
</div>
|
||||
)}
|
||||
{issues.length < 1 && logs.length < 1 && (
|
||||
<div className={"mx-auto text-lg"}>
|
||||
No debug information available.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<footer className="flex flex-col my-auto mx-auto">
|
||||
<div className="mx-auto">
|
||||
<a
|
||||
href="https://github.com/notea-org/notea"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Notea v{pkg.version}
|
||||
</a>
|
||||
</div>
|
||||
<div className="space-x-1">
|
||||
<span>MIT ©</span>
|
||||
<a
|
||||
href="https://github.com/notea-org/notea"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Notea Contributors
|
||||
</a>
|
||||
<span>2022</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DebugPage;
|
||||
|
||||
export const getServerSideProps = async (ctx: SSRContext) => {
|
||||
await ssr().use(applyMisconfiguration).run(ctx.req, ctx.res);
|
||||
|
||||
// has to be cast to non-null
|
||||
const debugInformation = ctx.req.props.debugInformation as DebugInformation;
|
||||
|
||||
let redirect;
|
||||
if (!debugInformation.issues.some((v) => v.severity === IssueSeverity.FATAL_ERROR)) {
|
||||
redirect = {
|
||||
destination: '/',
|
||||
permanent: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
redirect,
|
||||
props: ctx.req.props,
|
||||
};
|
||||
};
|
@ -11,8 +11,10 @@ import { applyCsrf } from 'libs/server/middlewares/csrf';
|
||||
import { SettingFooter } from 'components/settings/setting-footer';
|
||||
import { SSRContext, ssr } from 'libs/server/connect';
|
||||
import { applyReset } from 'libs/server/middlewares/reset';
|
||||
import { applyMisconfiguration } from 'libs/server/middlewares/misconfiguration';
|
||||
import { DebugInformation } from 'libs/shared/debugging';
|
||||
|
||||
const SettingsPage: NextPage<{ tree: TreeModel }> = ({ tree }) => {
|
||||
const SettingsPage: NextPage<{ debugInformation: DebugInformation, tree: TreeModel }> = ({ tree, debugInformation }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
@ -23,7 +25,7 @@ const SettingsPage: NextPage<{ tree: TreeModel }> = ({ tree }) => {
|
||||
<span>{t('Settings')}</span>
|
||||
</h1>
|
||||
|
||||
<SettingsContainer />
|
||||
<SettingsContainer debugInfo={debugInformation} />
|
||||
<SettingFooter />
|
||||
</div>
|
||||
</section>
|
||||
@ -41,6 +43,7 @@ export const getServerSideProps = async (ctx: SSRContext) => {
|
||||
.use(applySettings)
|
||||
.use(applyCsrf)
|
||||
.use(applyUA)
|
||||
.use(applyMisconfiguration)
|
||||
.run(ctx.req, ctx.res);
|
||||
|
||||
return {
|
||||
|
@ -9,6 +9,8 @@ module.exports = {
|
||||
colors: {
|
||||
gray: colors.gray,
|
||||
blue: colors.blue,
|
||||
red: colors.red,
|
||||
yellow: colors.yellow,
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
},
|
||||
@ -26,6 +28,7 @@ module.exports = {
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Noto Sans'].concat(defaultConfig.theme.fontFamily['sans']),
|
||||
mono: defaultConfig.theme.fontFamily['mono']
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
|
145
yarn.lock
145
yarn.lock
@ -3225,6 +3225,11 @@ at-least-node@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz"
|
||||
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
||||
|
||||
atomic-sleep@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
|
||||
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
|
||||
|
||||
autoprefixer@^10.2.4:
|
||||
version "10.4.12"
|
||||
resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.12.tgz"
|
||||
@ -3817,7 +3822,7 @@ color@^4.0.1:
|
||||
color-convert "^2.0.1"
|
||||
color-string "^1.9.0"
|
||||
|
||||
colorette@^2.0.16:
|
||||
colorette@^2.0.16, colorette@^2.0.7:
|
||||
version "2.0.19"
|
||||
resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz"
|
||||
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
|
||||
@ -4117,6 +4122,11 @@ data-urls@^2.0.0:
|
||||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
dateformat@^4.6.3:
|
||||
version "4.6.3"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
|
||||
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
|
||||
|
||||
dayjs@^1.10.4:
|
||||
version "1.11.5"
|
||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz"
|
||||
@ -4846,6 +4856,11 @@ expect@^27.5.1:
|
||||
jest-matcher-utils "^27.5.1"
|
||||
jest-message-util "^27.5.1"
|
||||
|
||||
fast-copy@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.0.tgz#875ebf33b13948ae012b6e51d33da5e6e7571ab8"
|
||||
integrity sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA==
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
|
||||
@ -4872,6 +4887,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
|
||||
resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fast-redact@^3.1.1:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
|
||||
integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
|
||||
|
||||
fast-safe-stringify@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz"
|
||||
@ -5182,6 +5202,17 @@ glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, gl
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^8.0.0:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e"
|
||||
integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^5.0.1"
|
||||
once "^1.3.0"
|
||||
|
||||
globals@^11.1.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz"
|
||||
@ -5301,6 +5332,14 @@ he@^1.2.0:
|
||||
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
help-me@^4.0.1:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.1.0.tgz#c105e78ba490d6fcaa61a3d0cd06e0054554efab"
|
||||
integrity sha512-5HMrkOks2j8Fpu2j5nTLhrBhT7VwHwELpqnSnx802ckofys5MO2SkLpgSz3dgNFHV7IYFX2igm5CM75SmuYidw==
|
||||
dependencies:
|
||||
glob "^8.0.0"
|
||||
readable-stream "^3.6.0"
|
||||
|
||||
hex-color-regex@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz"
|
||||
@ -6283,6 +6322,11 @@ jest@^27.1.0:
|
||||
import-local "^3.0.2"
|
||||
jest-cli "^27.5.1"
|
||||
|
||||
joycon@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
||||
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
||||
@ -7191,6 +7235,11 @@ object.values@^1.1.5:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.19.1"
|
||||
|
||||
on-exit-leak-free@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"
|
||||
integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==
|
||||
|
||||
once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
||||
@ -7445,6 +7494,56 @@ pinkie@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"
|
||||
integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==
|
||||
|
||||
pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
|
||||
integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
|
||||
dependencies:
|
||||
readable-stream "^4.0.0"
|
||||
split2 "^4.0.0"
|
||||
|
||||
pino-pretty@^9.1.1:
|
||||
version "9.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-9.1.1.tgz#e7d64c1db98266ca428ab56567b844ba780cd0e1"
|
||||
integrity sha512-iJrnjgR4FWQIXZkUF48oNgoRI9BpyMhaEmihonHeCnZ6F50ZHAS4YGfGBT/ZVNsPmd+hzkIPGzjKdY08+/yAXw==
|
||||
dependencies:
|
||||
colorette "^2.0.7"
|
||||
dateformat "^4.6.3"
|
||||
fast-copy "^3.0.0"
|
||||
fast-safe-stringify "^2.1.1"
|
||||
help-me "^4.0.1"
|
||||
joycon "^3.1.1"
|
||||
minimist "^1.2.6"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^1.0.0"
|
||||
pump "^3.0.0"
|
||||
readable-stream "^4.0.0"
|
||||
secure-json-parse "^2.4.0"
|
||||
sonic-boom "^3.0.0"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
pino-std-serializers@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz#4c20928a1bafca122fdc2a7a4a171ca1c5f9c526"
|
||||
integrity sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==
|
||||
|
||||
pino@^8.7.0:
|
||||
version "8.7.0"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-8.7.0.tgz#58621608a3d8540ae643cdd9194cdd94130c78d9"
|
||||
integrity sha512-l9sA5uPxmZzwydhMWUcm1gI0YxNnYl8MfSr2h8cwLvOAzQLBLewzF247h/vqHe3/tt6fgtXeG9wdjjoetdI/vA==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
fast-redact "^3.1.1"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport v1.0.0
|
||||
pino-std-serializers "^6.0.0"
|
||||
process-warning "^2.0.0"
|
||||
quick-format-unescaped "^4.0.3"
|
||||
real-require "^0.2.0"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
sonic-boom "^3.1.0"
|
||||
thread-stream "^2.0.0"
|
||||
|
||||
pirates@^4.0.4, pirates@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz"
|
||||
@ -7574,6 +7673,11 @@ prismjs@~1.27.0:
|
||||
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz"
|
||||
integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==
|
||||
|
||||
process-warning@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.0.0.tgz#341dbeaac985b90a04ebcd844d50097c7737b2ee"
|
||||
integrity sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==
|
||||
|
||||
process@^0.11.10:
|
||||
version "0.11.10"
|
||||
resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
|
||||
@ -7809,6 +7913,11 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
quick-format-unescaped@^4.0.3:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
|
||||
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
|
||||
|
||||
quick-lru@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz"
|
||||
@ -7978,6 +8087,11 @@ readdirp@~3.6.0:
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
real-require@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
|
||||
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
|
||||
|
||||
reduce-css-calc@^2.1.8:
|
||||
version "2.1.8"
|
||||
resolved "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz"
|
||||
@ -8252,6 +8366,11 @@ safe-regex-test@^1.0.0:
|
||||
get-intrinsic "^1.1.3"
|
||||
is-regex "^1.1.4"
|
||||
|
||||
safe-stable-stringify@^2.3.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz#34694bd8a30575b7f94792aa51527551bd733d61"
|
||||
integrity sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
|
||||
@ -8302,6 +8421,11 @@ scroll-into-view-if-needed@^2.2.28:
|
||||
dependencies:
|
||||
compute-scroll-into-view "^1.0.17"
|
||||
|
||||
secure-json-parse@^2.4.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.5.0.tgz#f929829df2adc7ccfb53703569894d051493a6ac"
|
||||
integrity sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w==
|
||||
|
||||
semver-compare@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz"
|
||||
@ -8448,6 +8572,13 @@ smooth-scroll-into-view-if-needed@^1.1.29:
|
||||
dependencies:
|
||||
scroll-into-view-if-needed "^2.2.28"
|
||||
|
||||
sonic-boom@^3.0.0, sonic-boom@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.0.tgz#ce9f2de7557e68be2e52c8df6d9b052e7d348143"
|
||||
integrity sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
|
||||
source-list-map@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
|
||||
@ -8529,6 +8660,11 @@ split.js@^1.6.0:
|
||||
resolved "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz"
|
||||
integrity sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==
|
||||
|
||||
split2@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809"
|
||||
integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
|
||||
@ -8858,6 +8994,13 @@ text-table@^0.2.0:
|
||||
resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
|
||||
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
|
||||
|
||||
thread-stream@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8"
|
||||
integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ==
|
||||
dependencies:
|
||||
real-require "^0.2.0"
|
||||
|
||||
throat@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user