-
Notifications
You must be signed in to change notification settings - Fork 29
Modules System Documentation (DEPRECATED)
Dynamic Module aims to enhance Side Content flexibility and capabilities, as well as to make each Side Content Tab to be able to load dynamically based on the program ran.
The spawning and despawning of module tab will be triggered whenever a program is evaluated, regardless whether it is from the REPL or the Editor.
Dynamic Module consists of two components:
- React Component for front end, which is the tab that will spawn or despawn on the SideContent. The tab can be used to display the output of the module file e.g. visualisation of wave, rune display, etc.
- Module file, at the modules repo e.g. game.js
A module creator can implement just the module file if there is no need for any visualisation on front-end, or just the React component if there is no need for any additional module file. Of course, implementing both React Component and module file is also possible.
Side Content accepts SideContentTab
(defined here). As such, the module tab must be of SideContentTab
type for it to be integrated into Side Content.
- Create a new
.ts
file, prepend the name file withSideContent
and save it undersrc/commons/sideContent/
. - Create a (functional) React component.
- Create a separate function that specify when to spawn the module OUTSIDE the React component within the same
.ts
file, following the signature(debuggerContext: DebuggerContext) => boolean
. - Wrap the React component into type of
SideContentTab
.
For an example of the .ts
file, refer to this sample module tab.
Write a body of anonymous function in javascript and upload them to the modules repo e.g. game.js
Commonly, we would want to make use of front end data e.g. the code to be run, Source version, etc. to be used by the module file.
The information of the raw code, external libraries used, context (among other things) are packaged into debuggerContext
and stored at the application store, specifically at state.workspaces.<YOUR CURRENT WORKSPACE LOCATION>.debuggerContext
, e.g. state.workspaces.playground.debuggerContext
.
The most up-to-date definition of debuggerContext
is defined here.
export type DebuggerContext = {
result: any;
lastDebuggerResult: any;
code: string;
context: Context;
workspaceLocation?: WorkspaceLocation;
};
By default, all integrated module tab will receive a props.workspaceLocation
as one of its props. By using this, it is possible to access the debuggerContext
. Of course, being a React component, we can also access anything under the store.
Below is an example of how to access the debuggerContext
.
const SideContentSampleTab = (props: any) => {
let workspaces = useSelector((state: OverallState) => state.workspaces);
// Require workspace location
if (!props.workspaceLocation) {
return <></>;
}
const debuggerContext = workspaces[props.workspaceLocation].debuggerContext;
... more code ...
}
Most likely, there is a need to provide the module file with Front End Data, e.g. the code to be run, Source version, etc. The question is, how do we do that?
We can provide the front end data to the module file as a property of the context
used by the js-slang evaluator.
When evaluating a js-slang code, js-slang evaluator require a context
to run the code in. Context
encapsulate all the necessary information to run the code e.g. library used, module file used, existing variables, etc.
By using runInContext
function provided by the js-slang, we can run the js-slang code within our desired context
. The context
creation can be done with createContext
function provided by js-slang as well.
Within the
debuggerContext
, we save the latest code to run. Using the js-slang evaluator, we can evaluate the js-slang code within desired context.
Below is the sample code of evaluating the js-slang code while providing front end data as part of the context:
// At the .ts file, within the body of the React Component
const context = createContext(4, [], {}, 'gpu', {
studentCodeLength: debuggerContext.code.length,
windowToUse: (window as any).MyLibrary,
});
const result = await runInContext(debuggerContext.code, context); // run the js-slang code
The above snippet will run the code (debuggerContext.code
) with the desired context
. The module file will be fed this context and can make use of it i.e. at the module file itself:
// At the module file
(_params) => {
const code_length = _params.studentCodeLength;
const window = _params.windowToUse
... rest of module file ...
}
The module file then can make use of the given parameters e.g. using the given window to display the result.
IMPORTANT: the module file will only be used by the js-slang if student has imported the module file:
import { ... } from 'my_library';
- Open
src/commons/sideContent/SideContentHelper.ts
. - Import your
SideContentTab
, e.g.yourOwnTab: SideContentTab
and include it into the arraypotentialTabs
.
e.g.
potentialTabs = [sampleTab, yourOwnTab]
.
We provide sample code as a template. Below, the module tab is spawned when program output is sample
, and despawned when program output is unsample
. It simply outputs the program output as its content.
At SideContentSampleTab.ts
:
import { IconNames } from '@blueprintjs/icons';
import * as React from 'react';
import { useSelector } from 'react-redux';
import { OverallState } from '../application/ApplicationTypes';
import { DebuggerContext } from '../workspace/WorkspaceTypes';
import { isCurrentlyActive,isEmptyDebuggerContext } from './SideContentHelper';
import { SideContentTab } from './SideContentTypes';
////////////////////////
// React Component //
////////////////////////
const SideContentSampleTab = (props: any) => {
const workspaces = useSelector((state: OverallState) => state.workspaces);
// Require workspace location
if (!props.workspaceLocation) {
return <></>;
}
const debuggerContext = workspaces[props.workspaceLocation].debuggerContext;
const replOutput = isEmptyDebuggerContext(debuggerContext)
? ''
: debuggerContext.result.value;
return (
<div>
<p>
{replOutput}
</p>
</div>
);
};
////////////////////////
// Spawning Behaviour //
////////////////////////
// Outside the react component
const toSpawnSampleTab = (debuggerContext: DebuggerContext): boolean => {
if (debuggerContext.result === undefined || debuggerContext.result.value === 'unsample') {
return false;
}
const isToSpawn = isCurrentlyActive('Sample', debuggerContext.workspaceLocation)
|| debuggerContext.result.value === 'sample';
return isToSpawn;
};
////////////////////////
// Wrap //
////////////////////////
// Wrap it within SideContentTab
export const sampleTab: SideContentTab = {
label: 'Sample',
iconName: IconNames.ASTERISK,
body: <SideContentSampleTab />,
toSpawn: toSpawnSampleTab // indicate the spawning behaviour function
};
export default SideContentSampleTab;
At SideContentHelper.ts
:
import { sampleTab } from './SideContentSampleTab';
////////////////////////
// Integration //
////////////////////////
...
const potentialTabs: SideContentTab[] = [sampleTab];
...
The debuggerContext
is collected from SagaMiddleware, specifically at WorkspaceSaga
during evaluation of any code. After evaluation of any code, we dispatch an action loaded with debuggerContext
. The corresponding reducer, WorkspaceReducer
, saves the debuggerContext
into the store.
Previously, there is almost no information passed by front end to the module tabs. In other words, module tabs and the module back end do not have direct access to the program being run.
---> = data flow
---+ = child component (no data flow)
program ---> sagas ---> front-end ---+ module front end
In the event that the module tab require additional information, they have to invent a way to collect the information and pass it to their corresponding module tab. This introduces many 'inventive' ways throughout the code base for their data collection.
---> = data flow
---+ = child component (no data flow)
_______________________
| | 'inventive' collection of data from sagas
| v
program ---> sagas ---> front-end ---+ module front end
As such, the solution is to introduce a formalised pipeline such that module tabs are able to easily access their required information. The front-end now saves the required data and save it into the store, where module tabs can easily access the required information. This eliminate the need for other 'inventive' ways to be deployed and clutter the code base.
---> = data flow
---+ = child component (no data flow)
------> store
| |
program ---> sagas ---> front-end | fetch required data from store, just like any React Component
| v
---+ module front end
On every program run, the debuggerContext
will be updated automatically. Side Content will be notified as it is subscribed to debuggerContext
and will prompt all of the potential module tabs with the new information. All potential module tabs that wants to spawn will be added into tabs to be rendered.
We pass the workSpaceLocation
as props into the module tab by directly modifying the JSX element (body
attribute in SideContentTab
).
const tabBody: JSX.Element = workspaceLocation
? {
...tab.body,
props: {
...tab.body.props,
workspaceLocation
}
}
: tab.body;
Passing the body as JSX element is a bad practice which makes it difficult to pass in props. Unfortunately, this is the current adopted way of Side Content, and Dynamic Module simply extends the bad practice. This should be changed in the future to not pass a JSX element.
- Home
- Overview
- System Implementation
-
Development Guide
- Getting Started
- Repository Structure
-
Creating a New Module
- Creating a Bundle
- Creating a Tab
- Writing Documentation
- Developer Documentation (TODO)
- Build System
- Source Modules
- FAQs
Try out Source Academy here.
Check out the Source Modules generated API documentation here.