@lwc/vitest-plugin 0.0.0-stage → 19.7.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ MIT LICENSE
2
+
3
+ Copyright (c) 2018, Salesforce.com, Inc.
4
+ All rights reserved.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,8 @@
1
- # Temporary Holding Version
1
+ # @lwc/vitest-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Vite/Vitest plugin that mocks `@salesforce/*` scoped imports for LWC unit tests.
4
+
5
+ It is the Vitest counterpart to the `@salesforce/*` mocking that `@lwc/jest-transformer`
6
+ performed under Jest. LWC compilation itself is handled separately by `@lwc/rollup-plugin`.
7
+
8
+ > **Status:** skeleton. The scoped-import mock logic lands in a later story.
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
- "name": "@lwc/vitest-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@lwc/vitest-plugin",
3
+ "version": "19.7.0-alpha.1",
4
+ "description": "Vite/Vitest plugin that mocks @salesforce/* scoped imports for LWC unit tests",
5
+ "homepage": "https://lwc.dev/",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/salesforce/lwc-test.git",
9
+ "directory": "packages/@lwc/vitest-plugin"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/salesforce/lwc-test/issues"
13
+ },
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "main": "src/index.js",
17
+ "keywords": [
18
+ "vitest",
19
+ "vite",
20
+ "lwc"
21
+ ],
22
+ "files": [
23
+ "/src/**/*.js"
24
+ ],
25
+ "exports": {
26
+ ".": "./src/index.js"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "gitHead": "599fb9694a3daffe1fb1f723bd34be130a37d16c"
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,230 @@
1
+ /*
2
+ * Copyright (c) 2026, 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
+ // Vite plugin: mocks `@salesforce/*` + `@label/` scoped imports for LWC unit tests.
9
+ const SCOPED_IMPORT_PREFIXES = [
10
+ '@salesforce/accessCheck/',
11
+ '@salesforce/apex', // also claims @salesforce/apexContinuation by prefix
12
+ '@salesforce/client/',
13
+ '@salesforce/contentAssetUrl/',
14
+ '@salesforce/customPermission/',
15
+ '@salesforce/i18n/',
16
+ '@salesforce/label/',
17
+ '@label/',
18
+ '@salesforce/messageChannel/',
19
+ '@salesforce/resourceUrl/',
20
+ '@salesforce/schema',
21
+ '@salesforce/user/Id',
22
+ '@salesforce/user/isGuest',
23
+ '@salesforce/userPermission/',
24
+ ];
25
+
26
+ // Rollup treats ids beginning with '\0' as virtual, so no other plugin loads them from disk.
27
+ const VIRTUAL_PREFIX = '\0salesforce:';
28
+
29
+ function isMockedSpecifier(source) {
30
+ return SCOPED_IMPORT_PREFIXES.some((prefix) => source.startsWith(prefix));
31
+ }
32
+
33
+ // Apex default-method imports (plain + continuation). Both are a callable returning a promise, so
34
+ // they share one fake — Jest mocks them identically. Trailing slash excludes bare `@salesforce/apex`
35
+ // named imports (their own story).
36
+ const APEX_METHOD_PREFIXES = ['@salesforce/apex/', '@salesforce/apexContinuation/'];
37
+
38
+ // Callable spy returning a promise, so both @wire and imperative calls work. Vite caches the
39
+ // virtual module per specifier, so all importers share one spy.
40
+ const APEX_METHOD_SOURCE = `import { vi } from 'vitest';\nexport default vi.fn(() => Promise.resolve());`;
41
+
42
+ // Bare `@salesforce/apex` (exact, no slash): real named exports for the module's API -- refreshApex
43
+ // returns a promise, getSObjectValue is a vi.fn() spy. Must be real `export` statements: Vitest's
44
+ // module runner builds the namespace from export statements only (syntheticNamedExports is Rollup-only).
45
+ const APEX_IMPORT_SPECIFIER = '@salesforce/apex';
46
+ const APEX_NAMED_IMPORTS_SOURCE = `import { vi } from 'vitest';\nexport const refreshApex = () => Promise.resolve();\nexport const getSObjectValue = vi.fn();`;
47
+
48
+ // Mock value is the specifier minus its matched prefix (e.g. `@salesforce/label/c.foo` -> `"c.foo"`).
49
+ const STRING_VALUE_PREFIXES = [
50
+ '@salesforce/label/',
51
+ '@label/', // legacy alias for @salesforce/label/
52
+ '@salesforce/resourceUrl/',
53
+ '@salesforce/contentAssetUrl/',
54
+ '@salesforce/messageChannel/',
55
+ '@salesforce/userPermission/',
56
+ '@salesforce/customPermission/',
57
+ '@salesforce/accessCheck/',
58
+ ];
59
+
60
+ function getStringValue(specifier) {
61
+ const prefix = STRING_VALUE_PREFIXES.find((p) => specifier.startsWith(p));
62
+ return prefix === undefined ? undefined : specifier.slice(prefix.length);
63
+ }
64
+
65
+ // user/* and client/* mock to fixed constants, not values derived from the specifier.
66
+ const USER_DEFAULTS = [
67
+ { prefix: '@salesforce/user/Id', value: '005000000000000000' },
68
+ { prefix: '@salesforce/user/isGuest', value: false },
69
+ ];
70
+ const CLIENT_PREFIX = '@salesforce/client/';
71
+ const CLIENT_MOCK_VALUES = {
72
+ formFactor: 'Large',
73
+ };
74
+
75
+ // Boxed in { value } so a legitimate `false` (isGuest) isn't mistaken for "no match".
76
+ function getFixedValue(specifier) {
77
+ const userDefault = USER_DEFAULTS.find((d) => specifier.startsWith(d.prefix));
78
+ if (userDefault !== undefined) {
79
+ return { value: userDefault.value };
80
+ }
81
+ if (specifier.startsWith(CLIENT_PREFIX)) {
82
+ const resource = specifier.slice(CLIENT_PREFIX.length);
83
+ return { value: resource in CLIENT_MOCK_VALUES ? CLIENT_MOCK_VALUES[resource] : '' };
84
+ }
85
+ return null;
86
+ }
87
+
88
+ const I18N_IMPORT_PREFIX = '@salesforce/i18n/';
89
+
90
+ // Ported from the Jest i18n transform's getMockValue -- keep in sync.
91
+ function mappedI18nValue(key) {
92
+ const parts = key.split('.');
93
+ switch (parts[0]) {
94
+ case 'lang':
95
+ return 'en';
96
+ case 'dir':
97
+ return 'ltr';
98
+ case 'locale':
99
+ return 'en-US';
100
+ case 'timeZone':
101
+ return 'America/Los_Angeles';
102
+ case 'currency':
103
+ return 'USD';
104
+ case 'firstDayOfWeek':
105
+ return 0;
106
+ case 'dateTime':
107
+ switch (parts[1]) {
108
+ case 'shortDateFormat':
109
+ return 'M/d/yyyy';
110
+ case 'mediumDateFormat':
111
+ return 'MMM d, yyyy';
112
+ case 'longDateFormat':
113
+ return 'MMMM d, yyyy';
114
+ case 'shortDateTimeFormat':
115
+ return 'M/d/yyyy h:mm a';
116
+ case 'mediumDateTimeFormat':
117
+ return 'MMM d, yyyy h:mm:ss a';
118
+ case 'shortTimeFormat':
119
+ return 'h:mm a';
120
+ case 'mediumTimeFormat':
121
+ return 'h:mm:ss a';
122
+ default:
123
+ return undefined;
124
+ }
125
+ case 'number':
126
+ switch (parts[1]) {
127
+ case 'numberFormat':
128
+ return '#,##0.###';
129
+ case 'percentFormat':
130
+ return '#,##0%';
131
+ case 'currencyFormat':
132
+ return '¤#,##0.00;(¤#,##0.00)';
133
+ case 'currencySymbol':
134
+ return '$';
135
+ default:
136
+ return undefined;
137
+ }
138
+ default:
139
+ return undefined;
140
+ }
141
+ }
142
+
143
+ // Mapped key -> its value; any other key -> ''
144
+ function getI18nValue(specifier) {
145
+ const key = specifier.slice(I18N_IMPORT_PREFIX.length);
146
+ const mapped = mappedI18nValue(key);
147
+ return mapped === undefined ? '' : mapped;
148
+ }
149
+
150
+ const SCHEMA_IMPORT_PREFIX = '@salesforce/schema';
151
+
152
+ function getSchemaValue(specifier) {
153
+ const resourcePath = specifier.slice(SCHEMA_IMPORT_PREFIX.length + 1);
154
+ const idx = resourcePath.indexOf('.');
155
+ if (idx === -1) {
156
+ return { objectApiName: resourcePath };
157
+ }
158
+ return {
159
+ objectApiName: resourcePath.slice(0, idx),
160
+ fieldApiName: resourcePath.slice(idx + 1),
161
+ };
162
+ }
163
+
164
+ // Narrow claim: only known keys are mocked; unknown @salesforce/site/* pass through to real resolution.
165
+ const SITE_PREFIX = '@salesforce/site/';
166
+ const SITE_VALUES = {
167
+ Id: '005000000000000000',
168
+ activeLanguages: [{ code: 'en-US', label: 'English (US)' }],
169
+ defaultLanguages: { code: 'en-US', label: 'English (US)' },
170
+ };
171
+
172
+ function getSiteValue(specifier) {
173
+ if (!specifier.startsWith(SITE_PREFIX)) {
174
+ return undefined;
175
+ }
176
+ const key = specifier.slice(SITE_PREFIX.length);
177
+ return Object.hasOwn(SITE_VALUES, key) ? SITE_VALUES[key] : undefined;
178
+ }
179
+
180
+ export function salesforceScopedImports() {
181
+ return {
182
+ name: '@lwc/vitest-plugin:salesforce-scoped-imports',
183
+ // Run before Vite's core `vite:resolve` so the mock wins even when the specifier also resolves to a real file on disk.
184
+ enforce: 'pre',
185
+ resolveId(source) {
186
+ const claimed = getSiteValue(source) !== undefined || isMockedSpecifier(source);
187
+ return claimed ? VIRTUAL_PREFIX + source : null;
188
+ },
189
+ load(id) {
190
+ if (!id.startsWith(VIRTUAL_PREFIX)) {
191
+ return null;
192
+ }
193
+ const specifier = id.slice(VIRTUAL_PREFIX.length);
194
+ if (APEX_METHOD_PREFIXES.some((prefix) => specifier.startsWith(prefix))) {
195
+ return APEX_METHOD_SOURCE;
196
+ }
197
+ if (specifier === APEX_IMPORT_SPECIFIER) {
198
+ return APEX_NAMED_IMPORTS_SOURCE;
199
+ }
200
+
201
+ const stringValue = getStringValue(specifier);
202
+ if (stringValue !== undefined) {
203
+ return `export default ${JSON.stringify(stringValue)};`;
204
+ }
205
+
206
+ const fixedValue = getFixedValue(specifier);
207
+ if (fixedValue !== null) {
208
+ return `export default ${JSON.stringify(fixedValue.value)};`;
209
+ }
210
+
211
+ if (specifier.startsWith(I18N_IMPORT_PREFIX)) {
212
+ return `export default ${JSON.stringify(getI18nValue(specifier))};`;
213
+ }
214
+
215
+ if (specifier.startsWith(SCHEMA_IMPORT_PREFIX)) {
216
+ return `export default ${JSON.stringify(getSchemaValue(specifier))};`;
217
+ }
218
+
219
+ const siteValue = getSiteValue(specifier);
220
+ if (siteValue !== undefined) {
221
+ return `export default ${JSON.stringify(siteValue)};`;
222
+ }
223
+
224
+ // Fallback for shapes without a dedicated value generator yet: echo the specifier.
225
+ return `export default ${JSON.stringify(specifier)};`;
226
+ },
227
+ };
228
+ }
229
+
230
+ export default salesforceScopedImports;