-
Notifications
You must be signed in to change notification settings - Fork 29
Creating a Bundle
This page contains a guide for Source Module authors to create their very own Source Module Bundle as well as details and explanations on the structure of a Source Module Bundle.
Similar to regular Javascript modules, Source allows developers to export functions and constants to users for importing into their programs.
For example, the binary_tree
module may want to provide an abstraction for Source programs to interact with the Binary Tree data structure. Thus, the binary_tree
module would expose functions such as make_tree
, left_branch
and right_branch
to be used in Source programs.
The typical bundle structure for a bundle looks like this:
.
└── src/
├── binary_tree/
│ └── index.ts // Entry Point
└── curve/
├── index.ts
├── functions.ts
└── ... // regular Javascript module code
Only functions that are exported by the module will be made available to users. Let us look at an example from the curve module.
// curve/functions.ts
/**
* Makes a Point with given x and y coordinates.
*
* @param x x-coordinate of new point
* @param y y-coordinate of new point
* @returns with x and y as coordinates
* @example
* ```
* const point = make_point(0.5, 0.5);
* ```
*/
export function make_point(x: number, y: number): Point {
return new Point(x, y, 0, [0, 0, 0, 1]);
}
/**
* Use this function to create the various `draw_connected` functions
*/
export function createDrawFunction(
scaleMode: ScaleMode,
drawMode: DrawMode,
space: CurveSpace,
isFullView: boolean,
): (numPoints: number) => RenderFunction {
// implementation hidden...
}
Note that curve/functions.ts
exports both createDrawFunction
and make_point
.
// curve/index.ts
export { make_point } from './functions';
Only make_point
is exported at the bundle's entry point however, so users will not be able to see createDrawFunction
, identical to how ES modules behave.
// curve/index.ts
export { make_point } from './functions'
and
// curve/index.ts
import { make_point } from './functions'
export default {
make_point,
}
are functionally identical: both expose make_point
to the user. However, combining both regular exports and a default export will cause the default export to be hidden.
import { make_point } from './functions'
export default {
make_point,
}
export { r_of, g_of, b_of } from './functions';
// Source Program
import { b_of, make_point } from 'curve'; // will result in b_of being defined but make_point will be undefined
Currently Source does not support namespace or default imports, so the make_point
function will not be able to imported into Source programs.
Some times, a bundle needs to be able to maintain some state information, or send information to a tab. Module Contexts form the solution to this problem.
Every time js-slang
evaluates Source code, it creates an evaluation context. Bundles can access this context by using this import:
// curve/functions.ts
import { context } from 'js-slang/moduleHelpers';
const drawnCurves = [];
context.moduleHelpers.curve.state = {
drawnCurves,
}
context.moduleHelpers
will not be null here, and is of the type Record<string, { tabs: any[], state: any }>
. To access a module's context, simply index the moduleHelpers
object using the bundle's name.
The state
object can be of any type - it is up to the developer to decide what needs to be stored as state.
This state
object can then be accessed by the module's tab, for example:
// Curve/index.tsx
export default {
toSpawn: (context) => {
return context.context.moduleHelpers.curve.state.drawnCurves.length > 0;
},
body: (context) => { /* implementation */ },
}
For more information refer to the documentation for tabs.
js-slang
guarantees that each module is only evaluated once per code evaluation, no matter how many import statements there are in a Source program.
Consider the following situation:
// curve/functions_0.ts
import { context } from 'js-slang/moduleHelpers';
const drawnCurves = [];
context.moduleHelpers.curve.state = {
drawnCurves,
}
export const draw_connected = (...) => {...}
// curve/functions_1.ts
import { context } from 'js-slang/moduleHelpers';
const drawnCurves = [];
context.moduleHelpers.curve.state = {
drawnCurves,
}
export const draw_3d_connected = (...) => {...}
// curve/functions_2.ts
import { draw_connected } from './functions_0.ts';
import { context } from 'js-slang/moduleHelpers';
const drawnCurves = [];
context.moduleHelpers.curve.state = {
drawnCurves,
}
export const someOtherFunc = (...) => { ... }
// curve/index.ts
export { draw_connected } from './functions_0.ts';
export { draw_3d_connected } from './functions_1.ts';
export { someOtherFunc } from './functions_2.ts';
Which state setting code would be evaluated first?
Code that does not imports code from other files will be evaluated first, in this case functions_2.ts
(because functions_1.ts
relies on it). However, it is not clear which of functions_0.ts
or functions_1.ts
will be evaluated first. Thus, importing the context multiple times will cause both writes and reads to that object to exhibit undefined behaviour.
To remedy this, either only import the context once in your bundle (recommended), and then have it exported for the rest of the bundle's code to use, or add checks:
import { context } from 'js-slang/moduleHelpers';
let drawnCurves = [];
if (context.moduleHelpers.curve.state) {
drawnCurves = context.moduleHelpers.curve.state.drawnCurves;
} else {
context.moduleHelpers.curve.state = {
drawnCurves,
}
}
Importing the context only in index.ts
(which is guaranteed to be evaluated last - it needs the rest of the code from your bundle) could also work, but will probably result in circular dependency warnings.
It is possible for one bundle to access the context of another:
import { context } from 'js-slang/moduleHelpers';
// If the rune module was also loaded, this object *may* not be null
if (context.moduleHelpers.rune) {
console.log('Both the rune and curve modules were loaded!')
} else {
console.log('Only the curve module was loaded')
}
However, the order in which modules are evaluated is not guaranteed. In the above code, if the curve
module was evaluated first, it would indicate that only the curve
module was loaded since rune
's state object has yet to be initialized. Thus, use this feature with caution.
Normally data flows from the bundle to the context object: i.e. the bundle contains the code that initializes the module's state object. However, it is also possible for the module's state object to be initialized before the bundle is loaded.
For example, the game
module's room preview feature utilizes a special evaluation context:
// within createContext()
// Create an evaluation context
this.context = createContext(Chapter.SOURCE_4, [], 'playground', Variant.DEFAULT);
// Initialize the context for the game module
this.context.moduleContexts.game = {
tabs: null,
state: {
scene: this,
preloadImageMap: this.preloadImageMap,
preloadSoundMap: this.preloadSoundMap,
preloadSpritesheetMap: this.preloadSpritesheetMap,
remotePath: (file: string) => toS3Path(file, true),
screenSize: screenSize,
createAward: (x: number, y: number, key: ItemId) => this.createAward(x, y, key)
}
};
// Pass the context to the runInContext function from js-slang
The game
bundle is then able to use the data provided to it:
// game/functions.ts
import { context } from 'js-slang/moduleHelpers';
export default function gameFuncs() {
const {
scene,
preloadImageMap,
preloadSoundMap,
preloadSpritesheetMap,
remotePath,
screenSize,
createAward,
} = context.moduleHelpers.game.state || {
// ...defaultValues
};
// Here we know for sure that the game module's state object has been initialized
// but the check is still here just in case the module was not used in its intended way
// do other things...
}
- 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.