I am writing a React application using Vite and Typescript, with automated tests using Vitest. One of my components uses react-viewer, and looks something like this
import Viewer from "react-viewer";
import { useState } from "react";
type MyComponentProps = {
url: string
};
export const MyComponent = ({ url }: MyComponentProps) => {
const [isVisible, setVisible] = useState(false);
return (
<div>
/* Some other components above which toggle the viewer */
<Viewer
visible={isVisible}
onClose={() => setVisible(false)}
images={[{ src: url, alt: "", downloadUrl: url }]}
noNavbar
/>
</div>
);
};
This works when I run the application with the development server. However, in the automated tests, I get this error:
Warning: React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object.
When I logged Viewer when running the tests, I got this
Object [Module] { default: [Function (anonymous)] }
But when I looked at the same log statement when running it in development, I got this
function default(A3)
It looks like in development mode, Viewer is imported as a React component (as I'd expect). But in the test environment, it's incorrectly imported as an object.
I found this issue on Vite's Github, which has a workaround. I tried following the same pattern, which works
import React from "react";
import Viewer from "react-viewer";
import ViewerProps from "react-viewer/lib/ViewerProps";
type PictureViewerComponent = (props: ViewerProps) => React.ReactPortal;
type ViewerComponent = PictureViewerComponent & {
default?: PictureViewerComponent;
};
export const PictureViewer: PictureViewerComponent =
(Viewer as ViewerComponent)?.default ?? Viewer;
But I'm a bit worried that I'll see this issues with other components that use default imports. It feels like there is an underlying issue with either my tsconfig.json or vite.config.ts.
Here is the tsconfig.json
{
"exclude": [
"node_modules",
"build",
"coverage",
"storybook-static",
"public"
],
"compilerOptions": {
"types": ["vite/client", "vitest/globals"],
"paths": {
"#acme/*": ["./src/*"]
},
"allowJs": true,
"baseUrl": ".",
"typeRoots": ["./node_modules/#types"],
"target": "ES6",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "ES6",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noImplicitAny": true,
"noEmit": true,
"jsx": "react-jsx"
}
}
And here is the vite.config.ts
/// <reference types="vitest" />
import * as path from "path";
import react from "#vitejs/plugin-react";
import { defineConfig, splitVendorChunkPlugin } from "vite";
import eslint from "vite-plugin-eslint";
const config = () => {
return defineConfig({
base: "/acme/",
plugins: [react(), eslint(), splitVendorChunkPlugin()],
resolve: {
alias: [
{
find: /~(.+)/,
replacement: path.join(process.cwd(), "node_modules/$1")
},
{
find: /#acme\//,
replacement: path.join(process.cwd(), "./src") + "/"
},
{
// Workaround due to https://github.com/vitejs/vite/issues/9200
find: "path",
replacement: "path-browserify"
}
]
},
server: {
host: "0.0.0.0",
port: 80,
base: "/"
},
esbuild: {
loader: "tsx",
logOverride: { "this-is-undefined-in-esm": "silent" }
},
build: {
outDir: "build"
},
test: {
globals: true,
environment: "jsdom",
mockReset: true,
restoreMocks: true,
clearMocks: true,
setupFiles: "./src/setupTests.js",
coverage: {
reporter: ["text", "html"],
exclude: [
"node_modules/",
"src/setupTests.js",
"**/{__tests__,__stories__,test,storybook}/"
]
}
}
});
};
export default config;
I tried toggling the moduleResolution, esModuleInterop, and isolatedModules options in tsconfig.json, without success. I also tried setting test.deps.interopDefault to false in vite.config.ts, also without success.
I also tried importing the component like this, also without success
import { default as Viewer } from "react-viewer";
Any help would be appreciated! Thank you in advance
Related
I'm integrating vitest with a NextJS13 app, but running into problems with a simple test run.
Not sure what the problem is, I tried to do some tweaking with the vitest.config.ts but no luck. I tried adding the dir option, modified the include option to grab files from the source file but no luck.
I thought maybe it had to do with the tsconfig.json file, but it's still outputting the error.
This is the directory of the file
Here are the files in question:
vitest.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'
import react from '#vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
setupFiles: 'setupTests.ts',
// dir: './src'
// includeSource: ['src/**/*.{js,ts,tsx}'],
},
});
tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["es6", "dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "ESNEXT",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"incremental": true,
// "paths": {
// "src": ["./src/*"]
// }
},
"exclude": ["node_modules"],
"include": ["vitest.config.ts","**/*.ts", "**/*.tsx", "next-env.d.ts",
"next.config.js"]
}
DataTable.test.tsx - src/common/components/DataTable/DataTable.test.tsx
// components
import DataTable from 'src/common/components/DataTable';
// dependencies
import {describe, it} from 'vitest'
import {screen, render} from '#testing-library/react'
describe('DataTable test', () => {
it('render the app', () => {
// arrange
render(<DataTable />)
// act
const assetText = screen.getByText("asset")
// assert
// expect(assetText).toBeInTheDocument()
})
})
DataTable component - src/common/components/DataTable/DataTable.tsx
export const DataTable = () => {
return (
<div>
<h1>assets</h1>
</div>
);
};
Index.tsx - src/common/components/DataTable/index.tsx
import { DataTable } from 'src/common/components/DataTable/DataTable';
export default DataTable;
I'm new to vitest and nextjs, your help/guidance will be appreciated.
There are two things needed here to make the import DataTable from 'src/common/components/DataTable'; import work:
TypeScript needs the paths compilerOption set.
Vite needs to have the same alias set.
The "paths" compilerOption in TypeScript will need a /* on the end of the "src" key to be able to resolve paths underneath the "src" directory (see tsconfig.json reference):
{
"compilerOptions": {
"paths": {
"src/*": ["./src/*"]
}
}
}
Vite/Vitest will also need to know how to resolve "src/common/components/DataTable", and that would usually be done with the resolve.alias setting, but rather than duplicating the alias here, you could also use a plugin, like vite-tsconfig-paths, to add the path aliases if finds in relevant tsconfig.json files:
import react from "#vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default {
plugins: [tsconfigPaths(), react()],
};
So I have a custom private npm package which I want to dynamically imports it like so.
export default function SelectedIcon({path, ...props}: Props) {
const {data: icon} = require(`#mypackagelib/icons/${path}`)
return <Icon icon={icon} {...props} />
}
// I also tried to have it differently such as:
export default function SelectedIcon({path, ...props}: Props) {
const Component = React.lazy(() => import(`#mypackagelib/icons/${path}`))
return <Component {...props} />
}
// However, same error, so it seems like dynamically importing this is the problem
However, I got many errors, all the same message for different components in the library, from running Storybook
Module parse failed: Unexpected token (2:101)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| /// <reference types="react" />
| import { PropA, PropB } from '../../File';
> declare function NameOfAComponent(props: PropA): JSX.Element;
| declare namespace NameOfAComponent {
| var data: PropB;
The weird thing that I don't understand is that, if I import the package like the below code, then it works
import OneOfTheComponent from '#mypackagelib/icons/OneOfTheComponent'
However, I have a case that I need to use dynamic import
I have the following config tsconfig.json when I build the #mypackagelib package with tsc and ts-node
{
"compilerOptions": {
"outDir": "dist",
"target": "ES2020",
"module": "commonjs",
"jsx": "react",
"lib": ["es6", "dom", "ES2020"],
"moduleResolution": "node",
"declaration": true,
"strict": true,
"skipLibCheck": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": false,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmitHelpers": true,
"importHelpers": true,
"pretty": false,
},
"ts-node": {
"swc": true
},
"exclude": [
"node_moduls_local",
"node_modules",
"dist"
],
"include": [
"types.d.ts",
"src/icons/**/*"
]
}
My main.js under ./storybook folder looks like this:
module.exports = {
stories: [
"../src/**/*.stories.#(ts|tsx)"
],
addons: [
"#storybook/addon-links",
"#storybook/addon-essentials",
"#storybook/addon-interactions",
"#storybook/preset-create-react-app",
{
name: 'storybook-addon-swc', // I thought adding this will works, but it doesn't
options: {
enable: true,
enableSwcLoader: true,
enableSwcMinify: true,
},
},
],
framework: "#storybook/react",
core: {
builder: 'webpack5',
},
experiments: {
topLevelAwait: true,
}
}
I think this might be webpack problem in general. What can I do to make it work? What loader do I need to add? What do my main.js file needs to look like? Why it needs a loader when I use dynamic imports but works fine if it's a normal import?
I was getting this error when import svg in React/Typescript/Webpack 5 project:
Cannot find module '../path' or its corresponding type declarations.
then I added:
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement> & { title?: string }>;
const src: string;
export default src;
}
to the custom.d.ts file. It works if I use the svg as an image src. But I need to use the svg as an ReactElement because I need to change its content based on user clicks.
I tried to import svg as
import DislikeIcon from '../../media/icons/dislike.svg';
const Component = () => (<> <DislikeIcon /> </>)
then I got this error:
<data:image/svg... /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.
then I tried:
import { ReactComponent as DislikeIcon } from '../../media/icons/dislike.svg';
and got:
Warning: React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
My webpack.config.js file
module.exports = {
...
module: {
rules: [
{
test: /\.[jt]sx?$/,
use: ['babel-loader'],
exclude: /node_modules/,
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'],
},
{
test: /\.svg$/,
loader: 'url-loader',
},
{
test: /\.(?:ico|gif|png|jpg|jpeg)$/i,
type: 'asset/resource',
},
{
test: /\.(woff(2)?|eot|ttf|otf)$/,
type: 'asset/inline',
},
],
},
...
};
my custom.d.ts file:
declare module '*.png' {
const value: string;
export = value;
}
declare module '*.jpg' {
const value: string;
export = value;
}
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement> & { title?: string }>;
const src: string;
export default src;
}
my tsconfig.json file:
{
"compilerOptions": {
"target": "ES5",
"module": "ESNext",
"moduleResolution": "node",
"lib": [
"DOM",
"ESNext"
],
"jsx": "react-jsx",
"noEmit": true,
"isolatedModules": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowJs": true,
"checkJs": true,
},
"include": ["src/**/*", "./custom.d.ts"],
"exclude": ["node_modules", "build"],
}
I searched a lot for an answer, but everyone says something similar. This is taking me a time I don't have. Does anyone have any suggestion?
Thank you for you attention!
I found the solution for this here: https://duncanleung.com/typescript-module-declearation-svg-img-assets/
With thanks to (Duncan Leung)
The solution was to create a ./src/#types/assets/index.d.ts TypeScript module declaration file for media assets.
One main gotcha about TypeScript module declaration files is in how they are included in tsconfig.json using the typeRoots property.
The property typeRoots defines the types folder where type declarations will be contained, but the index.d.ts module declaration files must be in a subfolder since each subfolder under typeRoots is considered a "package" and is added to your project.
We were incorrectly trying to place the .svg module declarations under ./src/#types/index.d.ts.
Moving the index.d.ts for .svg files to it's own ./src/#types/assets subfolder allowed TypeScript to correctly recognize the .svg module declarations.
./src/#types/assets/index.d.ts
declare module "\*.svg" {
import React = require("react");
export const ReactComponent: React.SFC<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}
declare module "\*.jpg" {
const content: string;
export default content;
}
declare module "\*.png" {
const content: string;
export default content;
}
declare module "\*.json" {
const content: string;
export default content;
}
Here is an example tsconfig.json:
tsconfig.json
{
"compileOnSave": false,
"compilerOptions": {
"target": "es5",
"module": "esnext",
"types": ["node"],
"moduleResolution": "node",
"esModuleInterop": true,
"typeRoots": ["./src/#types", "./node_modules/#types"],
"lib": ["dom", "es2015", "es2017"],
"jsx": "react",
"sourceMap": true,
"strict": true,
"resolveJsonModule": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"baseUrl": "./",
"paths": {
"~/*": ["src/*"],
"#turn/styled": ["src/styled"]
}
},
"include": ["./src/**/*", "./test-utils/**/*", "./__mocks__/**/*"],
"exclude": ["node_modules"]
}
I'm trying to configure storybook to display a dark theme, and so far i didn't find any solution to this problem.
so i followed the storybook docs,
and i'v setup the manager.js file like so:
// .storybook/manager.js
import { addons } from '#storybook/addons';
import { themes } from '#storybook/theming';
addons.setConfig({
theme: themes.dark,
});
i'v also printed the theme to the console so i see it arrives:
it may be worth mentioning that when the browser reload this file is read,
but if i change the source code and save the hot-reload don't work..
Here's how i specified the same theme for docs in .storybook/preview.js:
// .storybook/preview.js
import React from "react";
import { appTheme } from "../src/Common/theme";
import { ThemeProvider } from "styled-components";
import { makeDecorator } from "#storybook/addons";
import { addParameters, addDecorator } from "#storybook/react";
import defaultNotes from "./general-docs.md";
import { themes } from "#storybook/theming";
export const parameters = {
docs: {
theme: themes.dark
}
};
addParameters({
notes: defaultNotes,
options: {
showRoots: true
}
});
const withStyledTheme = storyFn => {
return <ThemeProvider theme={appTheme}>{storyFn()}</ThemeProvider>;
};
const styledThemed = makeDecorator({
name: "styled-theme",
wrapper: withStyledTheme
});
addDecorator(styledThemed);
addParameters(parameters);
this is how the main.js file looks like:
module.exports = {
stories: ["../src/**/*.stories.(ts|tsx|js|jsx|mdx)"],
addons: [
"#storybook/preset-create-react-app",
"#storybook/addon-actions",
"#storybook/addon-links",
"#storybook/addon-actions/register",
"#storybook/addon-knobs/register",
"#storybook/addon-notes/register-panel",
"storybook-addon-designs",
"#storybook/addon-docs/preset"
]
};
i work with typescript in this project,
so here is the tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"baseUrl": "src"
},
"include": ["src"]
}
What am i missing here ?
I've been stuck on the same problem for a while, but the solution was in the doc all along:
//.storybook/preview.js
import { themes } from '#storybook/theming';
export const parameters = {
docs: {
theme: themes.dark,
},
};
So yeah, you have to specify the theme twice, in two different files
This is a confusing bug, I'm coming back to frontend work after a hiatus of over an year.
Setup and configuration
I am experimenting with rollup and react-runkit to create some docs for my backend typescript project.
Typescript config
{
"compilerOptions": {
"allowUnreachableCode": false,
"alwaysStrict": true,
"declaration": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "esnext"],
"jsx": "react",
"module": "commonjs",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitThis": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"sourceMap": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"target": "es5"
},
"include": ["src/docs", "typings"],
"exclude": ["node_modules"]
}
Rollup config
import typescript from '#rollup/plugin-typescript';
import resolve from '#rollup/plugin-node-resolve';
import replace from '#rollup/plugin-replace';
export default {
input: 'src/docs/index.tsx',
output: [
{
file: 'src/docs/dist/bundle.js',
format: 'umd'
}
],
plugins: [
typescript({
tsconfig: './tsconfig.docs.json'
}),
resolve(),
replace({
'process.env.NODE_ENV': JSON.stringify('development')
})
]
}
The React module in question
import * as React from 'react';
import * as ReactDom from 'react-dom';
import * as Embed from 'react-runkit';
function App() {
const source = `console.log('Hello World');`;
return (
<div>
<Embed source={source} />
</div>
);
}
console.log('Hello, World');
const app = document.getElementById('app');
ReactDom.render(<App />, app);
READ THIS (THE ERROR, THE CONCERNING PART) in the bundled output:
function App() {
var source = "console.log('Hello World');";
return (undefined("div", null,
undefined(Embed, { source: source })));
}
console.log('Hello, World');
var app = document.getElementById('app');
undefined(undefined(App, null), app);
After a bit of digging, I find that when I include:
Option 1: Includes the React Source but not the JSX transform:
plugins: [
resolve(),
replace({
'process.env.NODE_ENV': JSON.stringify('development')
}),
commonjs(),
typescript({
jsx: 'react',
}),
]
gives me the react source in the bundle.js but leaves the <div> as undefined.
Option 2: Includes the React transform but not the react source:
plugins: [
resolve(),
replace({
'process.env.NODE_ENV': JSON.stringify('development')
}),
commonjs(),
typescript({
jsx: 'react',
module: 'CommonJS'
}),
]