Skip to content

Commit

Permalink
feat: add new addLog method to useLogs to append log messages in the …
Browse files Browse the repository at this point in the history
…new format
  • Loading branch information
1egoman committed Oct 21, 2024
1 parent 1516671 commit a5c981d
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 20 deletions.
66 changes: 50 additions & 16 deletions packages/web/src/components/apps/use-logs.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';

import { AppChannel } from '@/clients/websocket';
import { PreviewStatusPayloadType } from '@srcbook/shared';
import { DepsInstallLogPayloadType, PreviewLogPayloadType } from '@srcbook/shared';

export type LogMessage = {
type: 'npm_install_error' | 'vite_error'; // TODO: add more types like "warning" or "problem"
type: 'stderr' | 'stdout' | 'info';
source: 'vite' | 'npm install';
timestamp: Date;
contents: string;
message: string;
};

export interface LogsContextValue {
logs: Array<LogMessage>;
clearLogs: () => void;
addError: (message: Omit<LogMessage, 'timestamp'>) => void;
unreadLogsCount: number;
panelIcon: 'default' | 'error';

addLog: (type: LogMessage['type'], source: LogMessage['source'], message: string) => void;

open: boolean;
togglePane: () => void;
closePane: () => void;
}

const LogsContext = createContext<LogsContextValue | undefined>(undefined);
Expand All @@ -29,42 +33,72 @@ type ProviderPropsType = {
export function LogsProvider({ channel, children }: ProviderPropsType) {
const [logs, setLogs] = useState<Array<LogMessage>>([]);
const [unreadLogsCount, setUnreadLogsCount] = useState(0);
const [panelIcon, setPanelIcon] = useState<LogsContextValue['panelIcon']>('default');

const [open, setOpen] = useState(false);

function clearLogs() {
setLogs([]);
setPanelIcon('default');
setUnreadLogsCount(0);
}

const addError = useCallback((error: Omit<LogMessage, 'timestamp'>) => {
setLogs((logs) => [{ ...error, timestamp: new Date() }, ...logs]);
const addLog = useCallback((type: LogMessage['type'], source: LogMessage['source'], message: LogMessage['message']) => {
setLogs((logs) => [ ...logs, { type, message, source, timestamp: new Date() } ]);
if (type === 'stderr') {
setPanelIcon('error');
}
setUnreadLogsCount((n) => n + 1);
}, []);

function togglePane() {
setOpen((n) => !n);
setPanelIcon('default');
setUnreadLogsCount(0);
}

// If vite crashes, then create an error log
function closePane() {
setOpen(false);
setPanelIcon('default');
setUnreadLogsCount(0);
}

// As the server generates logs, show them in the logs panel
useEffect(() => {
function onViteError(payload: PreviewStatusPayloadType) {
if (payload.status !== 'stopped' || payload.stoppedSuccessfully) {
return;
function onPreviewLog(payload: PreviewLogPayloadType) {
for (const row of payload.log.data.split('\n')) {
addLog(payload.log.type, 'vite', row);
}
addError({ type: 'vite_error', contents: payload.logs ?? '' });
}

channel.on('preview:status', onViteError);
channel.on('preview:log', onPreviewLog);

return () => channel.off('preview:status', onViteError);
}, [channel, addError]);
function onDepsInstallLog(payload: DepsInstallLogPayloadType) {
for (const row of payload.log.data.split('\n')) {
addLog(payload.log.type, 'npm install', row);
}
}
channel.on('deps:install:log', onDepsInstallLog);

// TODO: if npm install fails, add an error log
return () => {
channel.off('preview:log', onPreviewLog);
channel.off('deps:install:log', onDepsInstallLog);
};
}, [channel, addLog]);

return (
<LogsContext.Provider value={{ logs, clearLogs, addError, unreadLogsCount, open, togglePane }}>
<LogsContext.Provider value={{
logs,
clearLogs,
unreadLogsCount,
panelIcon,

addLog,

open,
togglePane,
closePane,
}}>
{children}
</LogsContext.Provider>
);
Expand Down
11 changes: 7 additions & 4 deletions packages/web/src/components/apps/use-package-json.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function PackageJsonProvider({ channel, children }: ProviderPropsType) {
const [output, setOutput] = useState<Array<OutputType>>([]);
const [nodeModulesExists, setNodeModulesExists] = useState<boolean | null>(null);

const { addError } = useLogs();
const { addLog } = useLogs();

useEffect(() => {
channel.push('deps:status', {});
Expand All @@ -52,6 +52,8 @@ export function PackageJsonProvider({ channel, children }: ProviderPropsType) {

const npmInstall = useCallback(
async (packages?: Array<string>) => {
addLog('info', 'npm install', `Running ${!packages ? 'npm install' : `npm install ${packages.join(' ')}`}`);

// NOTE: caching of the log output is required here because socket events that call callback
// functions in here hold on to old scope values
let contents = '';
Expand All @@ -63,15 +65,16 @@ export function PackageJsonProvider({ channel, children }: ProviderPropsType) {
};
channel.on('deps:install:log', logCallback);

const statusCallback = ({ status }: DepsInstallStatusPayloadType) => {
const statusCallback = ({ status, code }: DepsInstallStatusPayloadType) => {
channel.off('deps:install:log', logCallback);
channel.off('deps:install:status', statusCallback);
setStatus(status);

addLog('info', 'npm install', `${!packages ? 'npm install' : `npm install ${packages.join(' ')}`} exited with status code ${code}`);

if (status === 'complete') {
resolve();
} else {
addError({ type: 'npm_install_error', contents });
reject(new Error(`Error running npm install: ${contents}`));
}
};
Expand All @@ -82,7 +85,7 @@ export function PackageJsonProvider({ channel, children }: ProviderPropsType) {
channel.push('deps:install', { packages });
});
},
[channel, addError],
[channel],
);

const clearNodeModules = useCallback(() => {
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/components/apps/use-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AppChannel } from '@/clients/websocket';
import { PreviewStatusPayloadType } from '@srcbook/shared';
import useEffectOnce from '@/components/use-effect-once';
import { usePackageJson } from './use-package-json';
import { useLogs } from './use-logs';

export type PreviewStatusType = 'booting' | 'connecting' | 'running' | 'stopped';

Expand All @@ -28,13 +29,16 @@ export function PreviewProvider({ channel, children }: ProviderPropsType) {
const [lastStoppedError, setLastStoppedError] = useState<string | null>(null);

const { npmInstall, nodeModulesExists } = usePackageJson();
const { addLog } = useLogs();

useEffect(() => {
function onStatusUpdate(payload: PreviewStatusPayloadType) {
setUrl(payload.url);
setStatus(payload.status);

if (payload.status === 'stopped' && !payload.stoppedSuccessfully) {
// FIXME: add log here
// addLog('info', 'npm install', `Running vite ${payload.XXX}`);
setLastStoppedError(payload.logs ?? '');
} else {
setLastStoppedError(null);
Expand All @@ -50,6 +54,7 @@ export function PreviewProvider({ channel, children }: ProviderPropsType) {
if (nodeModulesExists === false) {
await npmInstall();
}
addLog('info', 'npm install', `Running vite`);
channel.push('preview:start', {});
}

Expand Down

0 comments on commit a5c981d

Please sign in to comment.