Skip to content

feat(stepper): add common-stepper and runner-stepper plugin packages#20

Open
Shrey5132 wants to merge 17 commits into
source-academy:mainfrom
Shrey5132:feat/conductor-stepper
Open

feat(stepper): add common-stepper and runner-stepper plugin packages#20
Shrey5132 wants to merge 17 commits into
source-academy:mainfrom
Shrey5132:feat/conductor-stepper

Conversation

@Shrey5132

Copy link
Copy Markdown

This pull request introduces the common-stepper, runner-stepper, and web-stepper packages to plugins. It forms the core plugin infrastructure needed for the new Conductor-based Stepper framework.

@changeset-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7e1bf5f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

💥 An error occurred when fetching the changed packages and changesets in this PR
Some errors occurred when validating the changesets config:
The package or glob expression "@sourceacademy/web-stepper" is specified in the `ignore` option but it is not found in the project. You may have misspelled the package name or provided an invalid glob expression. Note that glob expressions must be defined according to https://www.npmjs.com/package/micromatch

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request establishes a monorepo workspace structure for Source Academy plugins, introducing a language-agnostic Stepper plugin system split into a shared protocol, a runner base class, and a React-based web visualizer. The review feedback focuses on enhancing robustness and error handling, specifically recommending that the spawnPromise helper in the build script handle the child process 'error' event, and that the custom AST renderer in SubstVisualizer.tsx include defensive null/undefined guards for nodes and argument lists to prevent runtime crashes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread lib/build.ts
Comment on lines +61 to +71
export async function spawnPromise(...args: Parameters<typeof spawn>) {
return new Promise<void>((resolve, reject) => {
const child = spawn(...args);
child.on("close", code => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Process exited with code ${code}`));
}
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The spawnPromise helper does not listen to the 'error' event on the spawned child process. If the executable fails to start (e.g., due to a missing command or system-level issue), Node.js will emit an unhandled 'error' event, causing the entire process to crash immediately instead of rejecting the promise. Adding an 'error' event listener that rejects the promise ensures robust error handling.

export async function spawnPromise(...args: Parameters<typeof spawn>) {
  return new Promise<void>((resolve, reject) => {
    const child = spawn(...args);
    child.on("error", reject);
    child.on("close", code => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`Process exited with code ${code}`));
      }
    });
  });
}

Comment on lines +265 to +266
function renderNode(currentNode: StepperNode, renderContext: RenderContext): React.ReactNode {
const styleWrapper = renderContext.styleWrapper;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To make the recursive AST renderer highly robust and defensive against malformed, incomplete, or language-specific AST nodes (e.g., from custom parsers), add a guard at the top of renderNode to safely handle null or undefined nodes. This prevents runtime crashes when rendering optional or missing child nodes.

function renderNode(currentNode: StepperNode | null | undefined, renderContext: RenderContext): React.ReactNode {
  if (!currentNode) {
    return null;
  }
  const styleWrapper = renderContext.styleWrapper;

Comment on lines +574 to +580
const renderFunctionArguments = (
nodes: StepperNode[],
renderNodeFn: typeof renderNode,
styleWrapper: StyleWrapper | undefined,
popoverDepth: number,
) => {
const args: React.ReactNode[] = nodes.map(arg =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If nodes (representing params or arguments) is undefined or null due to an unexpected AST structure, calling nodes.map will throw a TypeError and crash the entire React render tree. Adding a defensive check at the beginning of renderFunctionArguments ensures the component fails gracefully.

  const renderFunctionArguments = (
    nodes: StepperNode[] | undefined,
    renderNodeFn: typeof renderNode,
    styleWrapper: StyleWrapper | undefined,
    popoverDepth: number,
  ) => {
    if (!nodes) {
      return null;
    }
    const args: React.ReactNode[] = nodes.map(arg =>

Comment on lines +608 to +609
const renderArguments = (nodes: StepperNode[]) => {
const args: React.ReactNode[] = nodes.map(arg =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to renderFunctionArguments, renderArguments should defensively guard against nodes being undefined or null to prevent runtime crashes if a CallExpression or FunctionDeclaration has missing arguments.

Suggested change
const renderArguments = (nodes: StepperNode[]) => {
const args: React.ReactNode[] = nodes.map(arg =>
const renderArguments = (nodes: StepperNode[] | undefined) => {
if (!nodes) {
return "()";
}
const args: React.ReactNode[] = nodes.map(arg =>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants