Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"@sourceacademy/web-stepper"
"@sourceacademy/web-stepper",
"@sourceacademy/web-data-visualizer"
]
}
14 changes: 14 additions & 0 deletions src/common/data-visualizer/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
preset: "ts-jest/presets/js-with-ts-esm",
testEnvironment: "node",
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
},
],
},
testPathIgnorePatterns: [".*?dist/"],
coverageReporters: ["lcov"],
};
3 changes: 3 additions & 0 deletions src/common/data-visualizer/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "installable"
}
37 changes: 37 additions & 0 deletions src/common/data-visualizer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@sourceacademy/common-data-visualizer",
"version": "0.0.1",
"packageManager": "yarn@4.6.0",
"description": "Shared, language-agnostic protocol for the Source Academy data visualizer plugin pair",
"scripts": {
"build": "rollup -c",
"prepack": "yarn build",
"test": "jest",
"test-coverage": "jest --coverage"
},
"license": "ISC",
"files": [
"dist"
],
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.3.0",
"@types/jest": "^30.0.0",
"jest": "^30.4.2",
"rollup": "^4.60.2",
"ts-jest": "^29.4.11",
"tslib": "^2.8.1",
"typescript": "^6.0.3"
}
}
21 changes: 21 additions & 0 deletions src/common/data-visualizer/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import nodeResolve from "@rollup/plugin-node-resolve";
import terser from "@rollup/plugin-terser";
import typescript from "@rollup/plugin-typescript";

/**
* @type {import('rollup').RollupOptions}
*/
export default {
input: "src/index.ts",
output: [
{
file: "dist/index.cjs",
format: "cjs",
},
{
file: "dist/index.mjs",
format: "esm",
},
],
plugins: [nodeResolve(), typescript(), terser()],
};
11 changes: 11 additions & 0 deletions src/common/data-visualizer/src/__tests__/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { expect, test } from "vitest";

import { DATA_VISUALIZER_CHANNEL_ID, RUNNER_ID, WEB_ID } from "..";

test("runner and web ids are distinct", () => {
expect(RUNNER_ID).not.toBe(WEB_ID);
});

