ts-jest is not out-of-the-box compatible with Next.js, basically because in Next.js there is no need to add import React from "react" in every file.
That effectively means that in ts-jest when you import a .tsx file, all you get is 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
Work correctly out-of-the-box with Next.js
Test suite failed to run
TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
src/pages/index.tsx:2:13 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
2 return <div>hello world</div>
I made this repo using all the expected stuff for a testing environment using testing-library
https://github.com/PabloSzx/ts-jest-nextjs
Import React in all .tsx files, even if it's not needed for Next.js
Hi,
Would you please help us to investigate the issue ? We are at the moment lack of capacity to handle all community's requests. We really appreciate if you can help us by contributing to ts-jest.
I've been investigating and the automatic appending of import React from "react" in Next.js is from a custom plugin called jsx-pragma added to the Babel configuration here.
I've been trying to make it work inside ts-jest babel configuration through different options (comment and uncomment the alternatives), and I can't get it to work, I get errors like:
ReferenceError: [BABEL] ...\ts-jest-nextjs\__tests__\client.test.tsx: Unknown option: .value. Check out https://babeljs.io/docs/en/babel-core/#options for more information about options. when trying to use the default Next.js babel configuration preset[BABEL] ...\ts-jest-nextjs\__tests__\client.test.tsx: .value is not a valid Plugin property using the same options as mentioned in the documentation.plugins[0][0] must be a string, object, function trying to add the jsx-pragma custom pluginAny help would be appreciated
I checked your repo, a few modifications need to be done:
jest.config.js
module.exports = {
// preset: "@pablosz/ts-jest",
preset: 'ts-jest',
testEnvironment: 'jsdom',
globals: {
'ts-jest': {
tsConfig: '__tests__/tsconfig.json',
babelConfig: true,
},
},
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest',
},
};
remove .babelrc and replace with babel.config.js
module.exports = {
presets: [
'next/babel',
],
plugins: [
'@babel/plugin-transform-for-of',
[
require('./plugins/jsx-pragma'),
{
// This produces the following injected import for modules containing JSX:
// import React from 'react';
// var __jsx = React.createElement;
module: 'react',
importAs: 'React',
pragma: '__jsx',
property: 'createElement',
},
]
],
};
babel-jest dependencyHowever, I can't make it work.
$ jest
FAIL __tests__/client.test.tsx
● Test suite failed to run
TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
src/pages/index.tsx:2:13 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
2 return <div>hello world</div>
But this error looks slightly better. The errors you encountered are related to how to config ts-jest to work with babel
I've been investigating and the automatic appending of
import React from "react"in Next.js is from a custom plugin called jsx-pragma added to the Babel configuration here.If
Nextjsfully uses onlybabel, perhaps you can't usets-jestto achieve what Nextjs does. Probably you can only usejestwithbabel.
That means that ts-jest isn't able to work with babel and the ts-jest default config at the same time?
(Babel-jest doesn't work with decorators, but ts-jest does)
ts-jest can work with babel via transform config. But what NextJs does with import React from "react" is via babel plugin and they also handle transform ts files with babel (I suspect).
Because you are trying to achieve there is no need to add import React from "react" in every file., this can't be done without a custom typescript transformer (with ts-jest). Nextjs uses babel plugin to achieve that and babel plugin is an AST transformer (similar to custom typescript transformer).
If you want to use babel plugin jsx-pragma to achieve what Nextjs does, the only way is applying this plugin on all typescript files -> it means ts-jest isn't involved anymore. Regarding to issue with decorators, you can achieve that with another babel plugin babel-plugin-proposal-decorators. ts-jest default handles ts file by invoking ts compiler, that's why decorators work.
You can achieve the same thing like jsx-pragma by implementing a custom typescript transformer then declare that transformer in astTransformers config of ts-jest
Regarding to issue with decorators, you can achieve that with another babel plugin
babel-plugin-proposal-decorators.ts-jestdefault handlestsfile by invoking ts compiler, that's why decorators work.
Trust me, I've been fighting a long time to make work a fullstack project with NextJS - TypeGraphQL - TypeORM/Typegoose using a single compilation process using babel, and babel-plugin-proposal-decorators doesn't completely work 😥
You can achieve the same thing like
jsx-pragmaby implementing a custom typescript transformer then declare that transformer inastTransformersconfig ofts-jest
I've never made a TypeScript transformer before, I'll have to learn 😅
you can learn from jest-preset-angular. I hope it is clear for you now how to achieve what you need. I guess this issue can be closed ?
I mean, ts-jest still isn't out of the box compatible with Next.js, but if you prefer to close it meanwhile I work with the solution, it's ok
this is not the issue with ts-jest because NextJs is a framework and it applies a "magic" to achieve what you describe. Making ts-jest framework-dependent shouldn't be done.
@ahnpnl I came across this discussion because I have a similar issue. I am thinking of writing my own transformer as, but I cannot find any documentation on writing a typescript transformer for ts-jest. I mostly want to know how ts-jest imports the transformer? Do I need to make the transformer the default export from the file I specify in the astTransformers property in the jest.config.js?
Here is the example of a transformer: https://github.com/kulshekhar/ts-jest/blob/master/src/transformers/hoist-jest.ts
To define transformer for ts-jest: https://github.com/thymikee/jest-preset-angular/blob/c14d9a4f3d8840a590b5875d1f1e64e0dba0f930/jest-preset.js#L6
I just noticed that the documentation misses the configuration for ast transformers
@ahnpnl not sure if I should continue the discussion here but when I try to run a test with my new transformer I get the following error from jest,
(function (exports, require, module, __filename, __dirname) { import {
SyntaxError: Unexpected token {
It seems that it isn't liking that it's a typescript file? Here is my jest config.
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.svg$': '<rootDir>/svgTransform.js',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/*'],
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.jest.json',
astTransformers: ['<rootDir>/test-utils/ts-jest-astTransformer.ts'],
},
},
moduleDirectories: ['node_modules', '<rootDir>'],
moduleNameMapper: {
'^.+\\.svg$': '<rootDir>/fileMock.js',
'test-utils/reactTestingLibraryWrapper': '<rootDir>/test-utils/reactTestingLibraryWrapper',
},
};
Here is my astTransformer
import {
SourceFile,
Node,
TransformationContext,
Visitor,
isImportDeclaration,
createImportEqualsDeclaration,
updateSourceFileNode,
createExternalModuleReference,
createLiteral,
Transformer,
visitNode
} from "typescript";
function createVisitor(sf: SourceFile, _: TransformationContext): Visitor {
const hasReactImport = (node: Node): boolean => {
const nodeText = node.getText(sf);
const isReactImport = isImportDeclaration(node) &&
nodeText.includes("react");
let result = isReactImport;
node.forEachChild(child => {
result = result || hasReactImport(child);
});
return result;
}
const injectReactImport = (): SourceFile => {
return updateSourceFileNode(sf, [createImportEqualsDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
"React",
createExternalModuleReference(createLiteral("react"))
), ...sf.statements]);
};
return (node: Node) => {
if (hasReactImport(node)) return sf;
return injectReactImport();
};
}
export function factory() {
return (ctx: TransformationContext): Transformer<SourceFile> => (
sf: SourceFile
) => visitNode(sf, createVisitor(sf, ctx));
}
have you tried to compile your transformer to js ? I’m not sure transformer in ts will work.
@ahnpnl I went ahead and just converted it to a js file. What I am trying to do is inject a react import into modules that do not have one. Doesn't seem to be working. I get the following error
'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
When I print out nodes that are import declarations, after the transform, I can see the following
{
pos: -1,
end: -1,
flags: 8,
modifierFlagsCache: 536870912,
transformFlags: 536870912,
parent: undefined,
kind: 254,
decorators: undefined,
modifiers: undefined,
importClause:
NodeObject {
pos: -1,
end: -1,
flags: 8,
modifierFlagsCache: 536870912,
transformFlags: 536870912,
parent: undefined,
kind: 255,
name: [IdentifierObject],
namedBindings: undefined,
isTypeOnly: false },
moduleSpecifier:
TokenObject {
pos: -1,
end: -1,
flags: 8,
modifierFlagsCache: 0,
transformFlags: 536870912,
parent: undefined,
kind: 10,
text: 'react' } }
Which is the react import I am injecting. It would seem that the import is present in the Abstract Syntax tree but ts-jest is not seeing it?
is your transformer in commonjs ? Can you please paste the error stacktrace here ?
Ok here is the transformer
const {
createImportDeclaration,
createImportClause,
createIdentifier,
createLiteral,
isImportDeclaration,
updateSourceFileNode,
visitNode,
} = require('typescript');
function printImportDeclarationNodes(node, sf) {
const _isImportDeclaration = isImportDeclaration(node);
let result = _isImportDeclaration ? [node] : [];
node.forEachChild((child) => {
result = [...result, ...printImportDeclarationNodes(child, sf)];
});
return result;
}
function createVisitor(sf, _) {
// consle.log('********pre transform******\n', sf);
const hasReactImport = (node) => {
const nodeText = node.getText(sf);
const isReactImport = isImportDeclaration(node) && nodeText.includes('react');
let result = isReactImport;
node.forEachChild((child) => {
result = result || hasReactImport(child);
});
return result;
};
const injectReactImport = () => {
return updateSourceFileNode(sf, [
createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
createImportClause(createIdentifier('React'), undefined),
createLiteral('react')
),
...sf.statements,
]);
};
return (node) => {
if (hasReactImport(node)) return sf;
const file = injectReactImport();
console.log('********post transform******\n', printImportDeclarationNodes(file));
return file;
};
}
function factory() {
return (ctx) => (sf) => visitNode(sf, createVisitor(sf, ctx));
}
module.exports = { factory };
So the error is a typescript error. Sorry if that was misleading. Here is the output from jest
FAIL pages/web/brands.test.tsx
● Test suite failed to run
components/shared/landingDictionary/landingDictionaryHeader.tsx:69:3 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
69 <>
~~
components/shared/landingDictionary/landingDictionaryHeader.tsx:70:6 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
70 <StyledVerticalSpacer spacer />
~~~~~~~~~~~~~~~~~~~~
components/shared/landingDictionary/landingDictionaryHeader.tsx:71:6 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
71 <SubHeaderStyled>
~~~~~~~~~~~~~~~
components/shared/landingDictionary/landingDictionaryHeader.tsx:74:12 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
74 <InitialStyled key={key} onClick={(): void => onClick(key)}>
~~~~~~~~~~~~~
components/shared/landingDictionary/landingDictionaryHeader.tsx:75:14 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
75 <StyledH3>{key}</StyledH3>
~~~~~~~~
components/shared/landingDictionary/landingDictionaryHeader.tsx:80:6 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.
80 <StyledVerticalSpacer spacer />
~~~~~~~~~~~~~~~~~~~~
So the file where this error is happening does not import React, because it is a component in NextJs project. NextJs injects it at build time. That is why I want to inject a react import through using astTransformer.
Also I am getting the above error before I get any of the logs from the transformer. Wouldn't that mean tests are running before the transformer is even called?
@ahnpnl ^
@gabrielgrover I managed to setup a repo with jest, ts-jest for NextJs without the need of transformer https://github.com/ahnpnl/nextjs-with-jest-ts-jest
This repo makes it possible to use ts-jest for NextJs, but the original repo of this issue I can't get it to work.
I will check how to combine with your transformer to achieve the goal that no need to import react everywhere
@ahnpnl If you look at my PR I was able to make your test fail https://github.com/ahnpnl/nextjs-with-jest-ts-jest/pull/1
@ahnpnl this seems like a bug to me. Not sure if it is a bug with ts-jest or not.
ts-jest only supports generic testing with ts. If you import React everywhere you will have tests successfully (including import React in your ts as well as test file).
To achieve the purpose of not import React everywhere like NextJs does, the only way is to have the same transformer that NextJs does and ask ts-jest to use that transformer.
Most helpful comment
this is not the issue with
ts-jestbecause NextJs is a framework and it applies a "magic" to achieve what you describe. Makingts-jestframework-dependent shouldn't be done.