diff --git a/packages/gatsby-cli/src/reporter/__tests__/index.js b/packages/gatsby-cli/src/reporter/__tests__/index.js index b0423ebd15381..51328b4966e4a 100644 --- a/packages/gatsby-cli/src/reporter/__tests__/index.js +++ b/packages/gatsby-cli/src/reporter/__tests__/index.js @@ -1,5 +1,5 @@ const reporter = require(`../`) -const reporterActions = require(`../redux/actions`) +import * as reporterActions from "../redux/actions" // TODO: report.error now DOES return something. Get rid of this spying mocking stuff diff --git a/packages/gatsby-cli/src/reporter/catch-exit-signals.ts b/packages/gatsby-cli/src/reporter/catch-exit-signals.ts index 67c54680f50e1..8167a696ba16c 100644 --- a/packages/gatsby-cli/src/reporter/catch-exit-signals.ts +++ b/packages/gatsby-cli/src/reporter/catch-exit-signals.ts @@ -4,7 +4,7 @@ */ import signalExit from "signal-exit" import { getStore } from "./redux" -import reporterActions from "./redux/actions" +import { createPendingActivity } from "./redux/actions" import { ActivityStatuses } from "./constants" import { reporter } from "./reporter" @@ -25,7 +25,7 @@ export const prematureEnd = (): void => { // hack so at least one activity is surely failed, so // we are guaranteed to generate FAILED status // if none of activity did explicitly fail - reporterActions.createPendingActivity({ + createPendingActivity({ id: `panic`, status: ActivityStatuses.Failed, }) diff --git a/packages/gatsby-cli/src/reporter/redux/actions.js b/packages/gatsby-cli/src/reporter/redux/actions.js deleted file mode 100644 index 6b9c442babfdc..0000000000000 --- a/packages/gatsby-cli/src/reporter/redux/actions.js +++ /dev/null @@ -1,332 +0,0 @@ -// @flow -const uuidv4 = require(`uuid/v4`) -const convertHrtime = require(`convert-hrtime`) -const { trackCli } = require(`gatsby-telemetry`) -const { bindActionCreators } = require(`redux`) -const { dispatch, getStore } = require(`./index`) -const { - Actions, - ActivityLogLevels, - ActivityStatuses, - ActivityTypes, -} = require(`../constants`) -const signalExit = require(`signal-exit`) - -const getActivity = id => getStore().getState().logs.activities[id] - -/** - * @returns {Number} Milliseconds from activity start - */ -const getElapsedTimeMS = activity => { - const elapsed = process.hrtime(activity.startTime) - return convertHrtime(elapsed).milliseconds -} - -const ActivityStatusToLogLevel = { - [ActivityStatuses.Interrupted]: ActivityLogLevels.Interrupted, - [ActivityStatuses.Failed]: ActivityLogLevels.Failed, - [ActivityStatuses.Success]: ActivityLogLevels.Success, -} - -const getGlobalStatus = (id, status) => { - const { logs } = getStore().getState() - - const currentActivities = [id, ...Object.keys(logs.activities)] - - return currentActivities.reduce((generatedStatus, activityId) => { - const activityStatus = - activityId === id ? status : logs.activities[activityId].status - - if ( - activityStatus === ActivityStatuses.InProgress || - activityStatus === ActivityStatuses.NotStarted - ) { - return ActivityStatuses.InProgress - } else if ( - activityStatus === ActivityStatuses.Failed && - generatedStatus !== ActivityStatuses.InProgress - ) { - return ActivityStatuses.Failed - } else if ( - activityStatus === ActivityStatuses.Interrupted && - generatedStatus !== ActivityStatuses.InProgress - ) { - return ActivityStatuses.Interrupted - } - return generatedStatus - }, ActivityStatuses.Success) -} - -let cancelDelayedSetStatus = null -let weShouldExit = false -signalExit(() => { - weShouldExit = true -}) -/** - * Like setTimeout, but also handle signalExit - */ -const delayedCall = (fn, timeout) => { - const fnWrap = () => { - fn() - clear() - } - - const timeoutID = setTimeout(fnWrap, timeout) - const cancelSignalExit = signalExit(fnWrap) - - const clear = () => { - clearTimeout(timeoutID) - cancelSignalExit() - } - - return clear -} - -const actions = { - createLog: ({ - level, - text, - statusText, - duration, - group, - code, - type, - filePath, - location, - docsUrl, - context, - activity_current, - activity_total, - activity_type, - activity_uuid, - stack, - }) => { - return { - type: Actions.Log, - payload: { - level, - text, - statusText, - duration, - group, - code, - type, - filePath, - location, - docsUrl, - context, - activity_current, - activity_total, - activity_type, - activity_uuid, - timestamp: new Date().toJSON(), - stack, - }, - } - }, - createPendingActivity: ({ id, status = ActivityStatuses.NotStarted }) => { - const actionsToEmit = [] - - const logsState = getStore().getState().logs - - const globalStatus = getGlobalStatus(id, status) - - if (globalStatus !== logsState.status) { - actionsToEmit.push(actions.setStatus(globalStatus)) - } - - actionsToEmit.push({ - type: Actions.PendingActivity, - payload: { - id, - type: ActivityTypes.Pending, - status, - }, - }) - - return actionsToEmit - }, - setStatus: (status, force = false) => dispatch => { - const currentStatus = getStore().getState().logs.status - - if (cancelDelayedSetStatus) { - cancelDelayedSetStatus() - cancelDelayedSetStatus = null - } - - if (status !== currentStatus) { - if (status === `IN_PROGRESS` || force || weShouldExit) { - dispatch({ - type: Actions.SetStatus, - payload: status, - }) - } else { - cancelDelayedSetStatus = delayedCall(() => { - actions.setStatus(status, true)(dispatch) - }, 1000) - } - } - }, - startActivity: ({ - id, - text, - type, - status = ActivityStatuses.InProgress, - current, - total, - }) => { - const actionsToEmit = [] - - const logsState = getStore().getState().logs - - const globalStatus = getGlobalStatus(id, status) - - if (globalStatus !== logsState.status) { - actionsToEmit.push(actions.setStatus(globalStatus)) - } - - actionsToEmit.push({ - type: Actions.StartActivity, - payload: { - id, - uuid: uuidv4(), - text, - type, - status, - startTime: process.hrtime(), - - statusText: ``, - - current, - total, - }, - }) - - return actionsToEmit - }, - endActivity: ({ id, status }) => { - const activity = getActivity(id) - if (!activity) { - return null - } - const actionsToEmit = [] - if (activity.type === ActivityTypes.Pending) { - actionsToEmit.push({ - type: Actions.CancelActivity, - payload: { - id, - status: ActivityStatuses.Cancelled, - type: activity.type, - }, - }) - } else { - let durationMS = 0 - if (activity.status === ActivityStatuses.InProgress) { - durationMS = getElapsedTimeMS(activity) - trackCli(`ACTIVITY_DURATION`, { - name: activity.text, - duration: Math.round(durationMS), - }) - - const durationS = durationMS / 1000 - if (activity.errored) { - status = ActivityStatuses.Failed - } - actionsToEmit.push({ - type: Actions.EndActivity, - payload: { - uuid: activity.uuid, - id, - status, - duration: durationS, - type: activity.type, - }, - }) - - if (activity.type !== ActivityTypes.Hidden) { - actionsToEmit.push( - actions.createLog({ - text: activity.text, - level: ActivityStatusToLogLevel[status], - duration: durationS, - statusText: - activity.statusText || - (status === ActivityStatuses.Success && - activity.type === ActivityTypes.Progress - ? `${activity.current}/${activity.total} ${( - activity.total / durationS - ).toFixed(2)}/s` - : undefined), - activity_uuid: activity.uuid, - activity_current: activity.current, - activity_total: activity.total, - activity_type: activity.type, - }) - ) - } - } - } - - const logsState = getStore().getState().logs - - const globalStatus = getGlobalStatus(id, status) - - if (globalStatus !== logsState.status) { - actionsToEmit.push(actions.setStatus(globalStatus)) - } - - return actionsToEmit - }, - - updateActivity: ({ id, ...rest }) => { - const activity = getActivity(id) - if (!activity) { - return null - } - - return { - type: Actions.UpdateActivity, - payload: { - uuid: activity.uuid, - id, - ...rest, - }, - } - }, - setActivityErrored: ({ id, ...rest }) => { - const activity = getActivity(id) - if (!activity) { - return null - } - - return { - type: Actions.ActivityErrored, - payload: { - id, - }, - } - }, - setActivityStatusText: ({ id, statusText }) => - actions.updateActivity({ - id, - statusText, - }), - setActivityTotal: ({ id, total }) => - actions.updateActivity({ - id, - total, - }), - activityTick: ({ id, increment = 1 }) => { - const activity = getActivity(id) - if (!activity) { - return null - } - - return actions.updateActivity({ - id, - current: activity.current + increment, - }) - }, -} - -module.exports = bindActionCreators(actions, dispatch) diff --git a/packages/gatsby-cli/src/reporter/redux/actions.ts b/packages/gatsby-cli/src/reporter/redux/actions.ts new file mode 100644 index 0000000000000..82f7d5dbeea5f --- /dev/null +++ b/packages/gatsby-cli/src/reporter/redux/actions.ts @@ -0,0 +1,44 @@ +import { bindActionCreators } from "redux" +import { Dispatch } from "redux" +import { dispatch } from "./" +import { + createLog as internalCreateLog, + createPendingActivity as internalCreatePendingActivity, + setStatus as internalSetStatus, + startActivity as internalStartActivity, + endActivity as internalEndActivity, + updateActivity as internalUpdateActivity, + setActivityErrored as internalSetActivityErrored, + setActivityStatusText as internalSetActivityStatusText, + setActivityTotal as internalSetActivityTotal, + activityTick as internalActivityTick, +} from "./internal-actions" + +const actions = { + createLog: internalCreateLog, + createPendingActivity: internalCreatePendingActivity, + setStatus: internalSetStatus, + startActivity: internalStartActivity, + endActivity: internalEndActivity, + updateActivity: internalUpdateActivity, + setActivityErrored: internalSetActivityErrored, + setActivityStatusText: internalSetActivityStatusText, + setActivityTotal: internalSetActivityTotal, + activityTick: internalActivityTick, +} + +const boundActions = bindActionCreators( + actions, + (dispatch as any) as Dispatch +) + +export const createLog = boundActions.createLog as typeof internalCreateLog +export const createPendingActivity = boundActions.createPendingActivity as typeof internalCreatePendingActivity +export const setStatus = boundActions.setStatus as typeof internalSetStatus +export const startActivity = boundActions.startActivity as typeof internalStartActivity +export const endActivity = boundActions.endActivity as typeof internalEndActivity +export const updateActivity = boundActions.updateActivity as typeof internalUpdateActivity +export const setActivityErrored = boundActions.setActivityErrored as typeof internalSetActivityErrored +export const setActivityStatusText = boundActions.setActivityStatusText as typeof internalSetActivityStatusText +export const setActivityTotal = boundActions.setActivityTotal as typeof internalSetActivityTotal +export const activityTick = boundActions.activityTick as typeof internalActivityTick diff --git a/packages/gatsby-cli/src/reporter/redux/index.js b/packages/gatsby-cli/src/reporter/redux/index.js deleted file mode 100644 index a52d15d2576da..0000000000000 --- a/packages/gatsby-cli/src/reporter/redux/index.js +++ /dev/null @@ -1,84 +0,0 @@ -const Redux = require(`redux`) -const reducer = require(`./reducer`) - -const { ActivityTypes, Actions } = require(`../constants`) - -let store = Redux.createStore( - Redux.combineReducers({ - logs: reducer, - }), - {} -) - -const storeSwapListeners = [] -const onLogActionListeners = new Set() - -const isInternalAction = action => { - if ( - [ - Actions.PendingActivity, - Actions.CancelActivity, - Actions.ActivityErrored, - ].includes(action.type) - ) { - return true - } - - if ([Actions.StartActivity, Actions.EndActivity].includes(action.type)) { - return action.payload.type === ActivityTypes.Hidden - } - - return false -} - -const iface = { - getStore: () => store, - dispatch: action => { - if (!action) { - return - } - - if (Array.isArray(action)) { - action.forEach(item => iface.dispatch(item)) - return - } else if (typeof action === `function`) { - action(iface.dispatch) - return - } - - action = { - ...action, - timestamp: new Date().toJSON(), - } - - store.dispatch(action) - - if (isInternalAction(action)) { - // consumers (ipc, yurnalist, json logger) shouldn't have to - // deal with actions needed just for internal tracking of status - return - } - for (const fn of onLogActionListeners) { - fn(action) - } - }, - onStoreSwap: fn => { - storeSwapListeners.push(fn) - }, - onLogAction: fn => { - onLogActionListeners.add(fn) - return () => { - onLogActionListeners.delete(fn) - } - }, - setStore: s => { - s.dispatch({ - type: Actions.SetLogs, - payload: store.getState().logs, - }) - store = s - storeSwapListeners.forEach(fn => fn(store)) - }, -} - -module.exports = iface diff --git a/packages/gatsby-cli/src/reporter/redux/index.ts b/packages/gatsby-cli/src/reporter/redux/index.ts new file mode 100644 index 0000000000000..f3c207d1467ad --- /dev/null +++ b/packages/gatsby-cli/src/reporter/redux/index.ts @@ -0,0 +1,78 @@ +import { createStore, combineReducers } from "redux" +import { reducer } from "./reducer" +import { ActionsUnion, ISetLogs } from "./types" +import { isInternalAction } from "./utils" +import { Actions } from "../constants" + +let store = createStore( + combineReducers({ + logs: reducer, + }), + {} +) + +type GatsbyCLIStore = typeof store +type StoreListener = (store: GatsbyCLIStore) => void +type ActionLogListener = (action: ActionsUnion) => any +type Thunk = (...args: any[]) => ActionsUnion + +const storeSwapListeners: StoreListener[] = [] +const onLogActionListeners = new Set() + +export const getStore = (): typeof store => store + +export const dispatch = (action: ActionsUnion | Thunk): void => { + if (!action) { + return + } + + if (Array.isArray(action)) { + action.forEach(item => dispatch(item)) + return + } else if (typeof action === `function`) { + action(dispatch) + return + } + + action = { + ...action, + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore this is a typescript no-no.. + // And i'm pretty sure this timestamp isn't used anywhere. + // but for now, the structured logs integration tests expect it + // so it's easier to leave it and then explore as a follow up + timestamp: new Date().toJSON(), + } as ActionsUnion + + store.dispatch(action) + + if (isInternalAction(action)) { + // consumers (ipc, yurnalist, json logger) shouldn't have to + // deal with actions needed just for internal tracking of status + return + } + for (const fn of onLogActionListeners) { + fn(action) + } +} + +export const onStoreSwap = (fn: StoreListener): void => { + storeSwapListeners.push(fn) +} + +export const onLogAction = (fn: ActionLogListener): (() => void) => { + onLogActionListeners.add(fn) + + return (): void => { + onLogActionListeners.delete(fn) + } +} + +export const setStore = (s: GatsbyCLIStore): void => { + s.dispatch({ + type: Actions.SetLogs, + payload: store.getState().logs, + } as ISetLogs) + store = s + storeSwapListeners.forEach(fn => fn(store)) +} diff --git a/packages/gatsby-cli/src/reporter/redux/internal-actions.ts b/packages/gatsby-cli/src/reporter/redux/internal-actions.ts new file mode 100644 index 0000000000000..c89f6499000af --- /dev/null +++ b/packages/gatsby-cli/src/reporter/redux/internal-actions.ts @@ -0,0 +1,378 @@ +/* eslint-disable @typescript-eslint/camelcase */ + +import uuidv4 from "uuid/v4" +import { trackCli } from "gatsby-telemetry" +import signalExit from "signal-exit" +import { Dispatch } from "redux" + +import { getStore } from "./" +import { + Actions, + ActivityLogLevels, + ActivityStatuses, + ActivityTypes, +} from "../constants" +import { + IPendingActivity, + ICreateLog, + ISetStatus, + IStartActivity, + ICancelActivity, + IEndActivity, + IUpdateActivity, + IActivityErrored, + IGatsbyCLIState, + ISetLogs, +} from "./types" +import { + delayedCall, + getActivity, + getElapsedTimeMS, + getGlobalStatus, +} from "./utils" +import { IStructuredError } from "../../structured-errors/types" + +const ActivityStatusToLogLevel = { + [ActivityStatuses.Interrupted]: ActivityLogLevels.Interrupted, + [ActivityStatuses.Failed]: ActivityLogLevels.Failed, + [ActivityStatuses.Success]: ActivityLogLevels.Success, +} + +let weShouldExit = false +signalExit(() => { + weShouldExit = true +}) + +let cancelDelayedSetStatus: (() => void) | null +// TODO: THIS IS NOT WORKING ATM +export const setStatus = (status: string, force: boolean = false) => ( + dispatch: Dispatch +): void => { + const currentStatus = getStore().getState().logs.status + if (cancelDelayedSetStatus) { + cancelDelayedSetStatus() + cancelDelayedSetStatus = null + } + if (status !== currentStatus) { + if (status === `IN_PROGRESS` || force || weShouldExit) { + dispatch({ + type: Actions.SetStatus, + payload: status, + }) + } else { + cancelDelayedSetStatus = delayedCall(() => { + setStatus(status, true)(dispatch) + }, 1000) + } + } +} + +export const createLog = ({ + level, + text, + statusText, + duration, + group, + code, + type, + filePath, + location, + docsUrl, + context, + activity_current, + activity_total, + activity_type, + activity_uuid, + stack, +}: { + level: string + text?: string + statusText?: string + duration?: number + group?: string + code?: string + type?: string + filePath?: string + location?: IStructuredError["location"] + docsUrl?: string + context?: string + activity_current?: number + activity_total?: number + activity_type?: string + activity_uuid?: string + stack?: IStructuredError["stack"] +}): ICreateLog => { + return { + type: Actions.Log, + payload: { + level, + text, + statusText, + duration, + group, + code, + type, + filePath, + location, + docsUrl, + context, + activity_current, + activity_total, + activity_type, + activity_uuid, + timestamp: new Date().toJSON(), + stack, + }, + } +} + +type ActionsToEmit = Array> +export const createPendingActivity = ({ + id, + status = ActivityStatuses.NotStarted, +}: { + id: string + status?: ActivityStatuses +}): ActionsToEmit => { + const actionsToEmit: ActionsToEmit = [] + + const logsState = getStore().getState().logs + + const globalStatus = getGlobalStatus(id, status) + + if (globalStatus !== logsState.status) { + actionsToEmit.push(setStatus(globalStatus)) + } + + actionsToEmit.push({ + type: Actions.PendingActivity, + payload: { + id, + type: ActivityTypes.Pending, + status, + }, + }) + + return actionsToEmit +} + +type QueuedStartActivityActions = Array< + IStartActivity | ReturnType +> +export const startActivity = ({ + id, + text, + type, + status = ActivityStatuses.InProgress, + current, + total, +}: { + id: string + text: string + type: ActivityTypes + status?: ActivityStatuses + current?: number + total?: number +}): QueuedStartActivityActions => { + const actionsToEmit: QueuedStartActivityActions = [] + + const logsState = getStore().getState().logs + + const globalStatus = getGlobalStatus(id, status) + + if (globalStatus !== logsState.status) { + actionsToEmit.push(setStatus(globalStatus)) + } + + actionsToEmit.push({ + type: Actions.StartActivity, + payload: { + id, + uuid: uuidv4(), + text, + type, + status, + startTime: process.hrtime(), + statusText: ``, + current, + total, + }, + }) + + return actionsToEmit +} + +type QueuedEndActivity = Array< + ICancelActivity | IEndActivity | ICreateLog | ReturnType +> + +export const endActivity = ({ + id, + status, +}: { + id: string + status: ActivityStatuses +}): QueuedEndActivity | null => { + const activity = getActivity(id) + if (!activity) { + return null + } + + const actionsToEmit: QueuedEndActivity = [] + const durationMS = getElapsedTimeMS(activity) + const durationS = durationMS / 1000 + + if (activity.type === ActivityTypes.Pending) { + actionsToEmit.push({ + type: Actions.CancelActivity, + payload: { + id, + status: ActivityStatuses.Cancelled, + type: activity.type, + duration: durationS, + }, + }) + } else if (activity.status === ActivityStatuses.InProgress) { + trackCli(`ACTIVITY_DURATION`, { + name: activity.text, + duration: Math.round(durationMS), + }) + + if (activity.errored) { + status = ActivityStatuses.Failed + } + actionsToEmit.push({ + type: Actions.EndActivity, + payload: { + uuid: activity.uuid, + id, + status, + duration: durationS, + type: activity.type, + }, + }) + + if (activity.type !== ActivityTypes.Hidden) { + actionsToEmit.push( + createLog({ + text: activity.text, + level: ActivityStatusToLogLevel[status], + duration: durationS, + statusText: + activity.statusText || + (status === ActivityStatuses.Success && + activity.type === ActivityTypes.Progress + ? `${activity.current}/${activity.total} ${( + (activity.total || 0) / durationS + ).toFixed(2)}/s` + : undefined), + activity_uuid: activity.uuid, + activity_current: activity.current, + activity_total: activity.total, + activity_type: activity.type, + }) + ) + } + } + + const logsState = getStore().getState().logs + + const globalStatus = getGlobalStatus(id, status) + + if (globalStatus !== logsState.status) { + actionsToEmit.push(setStatus(globalStatus)) + } + + return actionsToEmit +} + +export const updateActivity = ({ + id = ``, + ...rest +}: { + id: string + statusText?: string + total?: number + current?: number +}): IUpdateActivity | null => { + const activity = getActivity(id) + if (!activity) { + return null + } + + return { + type: Actions.UpdateActivity, + payload: { + uuid: activity.uuid, + id, + ...rest, + }, + } +} + +export const setActivityErrored = ({ + id, +}: { + id: string +}): IActivityErrored | null => { + const activity = getActivity(id) + if (!activity) { + return null + } + + return { + type: Actions.ActivityErrored, + payload: { + id, + }, + } +} + +export const setActivityStatusText = ({ + id, + statusText, +}: { + id: string + statusText: string +}): IUpdateActivity | null => + updateActivity({ + id, + statusText, + }) + +export const setActivityTotal = ({ + id, + total, +}: { + id: string + total: number +}): IUpdateActivity | null => + updateActivity({ + id, + total, + }) + +export const activityTick = ({ + id, + increment = 1, +}: { + id: string + increment: number +}): IUpdateActivity | null => { + const activity = getActivity(id) + if (!activity) { + return null + } + + return updateActivity({ + id, + current: (activity.current || 0) + increment, + }) +} + +export const setLogs = (logs: IGatsbyCLIState): ISetLogs => { + return { + type: Actions.SetLogs, + payload: logs, + } +} diff --git a/packages/gatsby-cli/src/reporter/redux/reducer.js b/packages/gatsby-cli/src/reporter/redux/reducer.ts similarity index 89% rename from packages/gatsby-cli/src/reporter/redux/reducer.js rename to packages/gatsby-cli/src/reporter/redux/reducer.ts index 6de4219365c39..716d398ffa3fe 100644 --- a/packages/gatsby-cli/src/reporter/redux/reducer.js +++ b/packages/gatsby-cli/src/reporter/redux/reducer.ts @@ -1,13 +1,14 @@ -const { Actions } = require(`../constants`) +import { ActionsUnion, IGatsbyCLIState, ISetLogs } from "./types" +import { Actions } from "../constants" -module.exports = ( - state = { +export const reducer = ( + state: IGatsbyCLIState = { messages: [], activities: {}, status: ``, }, - action -) => { + action: ActionsUnion | ISetLogs +): IGatsbyCLIState => { switch (action.type) { case Actions.SetStatus: { return { @@ -96,5 +97,6 @@ module.exports = ( return action.payload } } + return state } diff --git a/packages/gatsby-cli/src/reporter/redux/types.ts b/packages/gatsby-cli/src/reporter/redux/types.ts new file mode 100644 index 0000000000000..102264dccbbac --- /dev/null +++ b/packages/gatsby-cli/src/reporter/redux/types.ts @@ -0,0 +1,123 @@ +import { Actions, ActivityStatuses, ActivityTypes } from "../constants" +import { IStructuredError } from "../../structured-errors/types" + +export interface IGatsbyCLIState { + messages: ILog[] + activities: { + [id: string]: IActivity + } + status: string +} + +export type ActionsUnion = + | ICreateLog + | ISetStatus + | IEndActivity + | IPendingActivity + | IStartActivity + | ICancelActivity + | IUpdateActivity + | IActivityErrored + | ISetLogs + +export interface IActivity { + startTime?: [number, number] + id: string + uuid: string + text: string + type: ActivityTypes + status: ActivityStatuses + statusText: string + current?: number + total?: number + duration?: number + errored?: boolean +} + +interface ILog { + level: string + text: string | undefined + statusText: string | undefined + duration: number | undefined + group: string | undefined + code: string | undefined + type: string | undefined + filePath: string | undefined + location: IStructuredError["location"] | undefined + docsUrl: string | undefined + context: string | undefined + activity_current: number | undefined + activity_total: number | undefined + activity_type: string | undefined + activity_uuid: string | undefined + timestamp: string + stack: IStructuredError["stack"] | undefined +} + +export interface ICreateLog { + type: Actions.Log + payload: ILog +} + +export interface ISetStatus { + type: Actions.SetStatus + payload: string +} + +export interface IPendingActivity { + type: Actions.PendingActivity + payload: { + id: string + type: ActivityTypes + status: ActivityStatuses + } +} + +export interface IStartActivity { + type: Actions.StartActivity + payload: IActivity +} + +export interface ICancelActivity { + type: Actions.CancelActivity + payload: { + id: string + status: ActivityStatuses.Cancelled + duration: number + type: ActivityTypes + } +} + +export interface IEndActivity { + type: Actions.EndActivity + payload: { + uuid: string + id: string + status: ActivityStatuses + duration: number + type: ActivityTypes + } +} + +export interface IUpdateActivity { + type: Actions.UpdateActivity + payload: { + uuid: string + id: string + statusText?: string + total?: number + current?: number + } +} + +export interface IActivityErrored { + type: Actions.ActivityErrored + payload: { + id: string + } +} + +export interface ISetLogs { + type: Actions.SetLogs + payload: IGatsbyCLIState +} diff --git a/packages/gatsby-cli/src/reporter/redux/utils.ts b/packages/gatsby-cli/src/reporter/redux/utils.ts new file mode 100644 index 0000000000000..f71a3fad20546 --- /dev/null +++ b/packages/gatsby-cli/src/reporter/redux/utils.ts @@ -0,0 +1,89 @@ +import { getStore } from "./index" +import convertHrtime from "convert-hrtime" +import { Actions, ActivityTypes, ActivityStatuses } from "../constants" +import { ActionsUnion, IActivity } from "./types" +import signalExit from "signal-exit" + +export const getGlobalStatus = ( + id: string, + status: ActivityStatuses +): ActivityStatuses => { + const { logs } = getStore().getState() + + const currentActivities = [id, ...Object.keys(logs.activities)] + + return currentActivities.reduce( + ( + generatedStatus: ActivityStatuses, + activityId: string + ): ActivityStatuses => { + const activityStatus = + activityId === id ? status : logs.activities[activityId].status + + if ( + activityStatus === ActivityStatuses.InProgress || + activityStatus === ActivityStatuses.NotStarted + ) { + return ActivityStatuses.InProgress + } else if ( + activityStatus === ActivityStatuses.Failed && + generatedStatus !== ActivityStatuses.InProgress + ) { + return ActivityStatuses.Failed + } else if ( + activityStatus === ActivityStatuses.Interrupted && + generatedStatus !== ActivityStatuses.InProgress + ) { + return ActivityStatuses.Interrupted + } + return generatedStatus + }, + ActivityStatuses.Success + ) +} + +export const getActivity = (id: string): IActivity | null => + getStore().getState().logs.activities[id] + +/** + * @returns {Number} Milliseconds from activity start + */ +export const getElapsedTimeMS = (activity: IActivity): number => { + const elapsed = process.hrtime(activity.startTime) + return convertHrtime(elapsed).milliseconds +} + +export const isInternalAction = (action: ActionsUnion): boolean => { + switch (action.type) { + case Actions.PendingActivity: + case Actions.CancelActivity: + case Actions.ActivityErrored: + return true + case Actions.StartActivity: + case Actions.EndActivity: + return action.payload.type === ActivityTypes.Hidden + default: + return false + } +} + +/** + * Like setTimeout, but also handle signalExit + */ +export const delayedCall = (fn: () => void, timeout: number): (() => void) => { + const fnWrap = (): void => { + fn() + // eslint-disable-next-line @typescript-eslint/no-use-before-define + clear() + } + + const timeoutID = setTimeout(fnWrap, timeout) + const cancelSignalExit = signalExit(fnWrap) + + const clear = (): void => { + clearTimeout(timeoutID) + cancelSignalExit() + } + + return clear +} diff --git a/packages/gatsby-cli/src/reporter/reporter-phantom.ts b/packages/gatsby-cli/src/reporter/reporter-phantom.ts index b052561c91eb9..40b7d8dbf1810 100644 --- a/packages/gatsby-cli/src/reporter/reporter-phantom.ts +++ b/packages/gatsby-cli/src/reporter/reporter-phantom.ts @@ -1,4 +1,4 @@ -import reporterActions from "./redux/actions" +import * as reporterActions from "./redux/actions" import { ActivityStatuses, ActivityTypes } from "./constants" import { Span } from "opentracing" diff --git a/packages/gatsby-cli/src/reporter/reporter-progress.ts b/packages/gatsby-cli/src/reporter/reporter-progress.ts index eb0477bb8d177..56d1c7ea4a0f2 100644 --- a/packages/gatsby-cli/src/reporter/reporter-progress.ts +++ b/packages/gatsby-cli/src/reporter/reporter-progress.ts @@ -1,4 +1,4 @@ -import reporterActions from "./redux/actions" +import * as reporterActions from "./redux/actions" import { ActivityStatuses, ActivityTypes } from "./constants" import { Span } from "opentracing" import { reporter as gatsbyReporter } from "./reporter" diff --git a/packages/gatsby-cli/src/reporter/reporter-timer.ts b/packages/gatsby-cli/src/reporter/reporter-timer.ts index 3d19179422fdf..4dd4749ebeffb 100644 --- a/packages/gatsby-cli/src/reporter/reporter-timer.ts +++ b/packages/gatsby-cli/src/reporter/reporter-timer.ts @@ -2,7 +2,7 @@ * This module is used when calling reporter. * these logs */ -import reporterActions from "./redux/actions" +import * as reporterActions from "./redux/actions" import { ActivityStatuses, ActivityTypes } from "./constants" import { Span } from "opentracing" import { reporter as gatsbyReporter } from "./reporter" @@ -38,7 +38,7 @@ export const createTimerReporter = ({ start(): void { reporterActions.startActivity({ id, - text, + text: text || `__timer__`, type: ActivityTypes.Spinner, }) }, diff --git a/packages/gatsby-cli/src/reporter/reporter.ts b/packages/gatsby-cli/src/reporter/reporter.ts index 87be5a5b65f35..0d566f047752f 100644 --- a/packages/gatsby-cli/src/reporter/reporter.ts +++ b/packages/gatsby-cli/src/reporter/reporter.ts @@ -3,7 +3,7 @@ import chalk from "chalk" import { trackError } from "gatsby-telemetry" import { globalTracer, Span } from "opentracing" -import reporterActions from "./redux/actions" +import * as reporterActions from "./redux/actions" import { LogLevels, ActivityStatuses } from "./constants" import { getErrorFormatter } from "./errors" import constructError from "../structured-errors/construct-error" diff --git a/packages/gatsby/src/redux/reducers/index.js b/packages/gatsby/src/redux/reducers/index.js index 86a4e2509a487..99eb602d82163 100644 --- a/packages/gatsby/src/redux/reducers/index.js +++ b/packages/gatsby/src/redux/reducers/index.js @@ -1,6 +1,7 @@ const reduxNodes = require(`./nodes`) const lokiNodes = require(`../../db/loki/nodes`).reducer import { redirectsReducer } from "./redirects" +import { reducer as logReducer } from "gatsby-cli/lib/reporter/redux/reducer" const backend = process.env.GATSBY_DB_NODES || `redux` @@ -64,7 +65,7 @@ module.exports = { babelrc: require(`./babelrc`), schemaCustomization: require(`./schema-customization`), themes: require(`./themes`), - logs: require(`gatsby-cli/lib/reporter/redux/reducer`), + logs: logReducer, inferenceMetadata: require(`./inference-metadata`), pageDataStats: require(`./page-data-stats`), pageData: require(`./page-data`),