Skip to content

Add Module Loader#47

Open
AaravMalani wants to merge 7 commits into
mainfrom
feat/add-module-loader
Open

Add Module Loader#47
AaravMalani wants to merge 7 commits into
mainfrom
feat/add-module-loader

Conversation

@AaravMalani

@AaravMalani AaravMalani commented Jul 1, 2026

Copy link
Copy Markdown
Member

This pull request introduces a new pluginset to load Source Academy modules via Conductor plugins. It adds three new packages, @sourceacademy/common-module-loader, @sourceacademy/runner-module-loader, and @sourceacademy/web-module-loader. It also reverts the repository to Yarn PnP

@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8d809c2

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 introduces a new Module Loader protocol and its implementation across three packages: a common package for shared types, a runner-side plugin to request and register modules, and a web-side plugin to fetch the module directory and serve module bundles. The feedback highlights critical improvements needed for protocol robustness and safety, including adding moduleName to response messages to prevent race conditions, handling potential promise rejections in asynchronous operations (such as dynamic imports and fetch requests), and securing the module name validation regex with anchors to prevent path traversal or input validation bypasses.

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 src/common/module-loader/src/index.ts
Comment thread src/runner/module-loader/src/index.ts
Comment thread src/web/module-loader/src/index.ts
Comment thread src/web/module-loader/src/index.ts Outdated
@Akshay-2007-1 Akshay-2007-1 self-requested a review July 3, 2026 12:58
error: "Module directory not loaded yet",
});
}
if (!(message.moduleName in this.moduleDirectory)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Module-existence check uses the in operator (!(message.moduleName in this.moduleDirectory)) against a plain JSON-parsed object, which walks the prototype chain. Requesting moduleName: "toString" (or "constructor") passes both this check and the ^[a-zA-Z0-9_-]+$ regex at line 50, then this.moduleDirectory["toString"].tabs (line 65) resolves to Object.prototype.toString, whose .tabs is undefined. A MODULE_RESPONSE goes out with tabs: undefined, violating the declared tabs: string[] contract — and the runner's loadTab (src/runner/module-loader/src/index.ts:48, msg.tabs.includes(tabName)) throws TypeError: Cannot read properties of undefined (reading 'includes') the first time a tab is loaded. Independently flagged by three separate finder passes.

Comment thread .yarnrc.yml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Will this disrupt the working of other plugins and packages in this repo? Its a high-blast-radius tooling change riding along on a feature PR.

if (newURL === this.moduleDirectoryURL && this.moduleDirectory) {
return;
}
this.moduleDirectoryURL = newURL;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

onModuleDirectoryURLChange sets this.moduleDirectoryURL = newURL (line 74) synchronously, before the fetch resolves. Two consequences: (a) two rapid calls with the same URL both pass the dedup guard (line 71, this.moduleDirectory is still null on the second call) and both fire redundant fetches; (b) if the fetch fails, the URL is already recorded as "current," so a later retry with that same URL short-circuits at line 71 and never actually re-fetches — the directory is permanently stuck stale/empty for that URL.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Could you give an example of onModuleDirectoryURLChange being called two times in a short span? That seems very improbable. For point b), I'll make the change

Comment on lines +63 to +67
};
this.__moduleRequestChannel.subscribe(handleResponse);
this.__moduleRequestChannel.send({
type: ModuleLoaderMessageType.REQUEST_MODULE,
moduleName,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two concurrent requestModule("chart") calls each subscribe their own handleResponse (line 64). If the underlying IChannel broadcasts one response to all subscribers, both listeners pass the moduleName filter (line 39) and both proceed to registerPlugin/initialise — double-registering the same module. (Plausible, contingent on the real IChannel supporting multiple simultaneous subscribers — worth confirming against the conductor implementation.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

requestModule should never be called twice with the same value, it's the equivalent of calling registerPlugin twice. I guess I could add a check to ensure this doesn't happen

}
const moduleBaseUrl = this.moduleDirectoryURL.slice(
0,
this.moduleDirectoryURL.lastIndexOf("/") + 1,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

moduleBaseUrl derivation assumes moduleDirectoryURL contains a /. A directory URL with none (e.g. a bare "modules.json") makes lastIndexOf("/") return -1, so moduleBaseUrl becomes ""

{
"name": "@sourceacademy/runner-module-loader",
"version": "0.0.1",
"packageManager": "yarn@4.12.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The two sibling packages added in this same PR pin yarn@4.6.0.

Any reason for the discreptancy?

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