test("has a stable channel id", () => {
expect(DATA_VISUALIZER_CHANNEL_ID).toBe("__data_visualizer");
});
94 changes: 94 additions & 0 deletions src/common/data-visualizer/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Shared, language-agnostic protocol for the Source Academy data visualizer plugin pair.
*
* The data visualizer is split into:
* - a {@link https://github.com/source-academy/conductor | Conductor} **runner** plugin
* (`@sourceacademy/runner-data-visualizer`) that turns a language's own runtime value into a
* {@link SerializedDataVisualizerNode} — a purely mechanical conversion, no graph algorithms, and
* - a **web/host** plugin (`@sourceacademy/web-data-visualizer`) that owns cycle-detection, pair/
* list/tree classification, and rendering.
*
* Classification lives host-side, once, shared across every language — unlike the stepper (where the
* stepping algorithm is necessarily per-language, because ASTs don't share structure across
* languages), pairs/lists/arrays *do* share structure across Source Academy's SICP-descended
* languages, so there is no reason to duplicate cycle-detection/classification per language.
*
* They communicate over a single {@link DATA_VISUALIZER_CHANNEL_ID | channel} using the
* {@link DataVisualizerMessage} protocol. Everything that crosses the channel must be plain,
* structured-clone-able JSON — class instances with methods (and live object identity) cannot survive
* a `MessageChannel`, which is why every compound node carries an explicit {@link RefId} instead.
*/

/** The channel the data visualizer runner and host plugins communicate over. */
export const DATA_VISUALIZER_CHANNEL_ID = "__data_visualizer";

/** The id of the runner (worker-side) data visualizer plugin. */
export const RUNNER_ID = "__runner_data_visualizer";

/** The id of the web/host (browser-side) data visualizer plugin. */
export const WEB_ID = "__web_data_visualizer";

/**
* The id used to look the data visualizer up in the plugin directory (i.e. the argument to
* `IRunnerPlugin.hostLoadPlugin`). The host resolves this to the web plugin's bundle URL.
*/
export const DATA_VISUALIZER_DIRECTORY_ID = "data-visualizer";

/**
* Identifies one distinct compound value (a pair/array or a function) within a single top-level
* argument of one `draw_data(...)` call (one element of a {@link SerializedDataVisualizerRow}, not
* the whole row). Assigned by the runner's `RefIdAllocator` on first encounter of a given runtime
* value (by reference, language-side); only unique **within the argument it appears in** — a
* `RefIdAllocator` is created fresh for *each argument*, not once per row, so a different argument
* (even in the same row) may reuse the same numeric id for a completely unrelated value. Never
* compare or reuse a `refId` across two different elements of a row, or across two different rows.
*
* This is what lets the host detect shared structure and cycles within one row without ever seeing a
* live reference: a runtime value already seen earlier in the same `sendDrawing()` call is re-emitted
* as a {@link SerializedDataVisualizerNode} `"ref"` node instead of being walked again.
*/
export type RefId = number;

/**
* One node of a drawable structure, serialized to plain JSON. Each language's runner-side adapter
* converts its own runtime values into this generic, tagged shape; the host's classifier and renderer
* never see a language-specific value.
*
* A pair and a native (arbitrary-length) list are both the `"array"` variant — Source Academy's
* SICP-descended languages represent a pair as a 2-element list, not a distinct type, so the host
* distinguishes "pair" from "list" for classification purposes by `children.length`, not by tag.
*/
export type SerializedDataVisualizerNode =
| { type: "array"; refId: RefId; children: SerializedDataVisualizerNode[] }
| { type: "empty" }
| { type: "leaf"; displayValue: string; label: string }
| { type: "function"; refId: RefId; displayValue: string }
| { type: "ref"; refId: RefId };

/** One row = the fully-serialized arguments of a single `draw_data(...)` call. */
export type SerializedDataVisualizerRow = SerializedDataVisualizerNode[];

/* -------------------------------------------------------------------------- */
/* Channel protocol */
/* -------------------------------------------------------------------------- */

/**
* Runner → host: the full current set of rows for this run. Replaces, not appends — mirrors the
* stepper's `StepperStepsMessage` replace-not-delta contract, so a host that missed earlier messages
* (e.g. because the tab was closed) is always brought fully up to date by the next one.
*/
export interface DataVisualizerRowsMessage {
type: "rows";
rows: SerializedDataVisualizerRow[];
}

/**
* Host → runner: asks the runner to (re)send the rows it last computed. Used to repopulate the
* display when the data visualizer tab is (re)opened without re-running the program.
*/
export interface DataVisualizerRequestMessage {
type: "request";
}

/** Every message that may cross the {@link DATA_VISUALIZER_CHANNEL_ID} channel. */
export type DataVisualizerMessage = DataVisualizerRowsMessage | DataVisualizerRequestMessage;
11 changes: 11 additions & 0 deletions src/common/data-visualizer/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"exclude": ["./dist"],
"include": ["./src"],
"compilerOptions": {
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["jest"]
}
}
13 changes: 13 additions & 0 deletions src/common/tabs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,23 @@ export interface ITabService {

/**
* The showTab method is used to display a tab that has been registered with the tab service. The tab is identified by its id, which is passed as an argument to the method. When the showTab method is called, the tab service will render the corresponding tab component with the arguments that were passed in when the tab was registered. This allows for dynamic rendering of tabs based on the arguments passed in.
*
* This also focuses the tab (makes it the currently-selected one) — appropriate when a plugin
* decides on its own, mid-session, that the student should be looking at it right now (e.g. a
* sound module the moment it starts playing/recording). Use {@link revealTab} instead when a tab
* should simply become available in the tab bar without stealing focus from whatever the student
* is currently looking at (e.g. when the plugin first loads).
* @param id The id of the tab to be displayed. This should correspond to the id of a tab that has been registered with the tab service.
*/
showTab(id: string): void;

/**
* Makes a registered tab visible in the tab bar without changing which tab is currently selected —
* the visible-but-unfocused counterpart to {@link showTab}.
* @param id The id of the tab to reveal. This should correspond to the id of a tab that has been registered with the tab service.
*/
revealTab(id: string): void;

/**
* The hideTab method is used to hide a tab that is currently being displayed. The tab is identified by its id, which is passed as an argument to the method. When the hideTab method is called, the tab service will stop rendering the corresponding tab component, effectively hiding it from view. This allows for dynamic hiding of tabs based on user interactions or other events.
*/
Expand Down
14 changes: 14 additions & 0 deletions src/runner/data-visualizer/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
preset: "ts-jest/presets/js-with-ts-esm",
testEnvironment: "node",
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
},
],
},
testPathIgnorePatterns: [".*?dist/"],
coverageReporters: ["lcov"],
};
3 changes: 3 additions & 0 deletions src/runner/data-visualizer/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "installable"
}
42 changes: 42 additions & 0 deletions src/runner/data-visualizer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@sourceacademy/runner-data-visualizer",
"version": "0.0.1",
"packageManager": "yarn@4.6.0",
"description": "Language-agnostic runner plugin for the Source Academy data visualizer",
"scripts": {
"build": "rollup -c",
"prepack": "yarn build",
"test": "jest",
"test-coverage": "jest --coverage"
},
"license": "ISC",
"files": [
"dist"
],
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"peerDependencies": {
"@sourceacademy/conductor": ">=0.3.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.3.0",
"@sourceacademy/common-data-visualizer": "workspace:*",
"@sourceacademy/conductor": ">=0.3.0",
"@types/jest": "^30.0.0",
"jest": "^30.4.2",
"rollup": "^4.60.2",
"ts-jest": "^29.4.11",
"tslib": "^2.8.1",
"typescript": "^6.0.3"
}
}
21 changes: 21 additions & 0 deletions src/runner/data-visualizer/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import nodeResolve from "@rollup/plugin-node-resolve";
import terser from "@rollup/plugin-terser";
import typescript from "@rollup/plugin-typescript";

/**
* @type {import('rollup').RollupOptions}
*/
export default {
input: "src/index.ts",
output: [
{
file: "dist/index.cjs",
format: "cjs",
},
{
file: "dist/index.mjs",
format: "esm",
},
],
plugins: [nodeResolve(), typescript(), terser()],
};
Loading
Loading