| name | stem-plugin-extraction |
| description | How to safely extract an inline STEM Lab tool from stem_lab_module.js into a standalone plugin file using window.StemLab.registerTool |
STEM Lab Plugin Extraction Skill
When to Use
Use this skill when extracting an inline tool definition from stem_lab_module.js into a standalone stem_tool_*.js plugin file that registers via window.StemLab.registerTool().
[!CAUTION]
Extraction scripts that match on string patterns can accidentally consume adjacent IIFE closures.
This caused a real production outage on 2026-03-24 when a missing })()), collapsed 43,000 lines.
Always run node -c syntax verification after extraction.
Architecture
STEM Lab tools come in two flavors:
- Inline tools: Defined directly inside
stem_lab_module.js as conditional branches of the StemLabModal component
- Plugin tools: Standalone files that register via
window.StemLab.registerTool('toolName', { render: fn }) and are loaded by the plugin fallback renderer
The plugin registry (_pluginFallback) provides a bridge context object (ctx) containing React, hooks, state, and utility functions.
Step-by-Step Extraction
Step 1: Identify the Inline Tool Boundaries
Search for the tool's inline definition in stem_lab_module.js:
stemLabTool === 'codingPlayground' && (() => {
})(),
Note the start line (the condition) and end line (the })(), closure).
Step 2: Check for Hoisted React Hooks
[!IMPORTANT]
This is the #1 cause of post-extraction crashes. React hooks cannot be called conditionally,
so they are often hoisted to the StemLabModal top level — outside the tool's inline body.
Search for hooks that reference your tool name:
python -c "
lines = open('stem_lab/stem_lab_module.js', encoding='utf-8').readlines()
for i, l in enumerate(lines):
if '_codingCanvas' in l or '_yourToolRef' in l:
print(f'{i}: {l.strip()}')"
Common hoisted patterns:
var _codingCanvasRef = React.useRef(null);
React.useEffect(function() { ... codingPlayground ... });
These hooks must be injected into the ctx bridge (see Step 5).
Step 3: Create the Plugin File
Create stem_lab/stem_tool_<name>.js with this structure:
window.StemLab.registerTool('toolName', {
icon: '🔬',
label: 'toolName',
desc: '',
color: 'slate',
category: 'creative',
render: function(ctx) {
var React = ctx.React;
var h = React.createElement;
var labToolData = ctx.toolData;
var setLabToolData = ctx.setToolData;
var setStemLabTool = ctx.setStemLabTool;
var addToast = ctx.addToast;
var ArrowLeft = ctx.icons.ArrowLeft;
var awardStemXP = ctx.awardXP;
var stemCelebrate = ctx.celebrate;
var callGemini = ctx.callGemini;
var _myToolRef = ctx._myToolRef;
return (function() {
})();
}
});
Step 4: Remove the Inline Copy
Delete the inline tool body from stem_lab_module.js. Be extremely careful with closing brackets.
Verification after deletion:
node -c stem_lab/stem_lab_module.js
If this fails with SyntaxError: Unexpected token ')' or Unexpected end of input, you removed too many or too few closing brackets.
Step 5: Bridge Hoisted Hooks
In stem_lab_module.js, find the _ctx object in _pluginFallback and add any hoisted hooks:
var _ctx = {
_myToolRef: typeof _myToolRef !== 'undefined' ? _myToolRef : null,
};
Step 6: Register as Plugin-Only
Add the tool to the _pluginOnlyTools object in _pluginFallback:
var _pluginOnlyTools = {
myToolName: true,
};
Step 7: Add to AlloFlowANTI.txt
Add a <script> tag for the new plugin file in the <!-- STEM Lab Plugins --> section:
<script src="$CDN_BASE/stem_lab/stem_tool_<name>.js"></script>
Step 8: Validate and Deploy
node -c stem_lab/stem_lab_module.js
node -c stem_lab/stem_tool_<name>.js
python -c "
src = open('stem_lab/stem_lab_module.js', encoding='utf-8').read()
print('Open parens:', src.count('('), 'Close:', src.count(')'))
print('Open braces:', src.count('{'), 'Close:', src.count('}'))
print('Open brackets:', src.count('['), 'Close:', src.count(']'))"
Post-Extraction Checklist
Known Pitfalls
- Closing bracket consumption: Python
str.replace() can match the IIFE closure of the next tool if boundaries aren't precise
- Hoisted hooks: React hooks must be at the top level of a component — they can't be nested inside the plugin's render function. Pass them through
ctx.
- Variable shadowing: The inline version may reference variables (
addToast, ArrowLeft) that are in scope in the monolith but not in the plugin. Always alias from ctx.
- Dual definitions: If both an inline and plugin version exist, the inline wins (it renders first). Always delete the inline version after extraction.