An easy-to-use platform for EEG experimentation in the classroom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

additional cleanup from rebase

TODO:

- jupyterEpics.ts --> pyodideEpics.ts, rm pyodideEpics.js

- combine pyodideActions.js into pyodideAcions.ts

- pass linting

+339 -416
+5 -11
app/actions/jupyterActions.ts app/actions/pyodideActions.ts
··· 1 1 import { createAction } from '@reduxjs/toolkit'; 2 2 import { ActionType } from 'typesafe-actions'; 3 - import { JUPYTER_VARIABLE_NAMES } from '../constants/constants'; 3 + import { PYODIDE_VARIABLE_NAMES } from '../constants/constants'; 4 4 5 5 // ------------------------------------------------------------------------- 6 6 // Actions 7 7 8 - export const JupyterActions = { 9 - LaunchKernel: createAction('LAUNCH_KERNEL'), 10 - RequestKernelInfo: createAction('REQUEST_KERNEL_INFO'), 8 + export const PyodideActions = { 11 9 SendExecuteRequest: createAction<string, 'SEND_EXECUTE_REQUEST'>( 12 10 'SEND_EXECUTE_REQUEST' 13 11 ), ··· 19 17 LoadERP: createAction<string, 'LOAD_ERP'>('LOAD_ERP'), 20 18 LoadTopo: createAction('LOAD_TOPO'), 21 19 CleanEpochs: createAction('CLEAN_EPOCHS'), 22 - CloseKernel: createAction('CLOSE_KERNEL'), 23 - SetKernel: createAction<any, 'SET_KERNEL'>('SET_KERNEL'), 24 - GetEpochsInfo: createAction<JUPYTER_VARIABLE_NAMES, 'GET_EPOCHS_INFO'>( 20 + GetEpochsInfo: createAction<PYODIDE_VARIABLE_NAMES, 'GET_EPOCHS_INFO'>( 25 21 'GET_EPOCHS_INFO' 26 22 ), 27 23 GetChannelInfo: createAction('GET_CHANNEL_INFO'), 28 - SetKernelStatus: createAction<any, 'SET_KERNEL_STATUS'>('SET_KERNEL_STATUS'), 29 - SetKernelInfo: createAction<any, 'SET_KERNEL_INFO'>('SET_KERNEL_INFO'), 30 24 SetMainChannel: createAction<any, 'SET_MAIN_CHANNEL'>('SET_MAIN_CHANNEL'), 31 25 SetEpochInfo: createAction<any, 'SET_EPOCH_INFO'>('SET_EPOCH_INFO'), 32 26 SetChannelInfo: createAction<any, 'SET_CHANNEL_INFO'>('SET_CHANNEL_INFO'), ··· 45 39 ReceiveStream: createAction<any, 'RECEIVE_STREAM'>('RECEIVE_STREAM'), 46 40 } as const; 47 41 48 - export type JupyterActionType = ActionType< 49 - typeof JupyterActions[keyof typeof JupyterActions] 42 + export type PyodideActionType = ActionType< 43 + typeof PyodideActions[keyof typeof PyodideActions] 50 44 >;
+7 -10
app/components/AnalyzeComponent.tsx
··· 34 34 } from '../utils/behavior/compute'; 35 35 import SecondaryNavComponent from './SecondaryNavComponent'; 36 36 import ClickableHeadDiagramSVG from './svgs/ClickableHeadDiagramSVG'; 37 - import JupyterPlotWidget from './JupyterPlotWidget'; 37 + import PyodidePlotWidget from './PyodidePlotWidget'; 38 38 import { HelpButton } from './CollectComponent/HelpSidebar'; 39 39 import { Kernel } from '../constants/interfaces'; 40 - import { JupyterActions } from '../actions/jupyterActions'; 40 + import { PyodideActions } from '../actions/pyodideActions'; 41 41 42 42 const ANALYZE_STEPS = { 43 43 OVERVIEW: 'OVERVIEW', ··· 74 74 [key: string]: string; 75 75 }; 76 76 77 - JupyterActions: typeof JupyterActions; 77 + PyodideActions: typeof PyodideActions; 78 78 } 79 79 80 80 interface State { ··· 163 163 const workspaceCleanData = await readWorkspaceCleanedEEGData( 164 164 this.props.title 165 165 ); 166 - if (this.props.kernelStatus === KERNEL_STATUS.OFFLINE) { 167 - this.props.JupyterActions.LaunchKernel(); 168 - } 169 166 const behavioralData = await readWorkspaceBehaviorData(this.props.title); 170 167 this.setState({ 171 168 eegFilePaths: workspaceCleanData.map((filepath) => ({ ··· 200 197 selectedFilePaths: data.value, 201 198 selectedSubjects: getSubjectNamesFromFiles(data.value), 202 199 }); 203 - this.props.JupyterActions.LoadCleanedEpochs(data.value); 200 + this.props.PyodideActions.LoadCleanedEpochs(data.value); 204 201 } 205 202 } 206 203 ··· 343 340 344 341 handleChannelSelect(channelName: string) { 345 342 this.setState({ selectedChannel: channelName }); 346 - this.props.JupyterActions.LoadERP(channelName); 343 + this.props.PyodideActions.LoadERP(channelName); 347 344 } 348 345 349 346 handleStepClick(step: string) { ··· 480 477 </Segment> 481 478 </Grid.Column> 482 479 <Grid.Column width={8}> 483 - <JupyterPlotWidget 480 + <PyodidePlotWidget 484 481 title={this.props.title} 485 482 imageTitle={`${this.concatSubjectNames( 486 483 this.state.selectedSubjects ··· 509 506 </Segment> 510 507 </Grid.Column> 511 508 <Grid.Column width={8}> 512 - <JupyterPlotWidget 509 + <PyodidePlotWidget 513 510 title={this.props.title} 514 511 imageTitle={`${this.concatSubjectNames( 515 512 this.state.selectedSubjects
+2 -5
app/components/CleanComponent/index.tsx
··· 21 21 import { EXPERIMENTS, DEVICES } from '../../constants/constants'; 22 22 import { readWorkspaceRawEEGData } from '../../utils/filesystem/storage'; 23 23 import CleanSidebar from './CleanSidebar'; 24 - import { JupyterActions, ExperimentActions } from '../../actions'; 24 + import { PyodideActions, ExperimentActions } from '../../actions'; 25 25 26 26 export interface Props { 27 27 type?: EXPERIMENTS; ··· 224 224 <Divider hidden section /> 225 225 <Grid textAlign="center" columns="equal"> 226 226 <Grid.Column> 227 - <Button 228 - secondary 229 - onClick={this.handleLoadData} 230 - > 227 + <Button secondary onClick={this.handleLoadData}> 231 228 Load Dataset 232 229 </Button> 233 230 </Grid.Column>
+2 -7
app/components/HomeComponent/index.tsx
··· 10 10 import { 11 11 EXPERIMENTS, 12 12 SCREENS, 13 - KERNEL_STATUS, 14 13 CONNECTION_STATUS, 15 14 DEVICE_AVAILABILITY, 16 15 DEVICES, ··· 41 40 import { SignalQualityData } from '../../constants/interfaces'; 42 41 import { getExperimentFromType } from '../../utils/labjs/functions'; 43 42 import { languagePluginLoader } from '../../utils/pyodide/pyodide'; 44 - 45 43 const { dialog } = remote; 46 44 47 - // this initiates pyodide 48 - languagePluginLoader; 49 45 50 46 const HOME_STEPS = { 51 47 // TODO: maybe change the recent and new labels, but not necessary right now ··· 65 61 ExperimentActions: typeof ExperimentActions; 66 62 history: History; 67 63 PyodideActions: typeof PyodideActions; 68 - kernelStatus: KERNEL_STATUS; 69 64 signalQualityObservable?: Observable<SignalQualityData>; 70 65 } 71 66 ··· 98 93 } 99 94 100 95 componentDidMount() { 101 - this.props.pyodideActions.launch(); 96 + this.props.PyodideActions.launch(); 102 97 this.setState({ recentWorkspaces: readWorkspaces() }); 103 98 } 104 99 ··· 231 226 if (!workspaceState) { 232 227 return undefined; 233 228 } 234 - const dateModified = workspaceState.dateModified; 229 + const { dateModified } = workspaceState; 235 230 return ( 236 231 <Table.Row key={dir} className={styles.experimentRow}> 237 232 <Table.Cell className={styles.experimentRowName}>
+1 -1
app/components/JupyterPlotWidget.tsx app/components/PyodidePlotWidget.tsx
··· 55 55 } 56 56 57 57 handleSave() { 58 - const buf = Buffer.from(this.state.rawData, "base64"); 58 + const buf = Buffer.from(this.state.rawData, 'base64'); 59 59 storePyodideImage(this.props.title, this.props.imageTitle, buf); 60 60 } 61 61
-10
app/constants/interfaces.ts
··· 93 93 } 94 94 95 95 // -------------------------------------------------------------------- 96 - // Jupyter 97 - 98 - export interface Kernel { 99 - config: Record<string, any>; 100 - connectionFile: string; 101 - kernelSpec: Record<string, any>; 102 - spawn: ChildProcess; 103 - } 104 - 105 - // -------------------------------------------------------------------- 106 96 // Device 107 97 108 98 // TODO: Write interfaces for device objects (Observables, Classes, etc)
+1 -1
app/containers/AnalyzeContainer.ts
··· 9 9 type: state.experiment.type, 10 10 deviceType: state.device.deviceType, 11 11 isEEGEnabled: state.experiment.isEEGEnabled, 12 - ...state.pyodide 12 + ...state.pyodide, 13 13 }; 14 14 } 15 15
+1 -1
app/containers/CleanContainer.ts
··· 11 11 group: state.experiment.group, 12 12 session: state.experiment.session, 13 13 deviceType: state.device.deviceType, 14 - ...state.pyodide 14 + ...state.pyodide, 15 15 }; 16 16 } 17 17
+1 -3
app/containers/HomeContainer.ts
··· 11 11 }; 12 12 } 13 13 14 - export default connect( 15 - mapDispatchToProps 16 - )(Home); 14 + export default connect(mapDispatchToProps)(Home);
+7 -8
app/epics/jupyterEpics.ts
··· 16 16 import { kernelInfoRequest, executeRequest } from '@nteract/messaging'; 17 17 import { toast } from 'react-toastify'; 18 18 import { isActionOf } from '../utils/redux'; 19 - import { JupyterActions, JupyterActionType } from '../actions'; 20 - import { execute, awaitOkMessage } from '../utils/jupyter/pipes'; 19 + import { PyodideActions, PyodideActionType } from '../actions'; 20 + import { execute, awaitOkMessage } from '../utils/pyodide/pipes'; 21 21 import { RootState } from '../reducers'; 22 22 import { getWorkspaceDir } from '../utils/filesystem/storage'; 23 23 import { ··· 34 34 plotERP, 35 35 plotTopoMap, 36 36 saveEpochs, 37 - } from '../utils/jupyter/cells'; 37 + } from '../utils/pyodide/cells'; 38 38 import { 39 39 EMOTIV_CHANNELS, 40 40 EVENTS, 41 41 DEVICES, 42 42 MUSE_CHANNELS, 43 - JUPYTER_VARIABLE_NAMES, 43 + PYODIDE_VARIABLE_NAMES, 44 44 } from '../constants/constants'; 45 45 import { 46 46 parseSingleQuoteJSON, 47 - parseKernelStatus, 48 47 debugParseMessage, 49 - } from '../utils/jupyter/functions'; 48 + } from '../utils/pyodide/functions'; 50 49 51 50 // ------------------------------------------------------------------------- 52 51 // Epics 53 52 54 - const launchEpic: Epic<JupyterActionType, JupyterActionType, RootState> = ( 53 + const launchEpic: Epic<PyodideActionType, PyodideActionType, RootState> = ( 55 54 action$ 56 55 ) => 57 56 action$.pipe( 58 - filter(isActionOf(JupyterActions.LaunchKernel)), 57 + filter(isActionOf(PyodideActions.LaunchKernel)), 59 58 mergeMap(() => from(find('brainwaves'))), 60 59 tap((kernelInfo) => { 61 60 if (isNil(kernelInfo)) {
+25 -8
app/epics/pyodideEpics.js
··· 122 122 action$.ofType(SET_PYODIDE_WORKER).pipe( 123 123 pluck('payload'), 124 124 tap((e) => 125 - toast.error(`Error in pyodideWorker at ${e.filename}, Line: ${e.lineno}, ${e.message}`) 125 + toast.error( 126 + `Error in pyodideWorker at ${e.filename}, Line: ${e.lineno}, ${e.message}` 127 + ) 126 128 ), 127 129 map(receivePyodideError) 128 130 ); ··· 171 173 filter((filePathsArray) => filePathsArray.length >= 1), 172 174 map((filePathsArray) => loadCleanedEpochs(filePathsArray)), 173 175 mergeMap(() => 174 - of(getEpochsInfo(PYODIDE_VARIABLE_NAMES.CLEAN_EPOCHS), getChannelInfo(), loadTopo()) 176 + of( 177 + getEpochsInfo(PYODIDE_VARIABLE_NAMES.CLEAN_EPOCHS), 178 + getChannelInfo(), 179 + loadTopo() 180 + ) 175 181 ) 176 182 ); 177 183 ··· 179 185 action$.ofType(CLEAN_EPOCHS).pipe( 180 186 map(cleanEpochsPlot), 181 187 map(() => 182 - saveEpochs(getWorkspaceDir(state$.value.experiment.title), state$.value.experiment.subject) 188 + saveEpochs( 189 + getWorkspaceDir(state$.value.experiment.title), 190 + state$.value.experiment.subject 191 + ) 183 192 ), 184 193 map(() => getEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) 185 194 ); ··· 201 210 const getChannelInfoEpic = (action$) => 202 211 action$.ofType(GET_CHANNEL_INFO).pipe( 203 212 map(requestChannelInfo), 204 - map((channelInfoString) => setChannelInfo(parseSingleQuoteJSON(channelInfoString))) 213 + map((channelInfoString) => 214 + setChannelInfo(parseSingleQuoteJSON(channelInfoString)) 215 + ) 205 216 ); 206 217 207 - const loadPSDEpic = (action$) => action$.ofType(LOAD_PSD).pipe(map(plotPSD), map(setPSDPlot)); 218 + const loadPSDEpic = (action$) => 219 + action$.ofType(LOAD_PSD).pipe(map(plotPSD), map(setPSDPlot)); 208 220 209 221 const loadTopoEpic = (action$, state$) => 210 222 action$.ofType(LOAD_TOPO).pipe( ··· 213 225 of( 214 226 setTopoPlot(topoPlot), 215 227 loadERP( 216 - state$.value.device.deviceType === DEVICES.EMOTIV ? EMOTIV_CHANNELS[0] : MUSE_CHANNELS[0] 228 + state$.value.device.deviceType === DEVICES.EMOTIV 229 + ? EMOTIV_CHANNELS[0] 230 + : MUSE_CHANNELS[0] 217 231 ) 218 232 ) 219 233 ) ··· 225 239 map((channelName) => { 226 240 if (MUSE_CHANNELS.includes(channelName)) { 227 241 return MUSE_CHANNELS.indexOf(channelName); 228 - } else if (EMOTIV_CHANNELS.includes(channelName)) { 242 + } 243 + if (EMOTIV_CHANNELS.includes(channelName)) { 229 244 return EMOTIV_CHANNELS.indexOf(channelName); 230 245 } 231 - console.warn('channel name supplied to loadERPEpic does not belong to either device'); 246 + console.warn( 247 + 'channel name supplied to loadERPEpic does not belong to either device' 248 + ); 232 249 return EMOTIV_CHANNELS[0]; 233 250 }), 234 251 map((channelIndex) => plotERP(channelIndex)),
+3 -3
app/reducers/index.ts
··· 1 1 import { combineReducers } from 'redux'; 2 2 import { connectRouter } from 'connected-react-router'; 3 3 import { History } from 'history'; 4 - import jupyter, { JupyterStateType } from './jupyterReducer'; 4 + import pyodide, { PyodideStateType } from './pyodideReducer'; 5 5 import device, { DeviceStateType } from './deviceReducer'; 6 6 import experiment, { ExperimentStateType } from './experimentReducer'; 7 7 8 8 export interface RootState { 9 - jupyter: JupyterStateType; 9 + pyodide: PyodideStateType; 10 10 device: DeviceStateType; 11 11 experiment: ExperimentStateType; 12 12 router: any; ··· 15 15 export default function createRootReducer(history: History) { 16 16 return combineReducers({ 17 17 router: connectRouter(history), 18 - jupyter, 18 + pyodide, 19 19 device, 20 20 experiment, 21 21 });
-103
app/reducers/jupyterReducer.ts
··· 1 - import { createReducer } from '@reduxjs/toolkit'; 2 - import { Kernel } from '../constants/interfaces'; 3 - import { KERNEL_STATUS } from '../constants/constants'; 4 - import { JupyterActions, ExperimentActions } from '../actions'; 5 - 6 - export interface JupyterStateType { 7 - readonly kernel: Kernel | null | undefined; 8 - readonly kernelStatus: KERNEL_STATUS; 9 - readonly mainChannel: any | null | undefined; 10 - readonly epochsInfo: Array<{ 11 - [key: string]: number | string; 12 - }>; 13 - readonly channelInfo: string[]; 14 - readonly psdPlot: 15 - | { 16 - [key: string]: string; 17 - } 18 - | null 19 - | undefined; 20 - readonly topoPlot: 21 - | { 22 - [key: string]: string; 23 - } 24 - | null 25 - | undefined; 26 - readonly erpPlot: 27 - | { 28 - [key: string]: string; 29 - } 30 - | null 31 - | undefined; 32 - } 33 - 34 - const initialState = { 35 - kernel: null, 36 - kernelStatus: KERNEL_STATUS.OFFLINE, 37 - mainChannel: null, 38 - epochsInfo: [], 39 - channelInfo: [], 40 - psdPlot: null, 41 - topoPlot: null, 42 - erpPlot: null, 43 - }; 44 - 45 - export default createReducer(initialState, (builder) => 46 - builder 47 - .addCase(JupyterActions.SetKernel, (state, action) => { 48 - return { 49 - ...state, 50 - kernel: action.payload, 51 - }; 52 - }) 53 - .addCase(JupyterActions.SetKernelStatus, (state, action) => { 54 - return { 55 - ...state, 56 - kernelStatus: action.payload, 57 - }; 58 - }) 59 - .addCase(JupyterActions.SetMainChannel, (state, action) => { 60 - return { 61 - ...state, 62 - mainChannel: action.payload, 63 - }; 64 - }) 65 - .addCase(JupyterActions.SetEpochInfo, (state, action) => { 66 - return { 67 - ...state, 68 - epochsInfo: action.payload, 69 - }; 70 - }) 71 - .addCase(JupyterActions.SetChannelInfo, (state, action) => { 72 - return { 73 - ...state, 74 - channelInfo: action.payload, 75 - }; 76 - }) 77 - .addCase(JupyterActions.SetPSDPlot, (state, action) => { 78 - return { 79 - ...state, 80 - psdPlot: action.payload, 81 - }; 82 - }) 83 - .addCase(JupyterActions.SetTopoPlot, (state, action) => { 84 - return { 85 - ...state, 86 - topoPlot: action.payload, 87 - }; 88 - }) 89 - .addCase(JupyterActions.SetERPPlot, (state, action) => { 90 - return { 91 - ...state, 92 - erpPlot: action.payload, 93 - }; 94 - }) 95 - .addCase(ExperimentActions.ExperimentCleanup, (state, action) => { 96 - return { 97 - ...state, 98 - epochsInfo: [], 99 - psdPlot: null, 100 - erpPlot: null, 101 - }; 102 - }) 103 - );
-96
app/reducers/pyodideReducer.js
··· 1 - // @flow 2 - import { 3 - SET_MAIN_CHANNEL, 4 - SET_EPOCH_INFO, 5 - SET_CHANNEL_INFO, 6 - SET_PSD_PLOT, 7 - SET_TOPO_PLOT, 8 - SET_ERP_PLOT, 9 - RECEIVE_EXECUTE_RETURN, 10 - SET_PYODIDE_STATUS 11 - } from "../epics/pyodideEpics"; 12 - import { ActionType } from "../constants/interfaces"; 13 - import { PYODIDE_STATUS } from "../constants/constants"; 14 - import { EXPERIMENT_CLEANUP } from "../epics/experimentEpics"; 15 - 16 - export interface PyodideStateType { 17 - +mainChannel: ?any; 18 - +epochsInfo: ?Array<{ [string]: number | string }>; 19 - +channelInfo: ?Array<string>; 20 - +psdPlot: ?{ [string]: string }; 21 - +topoPlot: ?{ [string]: string }; 22 - +erpPlot: ?{ [string]: string }; 23 - } 24 - 25 - const initialState = { 26 - mainChannel: null, 27 - epochsInfo: null, 28 - channelInfo: [], 29 - psdPlot: null, 30 - topoPlot: null, 31 - erpPlot: null, 32 - status: PYODIDE_STATUS.NOT_LOADED 33 - }; 34 - 35 - export default function pyodide( 36 - state: PyodideStateType = initialState, 37 - action: ActionType 38 - ) { 39 - switch (action.type) { 40 - case SET_MAIN_CHANNEL: 41 - return { 42 - ...state, 43 - mainChannel: action.payload, 44 - }; 45 - 46 - case SET_EPOCH_INFO: 47 - return { 48 - ...state, 49 - epochsInfo: action.payload, 50 - }; 51 - 52 - case SET_CHANNEL_INFO: 53 - return { 54 - ...state, 55 - channelInfo: action.payload, 56 - }; 57 - 58 - case SET_PSD_PLOT: 59 - return { 60 - ...state, 61 - psdPlot: action.payload, 62 - }; 63 - 64 - case SET_TOPO_PLOT: 65 - return { 66 - ...state, 67 - topoPlot: action.payload, 68 - }; 69 - 70 - case SET_ERP_PLOT: 71 - return { 72 - ...state, 73 - erpPlot: action.payload, 74 - }; 75 - 76 - case EXPERIMENT_CLEANUP: 77 - return { 78 - ...state, 79 - epochsInfo: null, 80 - psdPlot: null, 81 - erpPlot: null, 82 - }; 83 - 84 - case RECEIVE_EXECUTE_RETURN: 85 - return state; 86 - 87 - case SET_PYODIDE_STATUS: 88 - return { 89 - ...state, 90 - status: action.payload 91 - }; 92 - 93 - default: 94 - return state; 95 - } 96 - }
+85
app/reducers/pyodideReducer.ts
··· 1 + import { createReducer } from '@reduxjs/toolkit'; 2 + import { PyodideActions, ExperimentActions } from '../actions'; 3 + 4 + export interface PyodideStateType { 5 + readonly mainChannel: any | null | undefined; 6 + readonly epochsInfo: Array<{ 7 + [key: string]: number | string; 8 + }>; 9 + readonly channelInfo: string[]; 10 + readonly psdPlot: 11 + | { 12 + [key: string]: string; 13 + } 14 + | null 15 + | undefined; 16 + readonly topoPlot: 17 + | { 18 + [key: string]: string; 19 + } 20 + | null 21 + | undefined; 22 + readonly erpPlot: 23 + | { 24 + [key: string]: string; 25 + } 26 + | null 27 + | undefined; 28 + } 29 + 30 + const initialState = { 31 + mainChannel: null, 32 + epochsInfo: [], 33 + channelInfo: [], 34 + psdPlot: null, 35 + topoPlot: null, 36 + erpPlot: null, 37 + }; 38 + 39 + export default createReducer(initialState, (builder) => 40 + builder 41 + .addCase(PyodideActions.SetMainChannel, (state, action) => { 42 + return { 43 + ...state, 44 + mainChannel: action.payload, 45 + }; 46 + }) 47 + .addCase(PyodideActions.SetEpochInfo, (state, action) => { 48 + return { 49 + ...state, 50 + epochsInfo: action.payload, 51 + }; 52 + }) 53 + .addCase(PyodideActions.SetChannelInfo, (state, action) => { 54 + return { 55 + ...state, 56 + channelInfo: action.payload, 57 + }; 58 + }) 59 + .addCase(PyodideActions.SetPSDPlot, (state, action) => { 60 + return { 61 + ...state, 62 + psdPlot: action.payload, 63 + }; 64 + }) 65 + .addCase(PyodideActions.SetTopoPlot, (state, action) => { 66 + return { 67 + ...state, 68 + topoPlot: action.payload, 69 + }; 70 + }) 71 + .addCase(PyodideActions.SetERPPlot, (state, action) => { 72 + return { 73 + ...state, 74 + erpPlot: action.payload, 75 + }; 76 + }) 77 + .addCase(ExperimentActions.ExperimentCleanup, (state, action) => { 78 + return { 79 + ...state, 80 + epochsInfo: [], 81 + psdPlot: null, 82 + erpPlot: null, 83 + }; 84 + }) 85 + );
+1 -1
app/store/configureStore.dev.js
··· 42 42 const actionCreators = { 43 43 ...deviceActions, 44 44 ...pyodideActions, 45 - ...routerActions 45 + ...routerActions, 46 46 }; 47 47 // If Redux DevTools Extension is installed use it, otherwise use Redux compose 48 48 /* eslint-disable no-underscore-dangle */
+9 -11
app/utils/filesystem/read.js
··· 1 - const fs = require("fs"); 1 + const fs = require('fs'); 2 2 3 3 export const readFiles = (filePathsArray) => { 4 - return filePathsArray.map(path => { 5 - console.log('about to read file') 6 - const file = fs.readFileSync(path, 'utf8') 7 - console.log('read file') 8 - return file 9 - }) 10 - } 11 - 12 - 4 + return filePathsArray.map((path) => { 5 + console.log('about to read file'); 6 + const file = fs.readFileSync(path, 'utf8'); 7 + console.log('read file'); 8 + return file; 9 + }); 10 + }; 13 11 14 12 // ------------------------------------------- 15 13 // Helper methods 16 14 17 15 const formatFilePath = (filePath: string) => 18 - `"${filePath.replace(/\\/g, "/")}"`; 16 + `"${filePath.replace(/\\/g, '/')}"`;
+13 -5
app/utils/pyodide/index.js
··· 1 1 import * as path from 'path'; 2 2 import { readFileSync } from 'fs'; 3 - import { languagePluginLoader } from './pyodide'; 4 3 import { formatFilePath } from './functions'; 5 4 6 5 // --------------------------------- 7 6 // This file contains the JS functions that allow the app to access python-wasm through pyodide 8 7 // These functions wrap the python strings defined in the 9 - 10 8 11 9 // ----------------------------- 12 10 // Imports and Utility functions ··· 37 35 38 36 // NOTE: this command includes a ';' to prevent returning data 39 37 export const filterIIR = async (lowCutoff: number, highCutoff: number) => 40 - pyodideWorker.postMessage({ data: `raw.filter(${lowCutoff}, ${highCutoff}, method='iir');` }); 38 + pyodideWorker.postMessage({ 39 + data: `raw.filter(${lowCutoff}, ${highCutoff}, method='iir');`, 40 + }); 41 41 42 42 export const epochEvents = async ( 43 43 eventIDs: { [string]: number }, ··· 69 69 }; 70 70 71 71 export const requestChannelInfo = async () => 72 - pyodideWorker.postMessage({ data: `[ch for ch in clean_epochs.ch_names if ch != 'Marker']` }); 72 + pyodideWorker.postMessage({ 73 + data: `[ch for ch in clean_epochs.ch_names if ch != 'Marker']`, 74 + }); 73 75 74 76 // ----------------------------- 75 77 // Plot functions ··· 98 100 export const saveEpochs = (workspaceDir: string, subject: string) => 99 101 pyodideWorker.postMessage({ 100 102 data: `raw_epochs.save(${formatFilePath( 101 - path.join(workspaceDir, 'Data', subject, 'EEG', `${subject}-cleaned-epo.fif`) 103 + path.join( 104 + workspaceDir, 105 + 'Data', 106 + subject, 107 + 'EEG', 108 + `${subject}-cleaned-epo.fif` 109 + ) 102 110 )}`, 103 111 });
+9 -7
app/utils/pyodide/pipes.js
··· 13 13 export const awaitOkMessage = (action$) => 14 14 pipe( 15 15 mergeMap(() => 16 - action$.ofType(PyodideActions.ReceiveExecuteReply.type).pipe( 17 - pluck('payload'), 18 - filter<any>( 19 - (msg) => msg.channel === 'shell' && msg.content.status === 'ok' 20 - ), 21 - take(1) 22 - ) 16 + action$ 17 + .ofType(PyodideActions.ReceiveExecuteReply.type) 18 + .pipe( 19 + pluck('payload'), 20 + filter < 21 + any > 22 + ((msg) => msg.channel === 'shell' && msg.content.status === 'ok'), 23 + take(1) 24 + ) 23 25 ) 24 26 );
+146 -116
app/utils/pyodide/pyodide.js
··· 4 4 const port = process.env.PORT || 1212; 5 5 6 6 export const languagePluginLoader = new Promise((resolve, reject) => { 7 - 8 - var baseURL = 'http://localhost:' + port + '/src/'; 7 + let baseURL = `http://localhost:${port}/src/`; 9 8 // var baseURL = self.languagePluginUrl || 'https://iodide.io/pyodide-demo/'; 10 - baseURL = baseURL.substr(0, baseURL.lastIndexOf('/')) + '/'; 9 + baseURL = `${baseURL.substr(0, baseURL.lastIndexOf('/'))}/`; 11 10 12 - //////////////////////////////////////////////////////////// 11 + /// ///////////////////////////////////////////////////////// 13 12 // Package loading 14 - let loadedPackages = new Array(); 15 - var loadPackagePromise = new Promise((resolve) => resolve()); 13 + const loadedPackages = []; 14 + let loadPackagePromise = new Promise((resolve) => resolve()); 16 15 // Regexp for validating package name and URI 17 - var package_name_regexp = '[a-z0-9_][a-z0-9_\-]*' 18 - var package_uri_regexp = 19 - new RegExp('^https?://.*?(' + package_name_regexp + ').js$', 'i'); 20 - var package_name_regexp = new RegExp('^' + package_name_regexp + '$', 'i'); 16 + var package_name_regexp = '[a-z0-9_][a-z0-9_-]*'; 17 + const package_uri_regexp = new RegExp( 18 + `^https?://.*?(${package_name_regexp}).js$`, 19 + 'i' 20 + ); 21 + var package_name_regexp = new RegExp(`^${package_name_regexp}$`, 'i'); 21 22 22 - let _uri_to_package_name = (package_uri) => { 23 + const _uri_to_package_name = (package_uri) => { 23 24 // Generate a unique package name from URI 24 25 25 26 if (package_name_regexp.test(package_uri)) { 26 27 return package_uri; 27 - } else if (package_uri_regexp.test(package_uri)) { 28 - let match = package_uri_regexp.exec(package_uri); 28 + } 29 + if (package_uri_regexp.test(package_uri)) { 30 + const match = package_uri_regexp.exec(package_uri); 29 31 // Get the regexp group corresponding to the package name 30 32 return match[1]; 31 - } else { 32 - return null; 33 33 } 34 + return null; 34 35 }; 35 36 36 37 // clang-format off 37 - let preloadWasm = () => { 38 + const preloadWasm = () => { 38 39 // On Chrome, we have to instantiate wasm asynchronously. Since that 39 40 // can't be done synchronously within the call to dlopen, we instantiate 40 41 // every .so that comes our way up front, caching it in the 41 42 // `preloadedWasm` dictionary. 42 43 43 44 let promise = new Promise((resolve) => resolve()); 44 - let FS = pyodide._module.FS; 45 + const { FS } = pyodide._module; 45 46 46 47 function recurseDir(rootpath) { 47 48 let dirs; ··· 50 51 } catch (err) { 51 52 return; 52 53 } 53 - for (let entry of dirs) { 54 + for (const entry of dirs) { 54 55 if (entry.startsWith('.')) { 55 56 continue; 56 57 } 57 58 const path = rootpath + entry; 58 59 if (entry.endsWith('.so')) { 59 - if (Module['preloadedWasm'][path] === undefined) { 60 + if (Module.preloadedWasm[path] === undefined) { 60 61 promise = promise 61 - .then(() => Module['loadWebAssemblyModule']( 62 - FS.readFile(path), {loadAsync: true})) 62 + .then(() => 63 + Module.loadWebAssemblyModule(FS.readFile(path), { 64 + loadAsync: true, 65 + }) 66 + ) 63 67 .then((module) => { 64 - Module['preloadedWasm'][path] = module; 68 + Module.preloadedWasm[path] = module; 65 69 }); 66 70 } 67 71 } else if (FS.isDir(FS.lookupPath(path).node.mode)) { 68 - recurseDir(path + '/'); 72 + recurseDir(`${path}/`); 69 73 } 70 74 } 71 75 } ··· 73 77 recurseDir('/'); 74 78 75 79 return promise; 76 - } 80 + }; 77 81 // clang-format on 78 82 79 83 function loadScript(url, onload, onerror) { 80 - if (self.document) { // browser 84 + if (self.document) { 85 + // browser 81 86 const script = self.document.createElement('script'); 82 87 script.src = url; 83 - script.onload = (e) => { onload(); }; 84 - script.onerror = (e) => { onerror(); }; 88 + script.onload = (e) => { 89 + onload(); 90 + }; 91 + script.onerror = (e) => { 92 + onerror(); 93 + }; 85 94 self.document.head.appendChild(script); 86 - } else if (self.importScripts) { // webworker 95 + } else if (self.importScripts) { 96 + // webworker 87 97 try { 88 98 self.importScripts(url); 89 99 onload(); ··· 93 103 } 94 104 } 95 105 96 - let _loadPackage = (names, messageCallback) => { 106 + const _loadPackage = (names, messageCallback) => { 97 107 // DFS to find all dependencies of the requested packages 98 - let packages = self.pyodide._module.packages.dependencies; 99 - let loadedPackages = self.pyodide.loadedPackages; 100 - let queue = [].concat(names || []); 101 - let toLoad = new Array(); 108 + const packages = self.pyodide._module.packages.dependencies; 109 + const { loadedPackages } = self.pyodide; 110 + const queue = [].concat(names || []); 111 + const toLoad = []; 102 112 while (queue.length) { 103 113 let package_uri = queue.pop(); 104 114 ··· 107 117 if (packageName == null) { 108 118 console.error(`Invalid package name or URI '${package_uri}'`); 109 119 return; 110 - } else if (packageName == package_uri) { 120 + } 121 + if (packageName == package_uri) { 111 122 package_uri = 'default channel'; 112 123 } 113 124 114 125 if (packageName in loadedPackages) { 115 126 if (package_uri != loadedPackages[packageName]) { 116 - console.error(`URI mismatch, attempting to load package ` + 117 - `${packageName} from ${package_uri} while it is already ` + 118 - `loaded from ${loadedPackages[packageName]}!`); 127 + console.error( 128 + `URI mismatch, attempting to load package ` + 129 + `${packageName} from ${package_uri} while it is already ` + 130 + `loaded from ${loadedPackages[packageName]}!` 131 + ); 119 132 return; 120 133 } 121 134 } else if (packageName in toLoad) { 122 135 if (package_uri != toLoad[packageName]) { 123 - console.error(`URI mismatch, attempting to load package ` + 124 - `${packageName} from ${package_uri} while it is already ` + 125 - `being loaded from ${toLoad[packageName]}!`); 136 + console.error( 137 + `URI mismatch, attempting to load package ` + 138 + `${packageName} from ${package_uri} while it is already ` + 139 + `being loaded from ${toLoad[packageName]}!` 140 + ); 126 141 return; 127 142 } 128 143 } else { ··· 143 158 144 159 self.pyodide._module.locateFile = (path) => { 145 160 // handle packages loaded from custom URLs 146 - let packageName = path.replace(/\.data$/, ""); 161 + const packageName = path.replace(/\.data$/, ''); 147 162 if (packageName in toLoad) { 148 - let package_uri = toLoad[packageName]; 163 + const package_uri = toLoad[packageName]; 149 164 if (package_uri != 'default channel') { 150 - return package_uri.replace(/\.js$/, ".data"); 151 - }; 152 - }; 165 + return package_uri.replace(/\.js$/, '.data'); 166 + } 167 + } 153 168 return baseURL + path; 154 169 }; 155 170 156 - let promise = new Promise((resolve, reject) => { 171 + const promise = new Promise((resolve, reject) => { 157 172 if (Object.keys(toLoad).length === 0) { 158 173 resolve('No new packages to load'); 159 174 return; ··· 167 182 // monitorRunDependencies is called at the beginning and the end of each 168 183 // package being loaded. We know we are done when it has been called 169 184 // exactly "toLoad * 2" times. 170 - var packageCounter = Object.keys(toLoad).length * 2; 185 + let packageCounter = Object.keys(toLoad).length * 2; 171 186 172 187 self.pyodide._module.monitorRunDependencies = () => { 173 188 packageCounter--; 174 189 if (packageCounter === 0) { 175 - for (let packageName in toLoad) { 190 + for (const packageName in toLoad) { 176 191 self.pyodide.loadedPackages[packageName] = toLoad[packageName]; 177 192 } 178 193 delete self.pyodide._module.monitorRunDependencies; 179 194 self.removeEventListener('error', windowErrorHandler); 180 195 if (!isFirefox) { 181 - preloadWasm().then(() => {resolve(`Loaded ${packageList}`)}); 196 + preloadWasm().then(() => { 197 + resolve(`Loaded ${packageList}`); 198 + }); 182 199 } else { 183 200 resolve(`Loaded ${packageList}`); 184 201 } ··· 196 213 }; 197 214 self.addEventListener('error', windowErrorHandler); 198 215 199 - for (let packageName in toLoad) { 216 + for (const packageName in toLoad) { 200 217 let scriptSrc; 201 - let package_uri = toLoad[packageName]; 218 + const package_uri = toLoad[packageName]; 202 219 if (package_uri == 'default channel') { 203 220 scriptSrc = `${baseURL}${packageName}.js`; 204 221 } else { 205 222 scriptSrc = `${package_uri}`; 206 223 } 207 - loadScript(scriptSrc, () => {}, () => { 208 - // If the package_uri fails to load, call monitorRunDependencies twice 209 - // (so packageCounter will still hit 0 and finish loading), and remove 210 - // the package from toLoad so we don't mark it as loaded. 211 - console.error(`Couldn't load package from URL ${scriptSrc}`) 212 - let index = toLoad.indexOf(packageName); 213 - if (index !== -1) { 214 - toLoad.splice(index, 1); 215 - } 216 - for (let i = 0; i < 2; i++) { 217 - self.pyodide._module.monitorRunDependencies(); 224 + loadScript( 225 + scriptSrc, 226 + () => {}, 227 + () => { 228 + // If the package_uri fails to load, call monitorRunDependencies twice 229 + // (so packageCounter will still hit 0 and finish loading), and remove 230 + // the package from toLoad so we don't mark it as loaded. 231 + console.error(`Couldn't load package from URL ${scriptSrc}`); 232 + const index = toLoad.indexOf(packageName); 233 + if (index !== -1) { 234 + toLoad.splice(index, 1); 235 + } 236 + for (let i = 0; i < 2; i++) { 237 + self.pyodide._module.monitorRunDependencies(); 238 + } 218 239 } 219 - }); 240 + ); 220 241 } 221 242 222 243 // We have to invalidate Python's import caches, or it won't 223 244 // see the new files. This is done here so it happens in parallel 224 245 // with the fetching over the network. 225 - self.pyodide.runPython('import importlib as _importlib\n' + 226 - '_importlib.invalidate_caches()\n'); 246 + self.pyodide.runPython( 247 + 'import importlib as _importlib\n' + '_importlib.invalidate_caches()\n' 248 + ); 227 249 }); 228 250 229 251 return promise; 230 252 }; 231 253 232 - let loadPackage = (names, messageCallback) => { 254 + const loadPackage = (names, messageCallback) => { 233 255 /* We want to make sure that only one loadPackage invocation runs at any 234 256 * given time, so this creates a "chain" of promises. */ 235 - loadPackagePromise = 236 - loadPackagePromise.then(() => _loadPackage(names, messageCallback)); 257 + loadPackagePromise = loadPackagePromise.then(() => 258 + _loadPackage(names, messageCallback) 259 + ); 237 260 return loadPackagePromise; 238 261 }; 239 262 240 - //////////////////////////////////////////////////////////// 263 + /// ///////////////////////////////////////////////////////// 241 264 // Fix Python recursion limit 242 265 function fixRecursionLimit(pyodide) { 243 266 // The Javascript/Wasm call stack may be too small to handle the default ··· 253 276 } 254 277 try { 255 278 recurse(); 256 - } catch (err) { 257 - ; 258 - } 279 + } catch (err) {} 259 280 260 281 let recursionLimit = depth / 50; 261 282 if (recursionLimit > 1000) { 262 283 recursionLimit = 1000; 263 284 } 264 285 pyodide.runPython( 265 - `import sys; sys.setrecursionlimit(int(${recursionLimit}))`); 266 - }; 286 + `import sys; sys.setrecursionlimit(int(${recursionLimit}))` 287 + ); 288 + } 267 289 268 - //////////////////////////////////////////////////////////// 290 + /// ///////////////////////////////////////////////////////// 269 291 // Rearrange namespace for public API 270 - let PUBLIC_API = [ 292 + const PUBLIC_API = [ 271 293 'globals', 272 294 'loadPackage', 273 295 'loadedPackages', ··· 280 302 ]; 281 303 282 304 function makePublicAPI(module, public_api) { 283 - var namespace = {_module : module}; 284 - for (let name of public_api) { 305 + const namespace = { _module: module }; 306 + for (const name of public_api) { 285 307 namespace[name] = module[name]; 286 308 } 287 309 return namespace; 288 310 } 289 311 290 - //////////////////////////////////////////////////////////// 312 + /// ///////////////////////////////////////////////////////// 291 313 // Loading Pyodide 292 - let wasmURL = `${baseURL}pyodide.asm.wasm`; 314 + const wasmURL = `${baseURL}pyodide.asm.wasm`; 293 315 let Module = {}; 294 316 self.Module = Module; 295 317 ··· 299 321 Module.preloadedWasm = {}; 300 322 let isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; 301 323 302 - let wasm_promise = WebAssembly.compileStreaming(fetch(wasmURL)); 324 + const wasm_promise = WebAssembly.compileStreaming(fetch(wasmURL)); 303 325 Module.instantiateWasm = (info, receiveInstance) => { 304 - wasm_promise.then(module => WebAssembly.instantiate(module, info)) 305 - .then(instance => receiveInstance(instance)); 326 + wasm_promise 327 + .then((module) => WebAssembly.instantiate(module, info)) 328 + .then((instance) => receiveInstance(instance)); 306 329 return {}; 307 330 }; 308 331 309 - Module.checkABI = function(ABI_number) { 332 + Module.checkABI = function (ABI_number) { 310 333 if (ABI_number !== parseInt('1')) { 311 - var ABI_mismatch_exception = 312 - `ABI numbers differ. Expected 1, got ${ABI_number}`; 334 + const ABI_mismatch_exception = `ABI numbers differ. Expected 1, got ${ABI_number}`; 313 335 console.error(ABI_mismatch_exception); 314 336 throw ABI_mismatch_exception; 315 337 } ··· 317 339 }; 318 340 319 341 Module.locateFile = (path) => baseURL + path; 320 - var postRunPromise = new Promise((resolve, reject) => { 342 + const postRunPromise = new Promise((resolve, reject) => { 321 343 Module.postRun = () => { 322 344 delete self.Module; 323 345 fetch(`${baseURL}packages.json`) 324 - .then((response) => response.json()) 325 - .then((json) => { 326 - fixRecursionLimit(self.pyodide); 327 - self.pyodide.globals = 328 - self.pyodide.runPython('import sys\nsys.modules["__main__"]'); 329 - self.pyodide = makePublicAPI(self.pyodide, PUBLIC_API); 330 - self.pyodide._module.packages = json; 331 - resolve(); 332 - }); 346 + .then((response) => response.json()) 347 + .then((json) => { 348 + fixRecursionLimit(self.pyodide); 349 + self.pyodide.globals = self.pyodide.runPython( 350 + 'import sys\nsys.modules["__main__"]' 351 + ); 352 + self.pyodide = makePublicAPI(self.pyodide, PUBLIC_API); 353 + self.pyodide._module.packages = json; 354 + resolve(); 355 + }); 333 356 }; 334 357 }); 335 358 336 - var dataLoadPromise = new Promise((resolve, reject) => { 337 - Module.monitorRunDependencies = 338 - (n) => { 339 - if (n === 0) { 340 - delete Module.monitorRunDependencies; 341 - resolve(); 342 - } 343 - } 359 + const dataLoadPromise = new Promise((resolve, reject) => { 360 + Module.monitorRunDependencies = (n) => { 361 + if (n === 0) { 362 + delete Module.monitorRunDependencies; 363 + resolve(); 364 + } 365 + }; 344 366 }); 345 367 346 - Promise.all([ postRunPromise, dataLoadPromise ]).then(() => resolve()); 368 + Promise.all([postRunPromise, dataLoadPromise]).then(() => resolve()); 347 369 348 370 const data_script_src = `${baseURL}pyodide.asm.data.js`; 349 - loadScript(data_script_src, () => { 350 - const scriptSrc = `${baseURL}pyodide.asm.js`; 351 - loadScript(scriptSrc, () => { 352 - // The emscripten module needs to be at this location for the core 353 - // filesystem to install itself. Once that's complete, it will be replaced 354 - // by the call to `makePublicAPI` with a more limited public API. 355 - self.pyodide = pyodide(Module); 356 - self.pyodide.loadedPackages = new Array(); 357 - self.pyodide.loadPackage = loadPackage; 358 - }, () => {}); 359 - }, () => {}); 371 + loadScript( 372 + data_script_src, 373 + () => { 374 + const scriptSrc = `${baseURL}pyodide.asm.js`; 375 + loadScript( 376 + scriptSrc, 377 + () => { 378 + // The emscripten module needs to be at this location for the core 379 + // filesystem to install itself. Once that's complete, it will be replaced 380 + // by the call to `makePublicAPI` with a more limited public API. 381 + self.pyodide = pyodide(Module); 382 + self.pyodide.loadedPackages = []; 383 + self.pyodide.loadPackage = loadPackage; 384 + }, 385 + () => {} 386 + ); 387 + }, 388 + () => {} 389 + ); 360 390 });
+3 -3
app/utils/pyodide/webworker.js
··· 6 6 self.languagePluginUrl = './src'; 7 7 importScripts('./pyodide.js'); 8 8 9 - const onmessage = function(e) { 9 + const onmessage = function (e) { 10 10 // eslint-disable-line no-unused-vars 11 11 languagePluginLoader.then(() => { 12 12 // Preloaded packages 13 13 self.pyodide.loadPackage(['matplotlib', 'mne', 'pandas']).then(() => { 14 - const data = e.data; 14 + const { data } = e; 15 15 const keys = Object.keys(data); 16 - for (let key of keys) { 16 + for (const key of keys) { 17 17 if (key !== 'python') { 18 18 // Keys other than python must be arguments for the python script. 19 19 // Set them on self, so that `from js import key` works.
+5 -2
configs/webpack.config.renderer.dev.babel.js
··· 246 246 inline: true, 247 247 lazy: false, 248 248 hot: true, 249 - headers: { "Access-Control-Allow-Origin": "*" }, 250 - contentBase: [path.join(__dirname, "app", "dist"), path.join(__dirname, "app", "utils", "pyodide")], 249 + headers: { 'Access-Control-Allow-Origin': '*' }, 250 + contentBase: [ 251 + path.join(__dirname, 'app', 'dist'), 252 + path.join(__dirname, 'app', 'utils', 'pyodide'), 253 + ], 251 254 watchOptions: { 252 255 aggregateTimeout: 300, 253 256 ignored: /node_modules/,
-1
environment.yml
··· 13 13 - jupyter 14 14 - pip: 15 15 - mne 16 - - pyzmq>=18.0.1
+11 -3
internals/scripts/InstallPyodide.js
··· 33 33 }; 34 34 35 35 const downloadFile = (response) => { 36 - if (response.statusCode > 300 && response.statusCode < 400 && response.headers.location) { 36 + if ( 37 + response.statusCode > 300 && 38 + response.statusCode < 400 && 39 + response.headers.location 40 + ) { 37 41 if (url.parse(response.headers.location).hostname) { 38 42 https.get(response.headers.location, writeAndUnzipFile); 39 43 } else { ··· 49 53 50 54 (() => { 51 55 if (fs.existsSync(`${PYODIDE_DIR}${TAR_NAME}`)) { 52 - console.log(`${chalk.green.bold(`Pyodide is already present: ${PYODIDE_VERSION}...`)}`); 56 + console.log( 57 + `${chalk.green.bold(`Pyodide is already present: ${PYODIDE_VERSION}...`)}` 58 + ); 53 59 return; 54 60 } 55 - console.log(`${chalk.green.bold(`Downloading pyodide ${PYODIDE_VERSION}...`)}`); 61 + console.log( 62 + `${chalk.green.bold(`Downloading pyodide ${PYODIDE_VERSION}...`)}` 63 + ); 56 64 mkdirp.sync(`app/utils/pyodide/src`); 57 65 https.get(TAR_URL, downloadFile); 58 66 })();
+2
package.json
··· 258 258 "stylelint": "^13.6.1", 259 259 "stylelint-config-prettier": "^8.0.2", 260 260 "stylelint-config-standard": "^20.0.0", 261 + "tar-fs": "2.0.1", 261 262 "terser-webpack-plugin": "^3.0.7", 262 263 "testcafe": "^1.8.8", 263 264 "testcafe-browser-provider-electron": "^0.0.15", 264 265 "testcafe-react-selectors": "^4.0.0", 265 266 "typescript": "^3.9.7", 266 267 "typings-for-css-modules-loader": "^1.7.0", 268 + "unbzip2-stream": "1.4.2", 267 269 "url-loader": "^4.1.0", 268 270 "webpack": "^4.43.0", 269 271 "webpack-bundle-analyzer": "^3.8.0",