Skip to content

Commit e0640de

Browse files
authored
refactor(jest): add internal support for jest 27 (#3171)
this commit adds support for stencil to run jest 27 for its own unit tests. although it does not enable jest 27 for consumers of stencil, it is the first step in the process that future work shall build upon. update `jest`, `jest-cli` and `jest-environment-node` all to v27. jest v27 switched many defaults that would otherwise break stencil tests. to support jest v27: - explicitly use the 'jest-jasmine2' test runner in our own internal configuration. moving to 'jest-circus' will occur in STENCIL-307 and is not on the critical path for supporting Jest 27 - create a `getVmContext` function in jest environment setup, a new field that is required in v27 - remove unneeded `async` keyword from telemetry tests, which were causing jest to fail in addition to the changes above, several changes were required to be made to our `jest-preprocessor`, which is used both by consumers of stencil in addition to internal stencil tests: Between Jest v23 (the latest when this file was originally authored) and v27, the function signatures of `process` and `getCacheKey` have changed, most ostensibly between v26 and v27. for the purposes of this PR, we attempt to maintain backwards compatibility to the best of our ability, which leads to extra type checking. it is expected that this be removed in a future major version of Stencil. the context of `this` has changed between v26 and v27 when code coverage is generated for files that do not have tests associated with them. in the original implementation, running jest with the '--coverage' flag would cause a message to be printed to STDERR for _every file_ that didn't have test coverage. To circumvent this, cached instances of `tsconfig.json#options` and its stringified version have been hoisted to no longer require being referenced through `this`. an integer used to bust the Jest cache (previously just the number '6') has been refactored out to a constant, `CACHE_BUSTER` with a JSDoc to explain its usage and when it should be updated finally, disable the tests that are responsible for invoking jest as a part of the testing suite. the test assumes that jest can be run programmatically, specifically that the program has programmatic access to jest cli flags. with jest 27, this is no longer true. to limit the scope of this work (which is planned in STENCIL-71), temporarily disable the test. this is only necessary because stencil is internally running v27, but end users are still using v26. STENCIL-61: getCacheKey rework to support Jest 27 STENCIL-63: Jest 27 support for @stencil/core
1 parent 6faa5f2 commit e0640de

File tree

7 files changed

+5627
-4621
lines changed

7 files changed

+5627
-4621
lines changed

jest.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,7 @@ module.exports = {
5151
'<rootDir>/testing/',
5252
],
5353
testRegex: '/src/.*\\.spec\\.(ts|tsx|js)$',
54+
// TODO(STENCIL-307): Move away from Jasmine runner for internal Stencil tests, which involves re-working environment
55+
// setup
56+
testRunner: 'jest-jasmine2'
5457
};

package-lock.json

+5,479-4,587
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"test": "jest --coverage",
4545
"test.analysis": "cd test && npm run build",
4646
"test.dist": "node scripts --validate-build",
47-
"test.end-to-end": "cd test/end-to-end && npm ci && npm test && npm run test.dist",
47+
"//": "TODO(STENCIL-71): Add 'npm test &&' to the 'test.end-to-end' test",
48+
"test.end-to-end": "cd test/end-to-end && npm ci && npm run test.dist",
4849
"test.jest": "jest",
4950
"test.karma": "cd test/karma && npm ci && npm run karma",
5051
"test.karma.prod": "cd test/karma && npm ci && npm run karma.prod",
@@ -98,9 +99,9 @@
9899
"graceful-fs": "~4.2.6",
99100
"hash.js": "^1.1.7",
100101
"inquirer": "^7.3.3",
101-
"jest": "^26.6.3",
102-
"jest-cli": "^26.6.3",
103-
"jest-environment-node": "^26.6.2",
102+
"jest": "^27.4.3",
103+
"jest-cli": "^27.4.3",
104+
"jest-environment-node": "^27.4.2",
104105
"listr": "^0.14.3",
105106
"magic-string": "^0.25.7",
106107
"merge-source-map": "^1.1.0",

scripts/bundles/helpers/jest/jest-preset.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* The path's declared below are relative. Specifically, they are relative to the location of this file after
3+
* compilation of the Stencil compiler has completed. See `scripts/bundles/testing` for the location of this file
4+
* following compilation.
5+
*/
16
const path = require('path');
27
const testingDir = __dirname;
38
const rootDir = path.join(testingDir, '..');

src/cli/telemetry/test/telemetry.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as shouldTrack from '../shouldTrack';
44
import { createSystem } from '../../../compiler/sys/stencil-sys';
55
import { mockLogger } from '@stencil/core/testing';
66

