@lwc/vitest-preset 19.7.0-alpha.0 → 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
@@ -46,7 +46,7 @@ Namespace imports (`import Child from 'x/child'`, or `<x-child>` in a template)
46
46
  }
47
47
  ```
48
48
 
49
- This replaces the `moduleNameMapper` entry used with `@lwc/jest-preset`.
49
+ This replaces the namespace entries, such as `'^c/(.+)$'`, of the `moduleNameMapper` used with `@lwc/jest-preset`. For other entries, use the [`moduleNameMapper`](#modulenamemapper) option.
50
50
 
51
51
  #### nativeShadow
52
52
 
@@ -75,6 +75,36 @@ export default defineConfig({
75
75
  });
76
76
  ```
77
77
 
78
+ #### moduleNameMapper
79
+
80
+ Maps imports the way Jest's [`moduleNameMapper`](https://jestjs.io/docs/configuration#modulenamemapper-objectstring-string--arraystring) does, so stubs set up for Jest keep working:
81
+
82
+ ```js
83
+ export default defineConfig(
84
+ lwcVitestConfig(import.meta.url, {
85
+ moduleNameMapper: {
86
+ '^lightning/(.+)$': '<rootDir>/stubs/lightning/$1/$1',
87
+ },
88
+ })
89
+ );
90
+ ```
91
+
92
+ The rules are Jest's:
93
+
94
+ - Keys are regular expressions. They match anywhere in the import specifier unless anchored with `^` and `$`.
95
+ - The first key that matches, in the order written, wins. If none of its targets exist, the import fails; later keys aren't tried.
96
+ - The target replaces the whole specifier, with `$1`, `$2` and so on filled in from the key's capture groups.
97
+ - `<rootDir>` is the directory of the Vitest config.
98
+ - An array target uses the first path that exists.
99
+
100
+ Mapped imports are resolved before the `@salesforce/*` mocks, so a mapped `@salesforce/*` import loads your module, not the mock.
101
+
102
+ Vite's `resolve.alias` can't express these rules: a regular-expression alias replaces only the part of the specifier that matched. For example, `'^@salesforce/label/'` aliased to a file leaves the label name stuck to the end of the file's path.
103
+
104
+ Imports made from inside a dependency that Vitest externalizes (runs in Node without transforming it) aren't mapped. To map them, add the package to `test.server.deps.inline`.
105
+
106
+ The plugin behind this option is exported as `lwcModuleNameMapper({ moduleNameMapper, rootDir })`, for configs that build their own plugin list. Put it before `salesforceScopedImports()` from `@lwc/vitest-plugin`, or the mocks claim mapped `@salesforce/*` imports first.
107
+
78
108
  ### Custom matchers
79
109
 
