Skip to content

Commit 84f9428

Browse files
a-chabotclaude
andauthored
Only allow enablePrivateMethod to be true for lightning & interop namespaces (#420)
* Add enablePrivateMethods compiler option to jest-transformer Enable private methods support in LWC jest transformer by adding the enablePrivateMethods flag to compiler options. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Bump LWC to 9.1.0 and add private methods integration tests - Upgrade LWC packages from 9.0.3 to 9.1.0 for private methods support - Add integration tests for private methods feature - Tests verify that private methods work correctly within LWC components Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix yarn.lock to use public npm registry instead of internal proxy Replace Salesforce internal Nexus proxy URLs with public npm registry URLs to fix CI failures. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: extractNamespace Signed-off-by: a.chabot <a.chabot@salesforce.com> * feat: tests Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: add tests Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: always x Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: missed test Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: missed test Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: test Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: smoke-private-methods Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: relateive and absolute paths Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: new module Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: clean up tests Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: no fallback x Signed-off-by: a.chabot <a.chabot@salesforce.com> * fix: basic test Signed-off-by: a.chabot <a.chabot@salesforce.com> --------- Signed-off-by: a.chabot <a.chabot@salesforce.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6443f16 commit 84f9428

9 files changed

Lines changed: 257 additions & 37 deletions

File tree

packages/@lwc/jest-transformer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
],
2020
"files": [
2121
"/src/index.js",
22+
"/src/extract-namespace.js",
2223
"/src/ssr.js",
2324
"/src/transforms/*.js"
2425
],
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const { extractNamespace } = require('../extract-namespace');
2+
3+
describe('extractNamespace', () => {
4+
it("returns '' when path does not match modules/ or jest-modules/ layout", () => {
5+
expect(extractNamespace('/repo/src/foo.js')).toBe('');
6+
});
7+
8+
it('returns the namespace segment when path is /modules/{namespace}/...', () => {
9+
expect(extractNamespace('/modules/x/foo/bar.js')).toBe('x');
10+
expect(extractNamespace('/modules/x/foo/bar/baz.js')).toBe('x');
11+
expect(extractNamespace('/modules/lightning/spinner/spinner.js')).toBe('lightning');
12+
expect(extractNamespace('/modules/interop/spinner/spinner.js')).toBe('interop');
13+
});
14+
15+
it('returns namespace from modules/{namespace}/...', () => {
16+
expect(extractNamespace('/repo/modules/c/foo/foo.js')).toBe('c');
17+
});
18+
19+
it('returns namespace when path contains __tests__', () => {
20+
expect(extractNamespace('/repo/modules/c/foo/__tests__/foo.test.js')).toBe('c');
21+
});
22+
23+
it('returns namespace from jest-modules/{namespace}/...', () => {
24+
expect(extractNamespace('/repo/jest-modules/interop/bar/bar.js')).toBe('interop');
25+
});
26+
27+
it('normalizes Windows separators before matching', () => {
28+
expect(extractNamespace('C:\\repo\\modules\\lightning\\x\\x.js')).toBe('lightning');
29+
});
30+
31+
it('extracts namespace from absolute paths', () => {
32+
expect(extractNamespace('/var/project/modules/lightning/button/button.js')).toBe(
33+
'lightning'
34+
);
35+
expect(extractNamespace('/repo/nested/jest-modules/interop/cmp/cmp.js')).toBe('interop');
36+
});
37+
38+
it('extracts namespace from relative paths', () => {
39+
expect(extractNamespace('project/modules/smoke/widget/widget.js')).toBe('smoke');
40+
expect(extractNamespace('modules/c/foo/foo.js')).toBe('c');
41+
expect(extractNamespace('./src/jest-modules/interop/x/x.js')).toBe('interop');
42+
expect(extractNamespace('packages/pkg/modules/custom/lib/lib.js')).toBe('custom');
43+
});
44+
45+
it("returns '' when modules segment has no trailing path segment", () => {
46+
expect(extractNamespace('/repo/modules')).toBe('');
47+
});
48+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
jest.mock('@lwc/compiler/dist/index.cjs', () => ({
8+
transformSync: jest.fn(() => ({
9+
code: '/* lwc-compiled */',
10+
map: null,
11+
warnings: [],
12+
})),
13+
}));
14+
15+
jest.mock('@babel/core', () => ({
16+
transform: jest.fn(() => ({ code: '/* babel */' })),
17+
}));
18+
19+
jest.mock('@lwc/template-compiler/dist/index.cjs', () => ({
20+
generateScopeTokens: jest.fn(() => ({ cssScopeTokens: undefined })),
21+
}));
22+
23+
const lwcCompiler = require('@lwc/compiler/dist/index.cjs');
24+
const { transformLwc } = require('../index');
25+
26+
describe('transformLwc compiler options (namespace / enablePrivateMethods)', () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
31+
it('sets enablePrivateMethods true when path matches modules/... (lightning)', () => {
32+
transformLwc('export default class {}', '/repo/modules/lightning/foo/foo.js', false);
33+
34+
expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
35+
expect.any(String),
36+
'/repo/modules/lightning/foo/foo.js',
37+
expect.objectContaining({
38+
namespace: 'x',
39+
enablePrivateMethods: true,
40+
})
41+
);
42+
});
43+
44+
it('sets enablePrivateMethods true when path matches modules/... (interop)', () => {
45+
transformLwc('export default class {}', '/repo/modules/interop/bar/bar.js', false);
46+
47+
expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
48+
expect.any(String),
49+
'/repo/modules/interop/bar/bar.js',
50+
expect.objectContaining({
51+
namespace: 'x',
52+
enablePrivateMethods: true,
53+
})
54+
);
55+
});
56+
57+
it('sets enablePrivateMethods false when path matches modules/smoke/...', () => {
58+
transformLwc(
59+
'export default class {}',
60+
'/repo/modules/smoke/privateMethods/privateMethods.js',
61+
false
62+
);
63+
64+
expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
65+
expect.any(String),
66+
'/repo/modules/smoke/privateMethods/privateMethods.js',
67+
expect.objectContaining({
68+
namespace: 'x',
69+
enablePrivateMethods: false,
70+
})
71+
);
72+
});
73+
74+
it('sets enablePrivateMethods false for other modules/... namespaces', () => {
75+
transformLwc('export default class {}', '/repo/modules/c/baz/baz.js', false);
76+
77+
expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
78+
expect.any(String),
79+
'/repo/modules/c/baz/baz.js',
80+
expect.objectContaining({
81+
namespace: 'x',
82+
enablePrivateMethods: false,
83+
})
84+
);
85+
});
86+
87+
it('sets enablePrivateMethods false when path has no modules/ or jest-modules/ segment', () => {
88+
transformLwc('export default class {}', '/repo/src/standalone.js', false);
89+
90+
expect(lwcCompiler.transformSync).toHaveBeenCalledWith(
91+
expect.any(String),
92+
'/repo/src/standalone.js',
93+
expect.objectContaining({
94+
namespace: 'x',
95+
enablePrivateMethods: false,
96+
})
97+
);
98+
});
99+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
8+
/**
9+
* Extracts the namespace from file path
10+
* Handles patterns like:
11+
* - modules/{namespace}/{component}/{component}.js
12+
* - modules/{namespace}/{component}/__tests__/...
13+
* - jest-modules/{namespace}/{component}/{component}.js
14+
* Returns '' if the namespace cannot be determined
15+
* @param {string} filePath
16+
* @returns {string}
17+
*/
18+
function extractNamespace(filePath) {
19+
const normalizedPath = filePath.replace(/\\/g, '/');
20+
21+
// Match patterns: modules/{namespace} or jest-modules/{namespace}
22+
const match = normalizedPath.match(/(?:^|\/)(modules|jest-modules)\/([^/]+)\//);
23+
24+
if (match && match[2]) {
25+
return match[2];
26+
}
27+
28+
return '';
29+
}
30+
31+
module.exports = { extractNamespace };

packages/@lwc/jest-transformer/src/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const messageChannelScopedImport = require('./transforms/message-channel-scoped-
3838
const accessCheck = require('./transforms/access-check-scoped-import');
3939
const siteScopedImport = require('./transforms/site-scoped-import');
4040
const importMeta = require('./transforms/import-meta');
41+
const { extractNamespace } = require('./extract-namespace');
4142

4243
const BABEL_TS_CONFIG = {
4344
sourceMaps: 'inline',
@@ -96,7 +97,11 @@ function transformLWC(src, filePath, isSSR) {
9697
src = transformTypeScript(src, filePath);
9798
}
9899

99-
// Set default module name and namespace value for the namespace because it can't be properly guessed from the path
100+
// Extract namespace from the file path
101+
const pathNamespace = extractNamespace(filePath);
102+
const enablePrivateMethods = pathNamespace === 'lightning' || pathNamespace === 'interop';
103+
104+
// Set default module name value for the namespace because it can't be properly guessed from the path
100105
const compilerOptions = {
101106
name: 'test',
102107
namespace: 'x',
@@ -113,7 +118,7 @@ function transformLWC(src, filePath, isSSR) {
113118
scopedStyles: isKnownScopedCssFile(filePath),
114119
enableDynamicComponents: true,
115120
enableLwcOn: true,
116-
enablePrivateMethods: true,
121+
enablePrivateMethods,
117122
/**
118123
* Prevent causing tons of warning log lines.
119124
* @see {@link https://github.com/salesforce/lwc/pull/3544}
@@ -209,3 +214,4 @@ module.exports = {
209214
};
210215

211216
module.exports.transformLwc = transformLWC;
217+
module.exports.extractNamespace = extractNamespace;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
import { createElement } from 'lwc';
8+
import PrivateMethods from '../privateMethods';
9+
10+
describe('lightning private methods', () => {
11+
it('should allow calling private methods from public methods', () => {
12+
const element = createElement('lightning-private-methods', { is: PrivateMethods });
13+
document.body.appendChild(element);
14+
15+
expect(element.publicMethod()).toBe(1);
16+
expect(element.value).toBe(1);
17+
expect(element.getCounter()).toBe(1);
18+
});
19+
20+
it('should maintain state across multiple calls', () => {
21+
const element = createElement('lightning-private-methods', { is: PrivateMethods });
22+
document.body.appendChild(element);
23+
24+
element.publicMethod();
25+
element.publicMethod();
26+
const result = element.publicMethod();
27+
28+
expect(result).toBe(3);
29+
expect(element.getCounter()).toBe(3);
30+
});
31+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
import { LightningElement, api } from 'lwc';
8+
9+
export default class PrivateMethods extends LightningElement {
10+
@api value = 0;
11+
counter = 0;
12+
13+
#incrementPrivate() {
14+
this.counter++;
15+
return this.counter;
16+
}
17+
18+
@api
19+
publicMethod() {
20+
// Call private method from public method
21+
const count = this.#incrementPrivate();
22+
this.value = count;
23+
return count;
24+
}
25+
26+
@api
27+
getCounter() {
28+
return this.counter;
29+
}
30+
}
Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,10 @@
1-
/*
2-
* Copyright (c) 2018, salesforce.com, inc.
3-
* All rights reserved.
4-
* SPDX-License-Identifier: MIT
5-
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6-
*/
7-
import { createElement } from 'lwc';
8-
import PrivateMethods from '../privateMethods';
9-
10-
describe('private methods', () => {
11-
it('should allow calling private methods from public methods', () => {
12-
const element = createElement('smoke-private-methods', { is: PrivateMethods });
13-
document.body.appendChild(element);
14-
15-
expect(element.publicMethod()).toBe(1);
16-
expect(element.value).toBe(1);
17-
expect(element.getCounter()).toBe(1);
18-
});
19-
20-
it('should maintain state across multiple calls', () => {
21-
const element = createElement('smoke-private-methods', { is: PrivateMethods });
22-
document.body.appendChild(element);
23-
24-
element.publicMethod();
25-
element.publicMethod();
26-
const result = element.publicMethod();
27-
28-
expect(result).toBe(3);
29-
expect(element.getCounter()).toBe(3);
1+
describe('smoke private methods (smoke-private-methods)', () => {
2+
// A top-level `import '../privateMethods'` runs before tests; Jest transforms that file with
3+
// `enablePrivateMethods: false` for smoke namespace, so compilation throws
4+
it('fails to compile: # private methods need enablePrivateMethods (only lightning/interop paths)', () => {
5+
jest.resetModules();
6+
expect(() => {
7+
require('../privateMethods');
8+
}).toThrow(/LWC1007:[\s\S]*Class private methods are not enabled/);
309
});
3110
});

test/src/modules/smoke/privateMethods/privateMethods.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
/*
2-
* Copyright (c) 2018, salesforce.com, inc.
3-
* All rights reserved.
4-
* SPDX-License-Identifier: MIT
5-
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6-
*/
71
import { LightningElement, api } from 'lwc';
82

3+
// This component won't fully compile because it's not in lightning or interop namespace (namespace = smoke)
94
export default class PrivateMethods extends LightningElement {
105
@api value = 0;
116
counter = 0;

0 commit comments

Comments
 (0)