From c02b8b1da7ec9f486eac276d085576518c1eb285 Mon Sep 17 00:00:00 2001 From: rax-it Date: Tue, 2 Sep 2025 16:51:33 -0700 Subject: [PATCH 1/5] proposal: trojan horse private methods --- .../@lwc/babel-plugin-component/src/constants.ts | 2 ++ .../@lwc/babel-plugin-component/src/index.ts | 16 ++++++++++++++++ playground/src/modules/x/counter/counter.js | 3 +++ 3 files changed, 21 insertions(+) diff --git a/packages/@lwc/babel-plugin-component/src/constants.ts b/packages/@lwc/babel-plugin-component/src/constants.ts index 0f4e07d703..0b8517e44e 100644 --- a/packages/@lwc/babel-plugin-component/src/constants.ts +++ b/packages/@lwc/babel-plugin-component/src/constants.ts @@ -34,6 +34,7 @@ const TEMPLATE_KEY = 'tmpl'; const COMPONENT_NAME_KEY = 'sel'; const API_VERSION_KEY = 'apiVersion'; const COMPONENT_CLASS_ID = '__lwc_component_class_internal'; +const PRIVATE_METHOD_PREFIX = '__lwc_component_class_internal_private_'; export { DECORATOR_TYPES, @@ -46,4 +47,5 @@ export { COMPONENT_NAME_KEY, API_VERSION_KEY, COMPONENT_CLASS_ID, + PRIVATE_METHOD_PREFIX, }; diff --git a/packages/@lwc/babel-plugin-component/src/index.ts b/packages/@lwc/babel-plugin-component/src/index.ts index eea68b0bc1..3772963a37 100644 --- a/packages/@lwc/babel-plugin-component/src/index.ts +++ b/packages/@lwc/babel-plugin-component/src/index.ts @@ -15,6 +15,8 @@ import dedupeImports from './dedupe-imports'; import dynamicImports from './dynamic-imports'; import scopeCssImports from './scope-css-imports'; import compilerVersionNumber from './compiler-version-number'; +import privateMethodTransform from './private-method-transform'; +import reversePrivateMethodTransform from './reverse-private-method-transform'; import { getEngineImportSpecifiers } from './utils'; import type { BabelAPI, LwcBabelPluginPass } from './types'; import type { PluginObj } from '@babel/core'; @@ -33,6 +35,8 @@ export default function LwcClassTransform(api: BabelAPI): PluginObj Date: Tue, 2 Sep 2025 16:54:57 -0700 Subject: [PATCH 2/5] chore: add missing files --- .../src/private-method-transform.ts | 55 +++++++++++++++++++ .../src/reverse-private-method-transform.ts | 47 ++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 packages/@lwc/babel-plugin-component/src/private-method-transform.ts create mode 100644 packages/@lwc/babel-plugin-component/src/reverse-private-method-transform.ts diff --git a/packages/@lwc/babel-plugin-component/src/private-method-transform.ts b/packages/@lwc/babel-plugin-component/src/private-method-transform.ts new file mode 100644 index 0000000000..c85acae61f --- /dev/null +++ b/packages/@lwc/babel-plugin-component/src/private-method-transform.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import { PRIVATE_METHOD_PREFIX } from './constants'; +import type { BabelAPI, LwcBabelPluginPass } from './types'; +import type { NodePath, Visitor } from '@babel/core'; +import type { types } from '@babel/core'; + +/** + * Transforms private method identifiers from #privateMethod to __internal_only_privateMethod + * This function returns a Program visitor that transforms private methods before other plugins process them + */ +export default function privateMethodTransform({ + types: t, +}: BabelAPI): Visitor { + return { + // We need to run this plugin at "Program" level and not just at "ClassPrivateMethod" + // This is done to prevent *any* other plugins which has visitors on ClassPrivateMethod from seeing the original Node + Program: { + enter(path: NodePath) { + // Transform private methods BEFORE any other plugin processes them + path.traverse({ + ClassPrivateMethod(path: NodePath) { + const key = path.get('key'); + + if (key.isPrivateName()) { + const privateName = key.node.id.name; + const transformedName = `${PRIVATE_METHOD_PREFIX}${privateName}`; + + // Create a new ClassMethod node to replace the ClassPrivateMethod + const classMethod = t.classMethod( + 'method', // kind: 'method' | 'get' | 'set' + t.identifier(transformedName), // key + path.node.params, + path.node.body, + path.node.computed, + path.node.static, + path.node.generator, + path.node.async + ); + + // Replace the entire ClassPrivateMethod with the new ClassMethod + // this is important since we can't just replace PrivateName with an Identifier + // Hence, we need to replace the entire ClassPrivateMethod Node + path.replaceWith(classMethod); + } + }, + }); + }, + }, + }; +} diff --git a/packages/@lwc/babel-plugin-component/src/reverse-private-method-transform.ts b/packages/@lwc/babel-plugin-component/src/reverse-private-method-transform.ts new file mode 100644 index 0000000000..4e1135218c --- /dev/null +++ b/packages/@lwc/babel-plugin-component/src/reverse-private-method-transform.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +import { PRIVATE_METHOD_PREFIX } from './constants'; +import type { BabelAPI, LwcBabelPluginPass } from './types'; +import type { NodePath, Visitor } from '@babel/core'; +import type { types } from '@babel/core'; + +/** + * Reverses the private method transformation by converting methods with prefix {@link PRIVATE_METHOD_PREFIX} + * back to ClassPrivateMethod nodes. This runs after babelClassPropertiesPlugin to restore private methods. + * @see {@link ./private-method-transform.ts} for original transformation + */ +export default function reversePrivateMethodTransform({ + types: t, +}: BabelAPI): Visitor { + return { + ClassMethod(path: NodePath) { + const key = path.get('key'); + + // Check if the key is an identifier with our special prefix + if (key.isIdentifier()) { + const methodName = key.node.name; + + // Check if this method has our special prefix + if (methodName.startsWith(PRIVATE_METHOD_PREFIX)) { + // Extract the original private method name + const originalPrivateName = methodName.replace(PRIVATE_METHOD_PREFIX, ''); + + // Create a new ClassPrivateMethod node to replace the ClassMethod + const classPrivateMethod = t.classPrivateMethod( + 'method', // kind: 'method' | 'get' | 'set' + t.privateName(t.identifier(originalPrivateName)), // key + path.node.params, // params + path.node.body // body + ); + + // Replace the entire ClassMethod with the new ClassPrivateMethod + path.replaceWith(classPrivateMethod); + } + } + }, + }; +} From 167b857f09b8323d540b436560181c8e3824f137 Mon Sep 17 00:00:00 2001 From: rax-it Date: Thu, 4 Sep 2025 16:42:13 -0700 Subject: [PATCH 3/5] chore: add comments and async support --- .../src/private-method-transform.ts | 50 +++++++++++++++++-- .../src/reverse-private-method-transform.ts | 15 ++++-- playground/src/modules/x/counter/counter.js | 7 ++- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/@lwc/babel-plugin-component/src/private-method-transform.ts b/packages/@lwc/babel-plugin-component/src/private-method-transform.ts index c85acae61f..84d874f0d8 100644 --- a/packages/@lwc/babel-plugin-component/src/private-method-transform.ts +++ b/packages/@lwc/babel-plugin-component/src/private-method-transform.ts @@ -10,23 +10,65 @@ import type { NodePath, Visitor } from '@babel/core'; import type { types } from '@babel/core'; /** - * Transforms private method identifiers from #privateMethod to __internal_only_privateMethod + * Transforms private method identifiers from #privateMethod to __lwc_component_class_internal_private_privateMethod * This function returns a Program visitor that transforms private methods before other plugins process them + * + * + * CURRENTLY SUPPORTED: + * - Basic private methods: #methodName() + * - Static private methods: static #methodName() + * - Async private methods: async #methodName() + * - Method parameters: #method(param1, param2) + * - Method body: #method() { return this.value; } + * - Computed properties: #['method']() + * - Static methods: static #method() + * + * EDGE CASES & MISSING SUPPORT: + * + * 1. Private Methods with Decorators ❌ + * @api #method() { } // Should error gracefully + * @track #method() { } // Should error gracefully + * @wire #method() { } // This should be error as well ? + * + * 2. Private Methods in Nested Classes ❌ + * class Outer { #outerMethod() { } class Inner { #innerMethod() { } } } + * - probably supported, need to be tested + * + * 3. Private Methods with Rest/Spread ⚠️ + * #method(...args) { } // Rest parameters + * #method(a, ...rest) { } // Mixed parameters + * - needs to be tested + * + * 4. Private Methods with Default Parameters ⚠️ + * #method(param = 'default') { } + * #method(param = this.value) { } // `this` reference + * - needs to be tested + * + * 5. Private Methods with Destructuring ⚠️ + * #method({ a, b }) { } // Object destructuring + * #method([first, ...rest]) { } // Array destructuring + * - needs to be tested + * + * 6. Private Methods in Arrow Functions ❌ + * class MyClass { #method = () => { }; } + * - needs to be tested + * */ export default function privateMethodTransform({ types: t, }: BabelAPI): Visitor { return { - // We need to run this plugin at "Program" level and not just at "ClassPrivateMethod" - // This is done to prevent *any* other plugins which has visitors on ClassPrivateMethod from seeing the original Node Program: { enter(path: NodePath) { // Transform private methods BEFORE any other plugin processes them path.traverse({ + // We also need to ensure that there exists no decorator that exposes this method publicly + // ex: @api, @track etc. ClassPrivateMethod(path: NodePath) { const key = path.get('key'); - if (key.isPrivateName()) { + // kind: 'method' | 'get' | 'set' - only 'method' is in scope. + if (key.isPrivateName() && path.node.kind === 'method') { const privateName = key.node.id.name; const transformedName = `${PRIVATE_METHOD_PREFIX}${privateName}`; diff --git a/packages/@lwc/babel-plugin-component/src/reverse-private-method-transform.ts b/packages/@lwc/babel-plugin-component/src/reverse-private-method-transform.ts index 4e1135218c..368044b252 100644 --- a/packages/@lwc/babel-plugin-component/src/reverse-private-method-transform.ts +++ b/packages/@lwc/babel-plugin-component/src/reverse-private-method-transform.ts @@ -22,7 +22,8 @@ export default function reversePrivateMethodTransform({ const key = path.get('key'); // Check if the key is an identifier with our special prefix - if (key.isIdentifier()) { + // kind: 'method' | 'get' | 'set' - only 'method' is in scope. + if (key.isIdentifier() && path.node.kind === 'method') { const methodName = key.node.name; // Check if this method has our special prefix @@ -32,11 +33,17 @@ export default function reversePrivateMethodTransform({ // Create a new ClassPrivateMethod node to replace the ClassMethod const classPrivateMethod = t.classPrivateMethod( - 'method', // kind: 'method' | 'get' | 'set' + 'method', t.privateName(t.identifier(originalPrivateName)), // key - path.node.params, // params - path.node.body // body + path.node.params, + path.node.body, + path.node.static ); + // Set the additional properties that t.classPrivateMethod builder doesn't support + // this might be a bug on babel ?? + classPrivateMethod.async = path.node.async; + classPrivateMethod.generator = path.node.generator; + classPrivateMethod.computed = path.node.computed; // Replace the entire ClassMethod with the new ClassPrivateMethod path.replaceWith(classPrivateMethod); diff --git a/playground/src/modules/x/counter/counter.js b/playground/src/modules/x/counter/counter.js index fb2401ee09..478c5e1903 100644 --- a/playground/src/modules/x/counter/counter.js +++ b/playground/src/modules/x/counter/counter.js @@ -10,8 +10,11 @@ export default class extends LightningElement { } decrement() { this.counter--; - this.#something(); + // eslint-disable-next-line no-console + this.#something().then(console.log); } - #something() {} + async #something() { + return 'I am async'; + } } From 26c6c46f57b635a39f2b9d7e5f3247f38756c42a Mon Sep 17 00:00:00 2001 From: rax-it Date: Thu, 4 Sep 2025 16:44:23 -0700 Subject: [PATCH 4/5] chore: cleanup --- .../@lwc/babel-plugin-component/src/private-method-transform.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@lwc/babel-plugin-component/src/private-method-transform.ts b/packages/@lwc/babel-plugin-component/src/private-method-transform.ts index 84d874f0d8..059dd5677d 100644 --- a/packages/@lwc/babel-plugin-component/src/private-method-transform.ts +++ b/packages/@lwc/babel-plugin-component/src/private-method-transform.ts @@ -20,7 +20,6 @@ import type { types } from '@babel/core'; * - Async private methods: async #methodName() * - Method parameters: #method(param1, param2) * - Method body: #method() { return this.value; } - * - Computed properties: #['method']() * - Static methods: static #method() * * EDGE CASES & MISSING SUPPORT: From 62b88bb4cd7e1d9a24e7031ac6b96d074cbad1eb Mon Sep 17 00:00:00 2001 From: rax-it Date: Thu, 4 Sep 2025 16:54:20 -0700 Subject: [PATCH 5/5] chore: remove option manipulation --- packages/@lwc/babel-plugin-component/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@lwc/babel-plugin-component/src/index.ts b/packages/@lwc/babel-plugin-component/src/index.ts index 3772963a37..eb0599997a 100644 --- a/packages/@lwc/babel-plugin-component/src/index.ts +++ b/packages/@lwc/babel-plugin-component/src/index.ts @@ -44,7 +44,6 @@ export default function LwcClassTransform(api: BabelAPI): PluginObj