diff --git a/app/RSpade/Core/JsParsers/resource/js-transformer-server.js b/app/RSpade/Core/JsParsers/resource/js-transformer-server.js index 0b92c5bb3..6a6eee172 100755 --- a/app/RSpade/Core/JsParsers/resource/js-transformer-server.js +++ b/app/RSpade/Core/JsParsers/resource/js-transformer-server.js @@ -135,8 +135,35 @@ const targetPresets = { /** * Create custom plugin to prefix generated WeakMap variables and Babel helper functions * This plugin runs AFTER all other transformations to catch Babel-generated helpers + * + * DECORATOR + STATIC PROPERTIES WORKAROUND + * ========================================= + * This plugin also fixes a long-standing Babel bug where decorated classes with static + * properties don't export their class name to global scope. When Babel transforms: + * + * @decorator + * class Foo { static BAR = 1; } + * + * It wraps the class in an IIFE that returns the decorated class via `_applyDecs().c`, + * but never assigns it back to the original class name. This causes "Foo is not defined" + * errors at runtime. + * + * This is a known issue dating back to 2018 across multiple transpilers: + * - Babel: https://github.com/babel/babel/issues/12689 (decorators + class fields) + * - esbuild: https://github.com/evanw/esbuild/issues/3823 (same IIFE pattern issue) + * - SWC: https://github.com/nicolo-ribaudo/swc/issues/1 (200+ decorator test failures) + * + * The TC39 decorator proposal (2023-11) complexity makes this hard to fix properly. + * Our workaround: detect the `[_Foo, _initClass] = _applyDecs(...).c` pattern and + * add `var Foo = _hash_Foo;` at the end to export the class to global scope. + * + * Note: Babel truncates long variable names (e.g., `_Very_Long_Class_Name` becomes + * `_Very_Long_Cla`), so we match by finding variables in the `.c` destructuring pattern + * rather than by exact name comparison. */ function createPrefixPlugin(fileHash) { + const t = require('@babel/types'); + return function() { return { name: 'prefix-generated-variables', @@ -146,6 +173,10 @@ function createPrefixPlugin(fileHash) { // Track all top-level variables and functions that start with underscore const generatedNames = new Set(); + // Track class names found in ClassExpression nodes + const classNames = new Set(); + // Map: class binding variable -> class name + const classBindingVars = new Map(); // First pass: collect all generated variable and function names at top level for (const statement of program.node.body) { @@ -164,6 +195,48 @@ function createPrefixPlugin(fileHash) { } } + // Find all ClassExpression nodes and collect their names + program.traverse({ + ClassExpression(path) { + if (path.node.id && path.node.id.name) { + classNames.add(path.node.id.name); + } + } + }); + + // WORKAROUND: Find _applyDecs(...).c destructuring to identify class binding variables + // This is the hack for the decorator + static properties bug described above. + // Pattern: [_ClassName, _initClass] = _applyDecs(...).c + program.traverse({ + AssignmentExpression(path) { + const node = path.node; + // Check for array destructuring on left: [_X, _Y] = ... + if (node.left.type !== 'ArrayPattern') return; + // Check for .c member access on right: ...applyDecs(...).c + if (node.right.type !== 'MemberExpression') return; + if (node.right.property.name !== 'c' && node.right.property.value !== 'c') return; + // Check if it's a call to something with 'applyDecs' in name + const callee = node.right.object; + if (callee.type !== 'CallExpression') return; + const calleeName = callee.callee?.name || ''; + if (!calleeName.includes('applyDecs')) return; + + // First element of array pattern is the class binding variable + const firstElement = node.left.elements[0]; + if (!firstElement || firstElement.type !== 'Identifier') return; + const bindingVarName = firstElement.name; + + // Match to class name - the binding var is _ClassName or truncated + const varCore = bindingVarName.replace(/^_/, '').replace(/\d+$/, ''); + for (const className of classNames) { + if (className.startsWith(varCore)) { + classBindingVars.set(bindingVarName, className); + break; + } + } + } + }); + // Second pass: rename all references if (generatedNames.size > 0) { program.traverse({ @@ -178,6 +251,33 @@ function createPrefixPlugin(fileHash) { } }); } + + // WORKAROUND CONTINUED: Add global exports for class bindings + // This fixes the decorator + static properties bug by exporting the class name + // See comment block at createPrefixPlugin() for full explanation and issue links + if (classBindingVars.size > 0) { + const exports = []; + const exportedClasses = new Set(); + + for (const [internalName, className] of classBindingVars) { + // Skip if we already exported this class (can happen with multiple matching vars) + if (exportedClasses.has(className)) continue; + exportedClasses.add(className); + + const prefixedName = `_${fileHash}${internalName}`; + // Add: var ClassName = _hash_ClassName; + exports.push( + t.variableDeclaration('var', [ + t.variableDeclarator( + t.identifier(className), + t.identifier(prefixedName) + ) + ]) + ); + } + // Append exports to end of program + program.node.body.push(...exports); + } } }; };