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..eb0599997a 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 { }; } + * - needs to be tested + * + */ +export default function privateMethodTransform({ + types: t, +}: BabelAPI): Visitor { + return { + 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'); + + // 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); + } + }, + }); + }, + }, + }; +} 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..368044b252 --- /dev/null +++ b/packages/@lwc/babel-plugin-component/src/reverse-private-method-transform.ts @@ -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 { + return { + ClassMethod(path: NodePath) { + 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); + } + } + }, + }; +} diff --git a/playground/src/modules/x/counter/counter.js b/playground/src/modules/x/counter/counter.js index 058d61167c..478c5e1903 100644 --- a/playground/src/modules/x/counter/counter.js +++ b/playground/src/modules/x/counter/counter.js @@ -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'; } }