-
Notifications
You must be signed in to change notification settings - Fork 439
@W-20806053 Implement Private Methods #5711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 7 commits
bea64c5
f443394
2d9ad36
999c686
6b1f599
4b1e630
8dcb515
9b1e109
61ad8b5
76da7ac
fcf9c25
c409872
591bc67
a2d3e4b
7e51945
d4f93e4
f284247
380c24d
f18b317
3461010
2ba8959
d38ae20
c59f08a
446b34a
197d2ff
f024518
4d60549
500c3b3
e49f1b3
2820b37
cb1870d
c2a8c07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| /* | ||
| * 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 { DecoratorErrors } from '@lwc/errors'; | ||
| import { PRIVATE_METHOD_PREFIX, PRIVATE_METHOD_METADATA_KEY } from './constants'; | ||
| import { handleError } from './utils'; | ||
| 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 | ||
| */ | ||
| export default function privateMethodTransform({ | ||
| types: t, | ||
| }: BabelAPI): Visitor<LwcBabelPluginPass> { | ||
| return { | ||
| Program: { | ||
| enter(path: NodePath<types.Program>, state: LwcBabelPluginPass) { | ||
a-chabot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const transformedNames = new Set<string>(); | ||
|
|
||
| // Transform private methods BEFORE any other plugin processes them | ||
| path.traverse( | ||
| { | ||
| ClassPrivateMethod( | ||
|
||
| methodPath: NodePath<types.ClassPrivateMethod>, | ||
| methodState: LwcBabelPluginPass | ||
| ) { | ||
| const key = methodPath.get('key'); | ||
|
|
||
| // We only want kind: 'method'. | ||
| // Other options not included are 'get', 'set', and 'constructor'. | ||
| const methodKind = 'method'; | ||
a-chabot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (key.isPrivateName() && methodPath.node.kind === methodKind) { | ||
| const node = methodPath.node; | ||
|
|
||
| // Reject private methods with decorators (e.g. @api, @track, @wire) | ||
| if (node.decorators && node.decorators.length > 0) { | ||
| handleError( | ||
| methodPath, | ||
| { errorInfo: DecoratorErrors.DECORATOR_ON_PRIVATE_METHOD }, | ||
| methodState | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| const privateName = key.node.id.name; | ||
| const transformedName = `${PRIVATE_METHOD_PREFIX}${privateName}`; | ||
| const keyReplacement = t.identifier(transformedName); | ||
|
|
||
| // Create a new ClassMethod node to replace the ClassPrivateMethod | ||
| // https://babeljs.io/docs/babel-types#classmethod | ||
| const classMethod = t.classMethod( | ||
| methodKind, | ||
| keyReplacement, | ||
| node.params, | ||
| node.body, | ||
| node.computed, | ||
| node.static, | ||
| node.generator, | ||
| node.async | ||
| ) as types.ClassMethod; | ||
|
|
||
| // Preserve TypeScript annotations and source location when present | ||
| if (node.returnType != null) { | ||
| classMethod.returnType = node.returnType; | ||
| } | ||
| if (node.typeParameters != null) { | ||
| classMethod.typeParameters = node.typeParameters; | ||
| } | ||
| if (node.loc != null) { | ||
| classMethod.loc = node.loc; | ||
| } | ||
| // Preserve TypeScript/ECMAScript modifier flags (excluded from t.classMethod() builder) | ||
| if (node.abstract != null) { | ||
| classMethod.abstract = node.abstract; | ||
| } | ||
| if (node.access != null) { | ||
| classMethod.access = node.access; | ||
| } | ||
| if (node.accessibility != null) { | ||
| classMethod.accessibility = node.accessibility; | ||
| } | ||
| if (node.optional != null) { | ||
| classMethod.optional = node.optional; | ||
| } | ||
| if (node.override != null) { | ||
| classMethod.override = node.override; | ||
| } | ||
|
||
|
|
||
| // Replace the entire ClassPrivateMethod node with the new ClassMethod node | ||
| // (we can't just replace the key of type PrivateName with type Identifier) | ||
| methodPath.replaceWith(classMethod); | ||
| transformedNames.add(transformedName); | ||
| } | ||
| }, | ||
| }, | ||
| state | ||
| ); | ||
|
|
||
| (state.file.metadata as any)[PRIVATE_METHOD_METADATA_KEY] = transformedNames; | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| /* | ||
| * 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 { DecoratorErrors } from '@lwc/errors'; | ||
| import { PRIVATE_METHOD_PREFIX, PRIVATE_METHOD_METADATA_KEY } from './constants'; | ||
| import type { BabelAPI, LwcBabelPluginPass } from './types'; | ||
| import type { types, NodePath, Visitor } 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. | ||
| * | ||
| * Round-trip parity: to match {@link ./private-method-transform.ts}, this transform must copy the same | ||
| * properties from ClassMethod onto ClassPrivateMethod when present: returnType, typeParameters, loc, | ||
| * abstract, access, accessibility, optional, override (plus async, generator, computed from the builder). | ||
| * | ||
| * @see {@link ./private-method-transform.ts} for original transformation | ||
| */ | ||
| export default function reversePrivateMethodTransform({ | ||
| types: t, | ||
| }: BabelAPI): Visitor<LwcBabelPluginPass> { | ||
| // Scoped to this plugin instance's closure. Safe as long as each Babel run creates a | ||
| // fresh plugin via LwcReversePrivateMethodTransform(); would accumulate across files if | ||
| // the same instance were ever reused. | ||
| const reverseTransformedNames = new Set<string>(); | ||
|
|
||
| return { | ||
| ClassMethod(path: NodePath<types.ClassMethod>, state: LwcBabelPluginPass) { | ||
| 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)) { | ||
| const forwardTransformedNames: Set<string> | undefined = ( | ||
| state.file.metadata as any | ||
| )[PRIVATE_METHOD_METADATA_KEY]; | ||
|
|
||
| // If the method was not transformed by the forward pass, it is a | ||
| // user-defined method that collides with the reserved prefix. | ||
| // This will throw an error to tell the user to rename their function. | ||
| if (!forwardTransformedNames || !forwardTransformedNames.has(methodName)) { | ||
| const message = | ||
| DecoratorErrors.PRIVATE_METHOD_NAME_COLLISION.message.replace( | ||
| '{0}', | ||
| methodName | ||
| ); | ||
| throw path.buildCodeFrameError(message); | ||
| } | ||
|
|
||
| // Extract the original private method name | ||
| const originalPrivateName = methodName.replace(PRIVATE_METHOD_PREFIX, ''); | ||
|
|
||
| // Create a new ClassPrivateMethod node to replace the ClassMethod | ||
| const node = path.node; | ||
| const classPrivateMethod = t.classPrivateMethod( | ||
| 'method', | ||
| t.privateName(t.identifier(originalPrivateName)), // key | ||
| node.params, | ||
| node.body, | ||
| node.static | ||
| ); | ||
|
|
||
| // Properties the t.classPrivateMethod() builder doesn't accept (same as forward transform) | ||
| classPrivateMethod.async = node.async; | ||
| classPrivateMethod.generator = node.generator; | ||
| classPrivateMethod.computed = node.computed; | ||
|
|
||
| // Round-trip parity with private-method-transform: preserve TS annotations and modifier flags | ||
| if (node.returnType != null) classPrivateMethod.returnType = node.returnType; | ||
| if (node.typeParameters != null) | ||
| classPrivateMethod.typeParameters = node.typeParameters; | ||
| if (node.loc != null) classPrivateMethod.loc = node.loc; | ||
| if (node.abstract != null) classPrivateMethod.abstract = node.abstract; | ||
| if (node.access != null) classPrivateMethod.access = node.access; | ||
| if (node.accessibility != null) | ||
| classPrivateMethod.accessibility = node.accessibility; | ||
| if (node.optional != null) classPrivateMethod.optional = node.optional; | ||
| if (node.override != null) classPrivateMethod.override = node.override; | ||
a-chabot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Replace the entire ClassMethod with the new ClassPrivateMethod | ||
| path.replaceWith(classPrivateMethod); | ||
| reverseTransformedNames.add(methodName); | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| // After all nodes have been visited, verify that every method the forward transform | ||
| // renamed was also restored by the reverse transform. A mismatch here means an | ||
| // intermediate plugin (e.g. @babel/plugin-transform-class-properties) removed or | ||
| // renamed a prefixed method, leaving a mangled name in the final output. | ||
| Program: { | ||
| exit(_path: NodePath<types.Program>, state: LwcBabelPluginPass) { | ||
| const forwardTransformedNames: Set<string> | undefined = ( | ||
| state.file.metadata as any | ||
| )[PRIVATE_METHOD_METADATA_KEY]; | ||
|
|
||
| if (!forwardTransformedNames) { | ||
| return; | ||
| } | ||
|
|
||
| // Identify methods that were forward-transformed but never reverse-transformed | ||
| const missingFromReverse: string[] = []; | ||
| for (const name of forwardTransformedNames) { | ||
| if (!reverseTransformedNames.has(name)) { | ||
| missingFromReverse.push(name); | ||
| } | ||
| } | ||
|
|
||
| if (missingFromReverse.length > 0) { | ||
| throw new Error( | ||
| `Private method transform count mismatch: ` + | ||
| `forward transformed ${forwardTransformedNames.size} method(s), ` + | ||
| `but reverse transformed ${reverseTransformedNames.size}. ` + | ||
| `Missing reverse transforms for: ${missingFromReverse.join(', ')}` | ||
| ); | ||
| } | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.