80
110
  `expect().toThrowInConnectedCallback()` and its alias `toThrowErrorInConnectedCallback()` are registered automatically and typed for TypeScript. They behave as they do in [`@lwc/jest-preset`](https://github.com/salesforce/lwc-test/blob/master/packages/%40lwc/jest-preset/README.md#custom-matchers).
@@ -85,6 +115,7 @@ export default defineConfig({
85
115
  - **Component names come from the file path.** `@lwc/jest-transformer` compiles every component as `x-test`. Here a component keeps its real name (for example `smoke-mixin`), so tag names and CSS scope tokens differ.
86
116
  - **No SSR preset.** There is no counterpart to `@lwc/jest-preset/ssr`.
87
117
  - **`import.meta` is not rewritten.** `@lwc/jest-transformer` turns every `import.meta.env.X` into a boolean. Vitest keeps the real `import.meta`. See [`import.meta`](#importmeta).
118
+ - **A broken mapping fails the import.** If no target of a matching `moduleNameMapper` key exists, the import fails. Under Jest, `@lwc/jest-transformer` wraps the `@salesforce/*` imports it mocks in a try/catch, so a broken mapping for one of them quietly falls back to the default mock.
88
119
 
89
120
  ### `import.meta`
90
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lwc/vitest-preset",
3
- "version": "19.7.0-alpha.0",
3
+ "version": "19.7.0-alpha.1",
4
4
  "description": "Vitest preset configuration and setup to help test LWC",
5
5
  "homepage": "https://lwc.dev/",
6
6
  "repository": {
@@ -37,8 +37,8 @@
37
37
  "@babel/plugin-syntax-class-properties": "7.12.13",
38
38
  "@babel/plugin-syntax-decorators": "7.24.7",
39
39
  "@babel/preset-typescript": "7.24.7",
40
- "@lwc/jest-shared": "19.7.0-alpha.0",
41
- "@lwc/vitest-plugin": "19.7.0-alpha.0"
40
+ "@lwc/jest-shared": "19.7.0-alpha.1",
41
+ "@lwc/vitest-plugin": "19.7.0-alpha.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@lwc/engine-dom": "^9.1.1",
@@ -51,5 +51,5 @@
51
51
  "engines": {
52
52
  "node": ">=20"
53
53
  },
54
- "gitHead": "ded506234f9aac0e8195342811aae7bd22b48742"
54
+ "gitHead": "599fb9694a3daffe1fb1f723bd34be130a37d16c"
55
55
  }
package/src/index.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import lwc from '@lwc/rollup-plugin';
9
9
  import { salesforceScopedImports } from '@lwc/vitest-plugin';
10
+ import { lwcModuleNameMapper } from './module-name-mapper.js';
10
11
  import { lwcKnownScopeTokens } from './scope-tokens.js';
11
12
  import { lwcTypeScript } from './typescript.js';
12
13
 
@@ -87,7 +88,11 @@ function lwcCoverageExclude() {
87
88
 
88
89
  // Vitest config factory for LWC. Call as `lwcVitestConfig(import.meta.url)` from a vitest.config.
89
90
  // `nativeShadow: true` skips synthetic-shadow (Jest's `globals['lwc-jest'].nativeShadow`).
90
- export function lwcVitestConfig(importMetaUrl, { nativeShadow = false } = {}) {
91
+ // `moduleNameMapper` takes Jest's moduleNameMapper rules; `<rootDir>` is the config's directory.
92
+ export function lwcVitestConfig(
93
+ importMetaUrl,
94
+ { nativeShadow = false, moduleNameMapper = {} } = {}
95
+ ) {
91
96
  const rootDir = fileURLToPath(new URL('.', importMetaUrl));
92
97
  // Compiler options mirroring the jest-transformer config for parity.
93
98
  // rootDir is mandatory: buildStart crashes without it under Vitest (no rollup input).
@@ -115,6 +120,8 @@ export function lwcVitestConfig(importMetaUrl, { nativeShadow = false } = {}) {
115
120
  plugins: [
116
121
  lwcCoverageExclude(),
117
122
  lwcStylesheetIds(),
123
+ // Before the @salesforce/* mocks, which would otherwise claim a mapped specifier first.
124
+ lwcModuleNameMapper({ moduleNameMapper, rootDir }),
118
125
  salesforceScopedImports(),
119
126
  lwcTypeScript(),
120
127
  // Compile .js/.html/.css LWC bundles (replaces @lwc/jest-transformer's compile job).
@@ -162,4 +169,6 @@ export function lwcVitestConfig(importMetaUrl, { nativeShadow = false } = {}) {
162
169
  };
163
170
  }
164
171
 
172
+ // For configs that build their own plugin list; put it before salesforceScopedImports().
173
+ export { lwcModuleNameMapper };
165
174
  export default lwcVitestConfig;
@@ -0,0 +1,80 @@
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
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ // Marks the resolve of a mapped target: Jest resolves targets without the mapper.
11
+ const MAPPED = 'lwc-module-name-mapper';
12
+
13
+ // existsSync first: statSync throws on Vite's \0 virtual ids.
14
+ const isFile = (file) => fs.existsSync(file) && fs.statSync(file).isFile();
15
+
16
+ // Jest's replaceRootDirInPath: only a leading <rootDir> is replaced.
17
+ function replaceRootDir(target, rootDir) {
18
+ if (!target.startsWith('<rootDir>')) {
19
+ return target;
20
+ }
21
+ return path.resolve(rootDir, path.normalize(`./${target.slice('<rootDir>'.length)}`));
22
+ }
23
+
24
+ // Jest's moduleNameMapper as a Vite plugin. Vite's regex `resolve.alias` replaces only the part of
25
+ // a specifier that matched; Jest replaces the whole specifier with the target, filling in $1, $2...
26
+ // from the key's capture groups. Keys are unanchored regexes, the first key that matches (in
27
+ // insertion order) wins, and an array target uses the first path that exists.
28
+ export function lwcModuleNameMapper({ moduleNameMapper = {}, rootDir }) {
29
+ const rules = Object.entries(moduleNameMapper).map(([key, target]) => ({
30
+ key,
31
+ regex: new RegExp(key),
32
+ targets: [target].flat().map((t) => replaceRootDir(t, rootDir)),
33
+ }));
34
+ let entryImporter;
35
+ return {
36
+ name: 'lwc-module-name-mapper',
37
+ enforce: 'pre',
38
+ configResolved({ root }) {
39
+ // The importer Vite passes when there is none, as for Vitest's test and setup files.
40
+ entryImporter = path.join(root, 'index.html');
41
+ },
42
+ async resolveId(source, importer, options) {
43
+ // Jest maps only imports: never test or setup files, and never a mapped target.
44
+ if (importer === entryImporter || options?.custom?.[MAPPED]) {
45
+ return null;
46
+ }
47
+ const rule = rules.find(({ regex }) => regex.test(source));
48
+ if (!rule) {
49
+ return null;
50
+ }
51
+ const match = source.match(rule.regex);
52
+ const candidates = rule.targets.map((target) =>
53
+ target.replaceAll(/\$(\d+)/g, (_, index) => match[Number(index)] || '')
54
+ );
55
+ for (const candidate of candidates) {
56
+ const resolved = await this.resolve(candidate, importer, {
57
+ ...options,
58
+ skipSelf: true,
59
+ custom: { ...options?.custom, [MAPPED]: true },
60
+ });
61
+ // @lwc/rollup-plugin resolves any relative or absolute path, even a missing one, so
62
+ // check the disk. Untag stylesheet ids first: lwcStylesheetIds renames .css to
63
+ // .css.lwc.js. The importer is undefined when Vite resolves without one.
64
+ if (
65
+ resolved &&
66
+ (isFile(resolved.id.split('?')[0].replace(/\.css\.lwc\.js$/, '.css')) ||
67
+ (importer && isFile(path.resolve(path.dirname(importer), candidate))))
68
+ ) {
69
+ return resolved;
70
+ }
71
+ }
72
+ // Like Jest, the first matching key is final: later keys (and the @salesforce/* mocks)
73
+ // never get the specifier.
74
+ throw new Error(
75
+ `Could not locate module ${source} mapped as:\n${candidates.join('\n')}\n\n` +
76
+ `Please check the moduleNameMapper entry ${JSON.stringify(rule.key)}.`
77
+ );
78
+ },
79
+ };
80
+ }
package/types/index.d.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  /* eslint-disable */
2
2
  import 'vitest';
3
- import type { ViteUserConfig } from 'vitest/config';
3
+ import type { Plugin, ViteUserConfig } from 'vitest/config';
4
+
5
+ /** Jest `moduleNameMapper` rules: regex keys, targets that can use `$1`, `$2`... and `<rootDir>`. */
6
+ export type ModuleNameMapper = Record<string, string | string[]>;
4
7
 
5
8
  export interface LwcVitestOptions {
6
9
  /** Skip synthetic-shadow and run components in native shadow DOM. */
7
10
  nativeShadow?: boolean;
11
+ /** Map imports as Jest's `moduleNameMapper` does; `<rootDir>` is the config's directory. */
12
+ moduleNameMapper?: ModuleNameMapper;
8
13
  }
9
14
 
10
15
  /** Vitest config for LWC; call as `lwcVitestConfig(import.meta.url)` from a vitest config. */
@@ -14,6 +19,16 @@ export declare function lwcVitestConfig(
14
19
  ): ViteUserConfig;
15
20
  export default lwcVitestConfig;
16
21
 
22
+ /**
23
+ * The Vite plugin behind the `moduleNameMapper` option, for configs that build their own plugin
24
+ * list. Put it before `salesforceScopedImports()`, or the `@salesforce/*` mocks win.
25
+ */
26
+ export declare function lwcModuleNameMapper(options: {
27
+ moduleNameMapper?: ModuleNameMapper;
28
+ /** What `<rootDir>` in a target stands for. */
29
+ rootDir: string;
30
+ }): Plugin;
31
+
17
32
  interface Constructable {
18
33
  new (...args: any[]): any;
19
34
  }