7-
describe('telemetryBuildFinishedAction', async () => {
7+
describe('telemetryBuildFinishedAction', () => {
88
const config = {
99
outputTargets: [],
1010
flags: {
@@ -34,7 +34,7 @@ describe('telemetryBuildFinishedAction', async () => {
3434
});
3535
});
3636

37-
describe('telemetryAction', async () => {
37+
describe('telemetryAction', () => {
3838
const config = {
3939
outputTargets: [],
4040
flags: {

src/testing/jest/jest-environment.ts

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ export function createJestPuppeteerEnvironment() {
4545
await disconnectBrowser(this.browser);
4646
this.browser = null;
4747
}
48+
49+
getVmContext() {
50+
return super.getVmContext();
51+
}
4852
};
4953

5054
return JestEnvironment;

src/testing/jest/jest-preprocessor.ts

+129-28
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,79 @@ import { loadTypeScriptDiagnostic, normalizePath } from '@utils';
33
import { transpile } from '../test-transpile';
44
import { ts } from '@stencil/core/compiler';
55

6+
// TODO(STENCIL-306): Remove support for earlier versions of Jest
7+
type Jest26CacheKeyOptions = { instrument: boolean; rootDir: string };
8+
type Jest26Config = { instrument: boolean; rootDir: string };
9+
type Jest27TransformOptions = { config: Jest26Config };
10+
11+
/**
12+
* Type guard to differentiate Jest 27 `TransformOptions` from those found in Jest 26 and below
13+
* @param obj the entity to evaluate
14+
* @returns `true` if the `obj` is a `Jest27TransformOptions`, false otherwise
15+
*/
16+
const isJest27TransformOptions = (obj: unknown): obj is Jest27TransformOptions => {
17+
return typeof obj === 'object' && obj.hasOwnProperty('config');
18+
};
19+
20+
/**
21+
* Constant used for cache busting when the contents of this file have changed. When modifying this file, it's advised
22+
* this value be monotonically incremented.
23+
*/
24+
const CACHE_BUSTER = 7;
25+
26+
/**
27+
* Fields containing the consuming library's `tsconfig.json#options` entry. The first is the original representation,
28+
* where the second is the stringified version. These fields are cached to prevent unnecessary I/O & repetitive
29+
* stringification of the read options. Note that this caching does not persist across multiple Jest workers (I.E.
30+
* every Jest worker will read the `tsconfig.json` file and stringify it's `options` entry.
31+
*/
32+
let _tsCompilerOptions: ts.CompilerOptions | null = null;
33+
let _tsCompilerOptionsKey: string | null = null;
34+
635
export const jestPreprocessor = {
7-
process(sourceText: string, filePath: string, jestConfig: { rootDir: string }) {
8-
if (shouldTransform(filePath, sourceText)) {
36+
/**
37+
* Transforms a file to CommonJS to be used by Jest. The API for `process` is described in the
38+
* ["Writing custom transformers"](https://jestjs.io/docs/code-transformation#writing-custom-transformers)
39+
* documentation on the jest site. Unfortunately, the URL is not versioned at the time of this writing. For
40+
* reference, the v27.2 docs were referenced (the most recent available).
41+
*
42+
* This function attempts to support several versions of Jest (v23 through v27). Support for earlier versions of Jest
43+
* will be removed in a future major version of Stencil.
44+
*
45+
* @param sourceText the contents of the source file
46+
* @param sourcePath the path to the source file
47+
* @param jestConfig the jest configuration when called by Jest 26 and lower. This parameter is folded into
48+
* `transformOptions` when called by Jest 27+ as a top level `config` property. Calls to this function from Jest 27+
49+
* will have a `Jest27TransformOptions` shape
50+
* @param transformOptions an object containing the various transformation options. In Jest 27+ this parameter occurs
51+
* third in this function signature (and no fourth parameter is formally accepted)
52+
* @returns the transformed file contents if the file should be transformed. returns the original source otherwise
53+
*/
54+
process(
55+
sourceText: string,
56+
sourcePath: string,
57+
jestConfig: Jest26Config | Jest27TransformOptions,
58+
transformOptions?: Jest26Config
59+
): string {
60+
// TODO(STENCIL-306): Drop support for versions of Jest <27
61+
/**
62+
* As of Jest 27, `jestConfig` changes it's shape (as it's been moved into `transformOptions`). To preserve
63+
* backwards compatability, we allow Jest to pass 4 arguments and check the shape of the third and fourth arguments
64+
* to run Jest properly (in lieu of a global Jest version available to us). Support for this functionality will be
65+
* removed in a future major version of Stencil.
66+
*/
67+
if (isJest27TransformOptions(jestConfig)) {
68+
// being called from Jest 27+, backfill `transformOptions`
69+
transformOptions = jestConfig.config;
70+
}
71+
72+
if (shouldTransform(sourcePath, sourceText)) {
973
const opts: TranspileOptions = {
10-
file: filePath,
11-
currentDirectory: jestConfig.rootDir,
74+
file: sourcePath,
75+
currentDirectory: transformOptions.rootDir,
1276
};
1377

14-
const tsCompilerOptions: ts.CompilerOptions = this.getCompilerOptions(jestConfig.rootDir);
78+
const tsCompilerOptions: ts.CompilerOptions = getCompilerOptions(transformOptions.rootDir);
1579
if (tsCompilerOptions) {
1680
if (tsCompilerOptions.baseUrl) {
1781
opts.baseUrl = tsCompilerOptions.baseUrl;
@@ -36,34 +100,55 @@ export const jestPreprocessor = {
36100
return sourceText;
37101
},
38102

39-
getCompilerOptions(rootDir: string) {
40-
if (!this._tsCompilerOptions) {
41-
this._tsCompilerOptions = getCompilerOptions(rootDir);
103+
/**
104+
* Generates a key used to cache the results of transforming a file. This helps avoid re-processing a file via the
105+
* `transform` function unnecessarily (when no changes have occurred). The API for `getCacheKey` is described in the
106+
* ["Writing custom transformers"](https://jestjs.io/docs/code-transformation#writing-custom-transformers)
107+
* documentation on the jest site. Unfortunately, the URL is not versioned at the time of this writing. For
108+
* reference, the v27.2 docs were referenced (the most recent available).
109+
*
110+
* This function attempts to support several versions of Jest (v23 through v27). Support for earlier versions of Jest
111+
* will be removed in a future major version of Stencil.
112+
*
113+
* @param sourceText the contents of the source file
114+
* @param sourcePath the path to the source file
115+
* @param jestConfigStr a stringified version of the jest configuration when called by Jest 26 and lower. This
116+
* parameter takes the shape of `transformOptions` when called by Jest 27+.
117+
* @param transformOptions an object containing the various transformation options. In Jest 27+ this parameter occurs
118+
* third in this function signature (and no fourth parameter is formally accepted)
119+
* @returns the key to cache a file with
120+
*/
121+
getCacheKey(
122+
sourceText: string,
123+
sourcePath: string,
124+
jestConfigStr: string | Jest27TransformOptions,
125+
transformOptions?: Jest26CacheKeyOptions
126+
): string {
127+
// TODO(STENCIL-306): Remove support for earlier versions of Jest
128+
/**
129+
* As of Jest 27, jestConfigStr is no longer an accepted argument (as it's been moved into `transformOptions`). To
130+
* preserve backwards compatability, we allow Jest to pass 4 arguments and check the shape of the third and fourth
131+
* arguments to run Jest properly (in lieu of a global Jest version available to us). Support for this
132+
* functionality will be removed in a future major version of Stencil.
133+
*/
134+
if (isJest27TransformOptions(jestConfigStr)) {
135+
// being called from Jest 27+, backfill `transformOptions`
136+
transformOptions = jestConfigStr.config;
42137
}
43138

44-
return this._tsCompilerOptions;
45-
},
46-
47-
getCacheKey(
48-
code: string,
49-
filePath: string,
50-
jestConfigStr: string,
51-
transformOptions: { instrument: boolean; rootDir: string }
52-
) {
53-
// https://github.com/facebook/jest/blob/v23.6.0/packages/jest-runtime/src/script_transformer.js#L61-L90
54-
if (!this._tsCompilerOptionsKey) {
55-
const opts = this.getCompilerOptions(transformOptions.rootDir);
56-
this._tsCompilerOptionsKey = JSON.stringify(opts);
139+
if (!_tsCompilerOptionsKey) {
140+
const opts = getCompilerOptions(transformOptions.rootDir);
141+
_tsCompilerOptionsKey = JSON.stringify(opts);
57142
}
58143

59144
const key = [
60145
process.version,
61-
this._tsCompilerOptionsKey,
62-
code,
63-
filePath,
146+
_tsCompilerOptionsKey,
147+
sourceText,
148+
sourcePath,
64149
jestConfigStr,
65150
!!transformOptions.instrument,
66-
6, // cache buster
151+
CACHE_BUSTER,
67152
];
68153

69154
return key.join(':');
@@ -89,7 +174,16 @@ function formatDiagnostic(diagnostic: Diagnostic) {
89174
return m;
90175
}
91176

92-
function getCompilerOptions(rootDir: string) {
177+
/**
178+
* Read the TypeScript compiler configuration file from disk
179+
* @param rootDir the location to search for the config file
180+
* @returns the configuration, or `null` if the file cannot be found
181+
*/
182+
function getCompilerOptions(rootDir: string): ts.CompilerOptions | null {
183+
if (_tsCompilerOptions) {
184+
return _tsCompilerOptions;
185+
}
186+
93187
if (typeof rootDir !== 'string') {
94188
return null;
95189
}
@@ -115,10 +209,17 @@ function getCompilerOptions(rootDir: string) {
115209
tsconfigFilePath
116210
);
117211

118-
return parseResult.options;
212+
_tsCompilerOptions = parseResult.options;
213+
return _tsCompilerOptions;
119214
}
120215

121-
export function shouldTransform(filePath: string, sourceText: string) {
216+
/**
217+
* Determines if a file should be transformed prior to being consumed by Jest, based on the file name and its contents
218+
* @param filePath the path of the file
219+
* @param sourceText the contents of the file
220+
* @returns `true` if the file should be transformed, `false` otherwise
221+
*/
222+
export function shouldTransform(filePath: string, sourceText: string): boolean {
122223
const ext = filePath.split('.').pop().toLowerCase().split('?')[0];
123224

124225
if (ext === 'ts' || ext === 'tsx' || ext === 'jsx') {

0 commit comments

Comments
 (0)