Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/@lwc/babel-plugin-component/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -46,4 +47,5 @@ export {
COMPONENT_NAME_KEY,
API_VERSION_KEY,
COMPONENT_CLASS_ID,
PRIVATE_METHOD_PREFIX,
};
15 changes: 15 additions & 0 deletions packages/@lwc/babel-plugin-component/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -33,6 +35,8 @@ export default function LwcClassTransform(api: BabelAPI): PluginObj<LwcBabelPlug
const { Class: transformDecorators } = decorators(api);
const { Import: transformDynamicImports } = dynamicImports();
const { ClassBody: addCompilerVersionNumber } = compilerVersionNumber(api);
const { Program: transformPrivateMethods } = privateMethodTransform(api);
const { ClassMethod: reverseTransformPrivateMethods } = reversePrivateMethodTransform(api);

return {
manipulateOptions(opts, parserOpts) {
Expand All @@ -54,6 +58,15 @@ export default function LwcClassTransform(api: BabelAPI): PluginObj<LwcBabelPlug

// Add ?scoped=true to *.scoped.css imports
scopeCssImports(api, path);

// Transform private methods BEFORE any other plugin processes them
if (
transformPrivateMethods &&
typeof transformPrivateMethods === 'object' &&
'enter' in transformPrivateMethods
) {
(transformPrivateMethods as any).enter(path, state);
}
},
exit(path) {
const engineImportSpecifiers = getEngineImportSpecifiers(path);
Expand All @@ -68,6 +81,8 @@ export default function LwcClassTransform(api: BabelAPI): PluginObj<LwcBabelPlug

Class: transformDecorators,

ClassMethod: reverseTransformPrivateMethods,

ClassBody: addCompilerVersionNumber,

ExportDefaultDeclaration: transformCreateRegisterComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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 __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; }
* - 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<LwcBabelPluginPass> {
return {
Program: {
enter(path: NodePath<types.Program>) {
// 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<types.ClassPrivateMethod>) {
const key = path.get('key');

// 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}`;

// 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);
}
},
});
},
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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<LwcBabelPluginPass> {
return {
ClassMethod(path: NodePath<types.ClassMethod>) {
const key = path.get('key');

// Check if the key is an identifier with our special prefix
// 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
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',
t.privateName(t.identifier(originalPrivateName)), // key
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);
}
}
},
};
}
6 changes: 6 additions & 0 deletions playground/src/modules/x/counter/counter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@ export default class extends LightningElement {
}
decrement() {
this.counter--;
// eslint-disable-next-line no-console
this.#something().then(console.log);
}

async #something() {
return 'I am async';
}
}