NextJS turn a .MDX into a component - reactjs

I am trying to create an MDX blog and integrate it into my project. I currently have most of the setup done but I cannot for the life of me get MDX file into a component to render between my Layout. Here's my issue, all of the examples showcase something weird, for example,
MDX docs aren't typed, and I cannot get this to work in typescript
export default function Page({code}) {
const [mdxModule, setMdxModule] = useState()
const Content = mdxModule ? mdxModule.default : Fragment
useEffect(() => {
;(async () => {
setMdxModule(await run(code, runtime))
})()
}, [code])
return <Content />
}
And Nextjs just advises to put mdx files under /pages/, which doesn't fit my usecase
What I want:
Have an MDX file with Frontmatter YAML metadata
Load that MDX file into a component and display it
What I have currently
Nextjs config
import mdx from '#next/mdx';
import remarkFrontmatter from 'remark-frontmatter';
import remarkGfm from 'remark-gfm';
/** #type {import('next').NextConfig} */
const config = {
eslint: {
dirs: ['src'],
},
reactStrictMode: true,
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'tsx', 'ts'],
images: {
domains: [
'res.cloudinary.com',
'picsum.photos', //TODO
],
},
// SVGR
webpack: (config, options) => {
config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: [
{
loader: '#svgr/webpack',
options: {
typescript: true,
icon: true,
},
},
],
});
config.module.rules.push({
test: /\.mdx?$/,
use: [
options.defaultLoaders.babel,
{
loader: '#mdx-js/loader',
options: {
// providerImportSource: '#mdx-js/react',
remarkPlugins: [remarkFrontmatter, remarkGfm],
},
},
],
});
return config;
},
};
export default config;
My projects/[slug].tsx file
import { GetStaticPaths, GetStaticProps } from 'next';
import { getFileBySlugAndType, getFiles } from '#/lib/mdx/helpers';
import { Layout } from '#/components/layout/Layout';
import Seo from '#/components/Seo';
import * as runtime from 'react/jsx-runtime.js'
import { ProjectFrontMatter } from '#/types/frontmatter';
import { useEffect, useState } from 'react';
import {compile, run} from '#mdx-js/mdx'
import { MDXContent } from 'mdx/types';
type SingleProjectPageProps = {
frontmatter: ProjectFrontMatter;
content: string;
};
export default function SingleProjectPage({
frontmatter,
content,
}: SingleProjectPageProps) {
return (
<Layout>
<Seo
templateTitle={frontmatter.name}
description={frontmatter.description}
date={new Date(frontmatter.publishedAt).toISOString()}
/>
<main>
<section className=''>
<div className='layout'>
<div className='mt-8 flex flex-col items-start gap-4 md:flex-row-reverse md:justify-between'>
{/* <CustomLink
href={`https://github.com/theodorusclarence/theodorusclarence.com/blob/main/src/contents/projects/${frontmatter.slug}.mdx`}
>
Edit this on GitHub
</CustomLink>
<CustomLink href='/projects'>← Back to projects</CustomLink> */}
</div>
</div>
</section>
</main>
</Layout>
);
}
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getFiles('projects');
return {
paths: posts.map((p) => ({
params: {
slug: p.replace(/\.mdx/, ''),
},
})),
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const slug = params!['slug'] as string;
const { data, content } = getFileBySlugAndType(slug, 'projects');
return {
props: { frontmatter: data as ProjectFrontMatter, content },
};
};
And my helper function
export const getFileBySlugAndType = (slug: string, type: ContentType) => {
const file = readFileSync(
join(process.cwd(), 'src', 'content', type, slug + '.mdx'),
'utf-8'
);
const { data, content } = matter(file);
return { data, content };
};
I get as props in my SingleProjectPage, frontmatter data, and a string of the rest of the MDX content, which is correct, but I need to turn this string into MDX component. One of the libraries that does this is MDX-Bundler, but it hasn't been updated this year, and I'd prefer to use mdx-js if possible as it just release 2.0 version.

Related

(Gatsby) How to pass image source as a prop in MDX component

I'm trying to create a Figure component in which I pass the img src along with other data.
I realize it's not straightforward—e.g., this thread—but I thought it would work fine with normal HTML img tags.
The image is not displaying. Does that mean that this limitation also applies to HTML img tags within components?
If this is the case, I guess I indeed ought to use Gatsby's dynamic images. How would I do this in a static query (nonpage component)? This thread had me believing it isn't possible—or at least a hack?
The component inside MDX documents:
<Figure
image=""
size=""
caption=""
credits=""
/>
Figure.js:
import * as React from "react"
const Figure = ({ image, size, caption, credits }) => {
return (
<figure className={size}>
<img src={image} alt={caption} />
<figcaption>
<span>{caption}</span>
<span>{credits}</span>
</figcaption>
</figure>
)
}
export default Figure
articlePostTemplate.js
import * as React from "react"
import { graphql } from "gatsby"
import { MDXRenderer } from "gatsby-plugin-mdx"
import Layout from "../components/layout.js"
import Seo from "../components/seo.js"
const PostTemplate = ({ data, location }) => {
let post = data.mdx
return (
<Layout location={location}>
<Seo
title={post.frontmatter.title}
description={post.frontmatter.lead}
date={post.frontmatter.computerDate}
/>
<article className="post">
<header className="post" id="intro">
<p className="date">
<time dateTime={post.frontmatter.computerDate}>{post.frontmatter.humanDate}</time>
</p>
<h1 itemprop="headline">{post.frontmatter.title}</h1>
<p className="lead">{post.frontmatter.lead}</p>
</header>
<section className="post" id="body-text">
<MDXRenderer data={data}>{post.body}</MDXRenderer>
</section>
</article>
</Layout>
)
}
export default PostTemplate
export const pageQuery = graphql`
query PostBySlug(
$id: String!
) {
site {
siteMetadata {
title
}
}
mdx(id: { eq: $id }) {
id
excerpt(pruneLength: 160)
body
frontmatter {
title
computerDate: date(formatString: "YYYY-MM-DD")
humanDate: date(formatString: "D. MMMM YYYY", locale: "nb")
hook
type
lead
featuredImage {
childImageSharp {
fluid(maxWidth: 800) {
...GatsbyImageSharpFluid
}
}
}
}
}
}
`
gatsby-config.js
module.exports = {
…
},
plugins: [
`gatsby-plugin-image`,
`gatsby-plugin-sitemap`,
{
resolve: `gatsby-plugin-mdx`,
options: {
extensions: [`.md`, `.mdx`],
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-images`,
options: {
maxWidth: `900000000000`,
linkImagesToOriginal: false,
backgroundColor: `none`,
},
},
{
resolve: `gatsby-remark-responsive-iframe`,
options: {
wrapperStyle: `margin-bottom: 1.07var(--line-length)`,
},
},
`gatsby-remark-prismjs`,
`gatsby-remark-copy-linked-files`,
`gatsby-remark-smartypants`,
{
resolve: `gatsby-remark-autolink-headers`,
options: {
icon: false,
itemprop: `heading`,
maintainCase: false,
removeAccents: true,
elements: [`h2`, `h3`, `h4`],
},
}
],
},
},
…
{
resolve: `gatsby-source-filesystem`,
options: {
name: `content`,
path: `${__dirname}/content/`,
}
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/data`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/images`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/pages/`,
}
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `journalistikk`,
path: `${__dirname}/content/journalism/`,
}
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `discussion`,
path: `${__dirname}/content/discussion/`,
}
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `photography`,
path: `${__dirname}/content/photography/`,
}
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `Gatsby Starter Blog`,
short_name: `GatsbyJS`,
start_url: `/`,
background_color: `#ffffff`,
display: `minimal-ui`,
icon: `src/images/gatsby-icon.png`,
},
},
`gatsby-plugin-react-helmet`,
],
}
gatsby-node.js
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
const articlePostTemplate = path.resolve(`./src/templates/articlePostTemplate.js`)
const result = await graphql(
`
{
allMdx(
sort: { fields: [frontmatter___date], order: ASC }
limit: 1000
) {
nodes {
id
frontmatter {
title
computerDate: date(formatString: "YYYY-MM-DD")
humanDate: date(formatString: "D. MMMM YYYY", locale: "nb")
}
fields {
slug
}
}
}
}
`
)
if (result.errors) {
reporter.panicOnBuild(
`There was an error loading your blog posts`,
result.errors
)
return
}
const posts = result.data.allMdx.nodes
if (posts.length > 0) {
posts.forEach((post, index) => {
[index + 1].id
createPage({
path: post.fields.slug,
component: articlePostTemplate,
context: {
id: post.id
},
})
})
}
}
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `Mdx`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
createTypes(`
type SiteSiteMetadata {
author: Author
siteUrl: String
social: Social
}
type Author {
name: String
summary: String
}
type Social {
twitter: String
instagram: String
mail: String
}
type MarkdownRemark implements Node {
frontmatter: Frontmatter
fields: Fields
}
type Frontmatter {
title: String
description: String
date: Date #dateformat
}
type Fields {
slug: String
}
`)
}
layout.js
import * as React from "react"
import { MDXProvider } from "#mdx-js/react"
import { Link } from "gatsby"
import DWChart from "react-datawrapper-chart"
import Header from "./Header"
import Footer from "./Footer"
import Figure from "./Figure"
const shortcodes = { Link, DWChart, Figure }
export default function Layout({ children }) {
return (
<div className="layout-wrapper">
<Header />
<main>
<MDXProvider components={shortcodes}>{children}</MDXProvider>
</main>
<Footer />
</div>
)
}
The limitation you mention in How to pass a path of image as a prop in Gatsby in Gatsby-plugin-image only applies to StaticImage + dynamic props data (with a dynamic source), meaning that the component that returns a StaticImage cannot receive the src as a props, because all the props of StaticImage needs to be statically analyzed.
In other words, your MDX can receive a src to be used in a <img> tag or you can use GatsbyImage component if properly configured.
Keep in mind that the query that will fetch GatsbyImage data (childImageSharp, gatsbyImageData, etc) must be placed in a top-level component (pages) if using a page query or in a useStaticQuery hook if used elsewhere.
The approach in both scenarios (using <img> or GatsbyImage) is similar. You need to:
Provide your image sources in your markdown file
Query those images using a page query or useStaticQuery to provide to your MDXProvider the queried image props. Using the blog example:
<MDXProvider>
<MDXRenderer data={data}/>
</MDXProvider>
data stands for your queried data in a GraphQL page query or useStaticQuery. Without knowing your data structure I haven't add the GraphQL part because I do not know the nodes available.
At this point, your MDXProvider holds the images data (all from your markdown files), you just need to provide it to your GatsbyImage or Figure component:
// some.mdx
import Figure from 'path/to/figure/component';
## Post Body
Lorem ipsum dolor...
<Figure img={props.data} />
As you can see, I lift all props to Figure just to allow to debug (using console.logs() for example) to see the available props there like:
const Figure = (props) => {
console.log(props);
return (
<div>I will be a figure in a future</div>
)
}
export default Figure
In that way, you will be able to pass to Figure something like img={props.someNode.frontmatter.imageNodeSource}.
Again, without knowing your data structure is like a pie in the sky but get the idea.
Hat tip to Ferran for helpful guidance.
After more research, I revised my solution—
articleTemplate.js
/* shortcodes */
const ArticleTemplate = ({ data, location }) => {
let post = data.mdx
return (
<Layout location={location}>
<Seo
title={post.frontmatter.title}
description={post.frontmatter.lead}
date={post.frontmatter.computerDate}
/>
<article className="article">
<p className="date">
<time dateTime={post.frontmatter.computerDate}>{post.frontmatter.humanDate}</time>
</p>
<h1 itemprop="headline">{post.frontmatter.title}</h1>
<p className="lead" itemprop="introduction">{post.frontmatter.lead}</p>
<MDXProvider components={shortcodes}>
<MDXRenderer
data={post.frontmatter.thumbnail}
localImages={post.frontmatter.embeddedImagesLocal}
>
{post.body}
</MDXRenderer>
</MDXProvider>
</article>
</Layout>
)
}
export default ArticleTemplate
export const pageQuery = graphql`
query ArticleBySlug($id: String!) {
site {
siteMetadata {
title
}
}
mdx(id: {eq: $id}) {
id
excerpt(pruneLength: 160)
body
frontmatter {
title
computerDate: date(formatString: "YYYY-MM-DD")
humanDate: date(formatString: "D. MMMM YYYY", locale: "nb")
hook
type
lead
thumbnail {
childImageSharp {
gatsbyImageData(
layout: FULL_WIDTH
)
}
}
embeddedImagesLocal {
childImageSharp {
gatsbyImageData
}
}
}
}
}
`
figure.js
import * as React from "react"
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
const Figure = ({ source, size, caption, credit }) => {
return (
<figure className={size}>
<GatsbyImage image={getImage(source)} alt={caption} />
<figcaption>
<span>{caption}</span>
<span>{credit}</span>
</figcaption>
</figure>
);
}
export default Figure
index.mdx
---
…
thumbnail: "thumb.jpeg"
embeddedImagesLocal:
- "first.jpeg"
- "second.jpeg"
---
<Figure
source={(props.localImages [0])} <!-- first image; second image would be `[1]` -->
size="I'm a `className`"
caption="I'm a caption"
photographer="I'm a name."
/>
(Marking this as the solution as it's the most helpful for anyone looking to do this in the future. It also shows how to query for embedded images and featured images—at the same time.)

Exclude react-quill from vendor and bundle size

import React from 'react';
import clsx from 'clsx';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import styles from './styles.scss';
interface Props {
label: string;
value: any;
className?: string;
inputProps: {
onChange: (e: any) => void;
};
}
const RichText = ({ value = '', className, inputProps, label }: Props) => {
const { onChange } = inputProps;
const modules = {
toolbar: [['bold', 'italic', 'underline'], [{ align: [] }]],
};
const formats = ['bold', 'italic', 'underline'];
return (
<div>
<label>{label}</label>
<ReactQuill
value={value}
onChange={onChange}
formats={formats}
modules={modules}
className={clsx(styles.root, className)}
/>
</div>
);
};
export default RichText;
Above you can see my rich-text component , where I user react-quill npm package. I use it only in 1 place in my code , but it add 50-60 kb to my bundle size and it annoying me. I've tried to load it dynamically by doing
const ref = useRef()
useEffect(() => {
import('react-quill').then(data => {
ref.current = data.default
})
}, [])
const ReactQuill = ref.current
But it still sit in my bundle size. I've tried to load it by external url by this hook
import { useState } from 'react'
import { useMountEffect } from 'hooks'
const useExternalLibrary = ({ url, libName }) => {
const [lib, setLib] = useState({})
const fetchJsFromCDN = (src, externals = []) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.setAttribute('src', src)
script.addEventListener('load', () => {
resolve(
externals.map(key => {
return window[key]
})
)
})
script.addEventListener('error', reject)
document.body.appendChild(script)
})
}
useMountEffect(() => {
fetchJsFromCDN(url, [libName]).then(([library]) => {
setLib(library)
})
})
return {
lib
}
}
export default useExternalLibrary
Where you can pass url and how it should be called in global space, url is - https://unpkg.com/react-quill#1.3.3/dist/react-quill.js , but It throw error, that you should have for a start React in global , then it is broken by prop-types library , I don't use it in my project, etc . And I have no idea what else should I try to prevent it be in my bundle size , and load it only when I need it
optimization: {
minimize: true,
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
minimizer: [
new TerserPlugin({
extractComments: false,
}),
],
},
Above you can also see webpack optimization configuration, and also i've tried to wrap it to lazy
const ReactQuill = lazy(() => import('react-quill'));
As per your current webpack configuration, webpack is spitting out all modules inside node_modules as a single chunk named vendor. This is why you are not able to achieve the lazy loading for react-quill.
If you want to create a separate chunk for react-quill, you can modify splitChunks so that it creates a separate chunk for quill and react-quill modules:
splitChunks: {
cacheGroups: {
reactQuillVendor: {
test: /[\\/]node_modules[\\/](quill|react-quill)[\\/]/,
name: 'reactQuillVendor',
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
},
},
},

How to properly render images using a map function with Gatsby.js

This app was originally built using react, however we have decided to convert everything over and use Gatsbyjs. I am new to Gatsby and I am trying to get my images to render correctly using the artist data.
Here is how this part of the data was originally built:
const images = [
AngelAndJaamiHeadshot,
BillyHawkinsHeadshot,
BostonWellsHeadshot,
BreanahAlvizHeadshot,
CarloCollantesHeadshot,
CarloDarangHeadshot,
ChrisLumbaHeadshot,
ChrisMartinHeadshot,
CJDelaVegaHeadshot,
CyeBongalosHeadshot,
DanielKimHeadshot,
DavidDarkieSimmonsHeadshot,
// DavidDiosoHeadshot,
DavidSlaneyHeadshot,
DavinLawsonHeadshot,
DevinHeadshot,
DustinYuHeadshot,
EmilyRowanHeadshot,
HonrieDualanHeadhot,
HughAparenteHeadshot,
JaamiWaaliVillalobosHeadshot,
JeremyBorjaHeadshot,
JonathanSisonHeadshot,
JordanBautistaHeadshot,
JordanRileyHeadshot,
JuliaKestnerHeadshot,
JustinArcegaHeadshot,
KaitlynSungHeadshot,
KaylarPrieteHeadshot,
KeyanaReedHeadshot,
KikoJamesHeadshot,
KirstieAndJeremyHeadshot,
KirstieHeadshot,
KJEstudilloHeadshot,
LarkinPoyntonHeadshot,
MitchVillarealHeadhsot,
MoanaRakanaceHeadshot,
NoelleFrancoHeadshot,
PhuongLeHeadshot,
SamMooreHeadshot,
TonyRayHeadshot,
TracySeilerHeadshot,
TrishaOcampoHeadshot,
YutaNakamuraHeadshot,
defaultHeadshot,
]
export const buildArtistsData = (artists) => {
return artists.map((artist, idx) => {
return {
...artist,
imageUrl:
idx >= 43
? defaultHeadshot
: images[`${artist.firstName}${artist.lastName}Headshot`],
}
})
}
And this is how it was used in my Artists component:
const ArtistsPage = () => {
const artists = buildArtistsData(ARTISTS)
...
<div className={classes.flexContainer}>
{artists
.map(
(
{ city, currentTeam, firstName, lastName, imageUrl },
idx: number
) => {
return (
<div className={classes.flexItem} key={idx}>
<img
className={classes.artistCardImg}
src={imageUrl}
alt='artist-image'
/>
<div className={classes.artistCardName}>
{`${firstName} ${lastName}`.toUpperCase()}
</div>
<div className={classes.artistCardText}>{city}</div>
<div className={classes.artistCardText}>{currentTeam}</div>
</div>
)
}
)}
</div>
But now that I am using Gatsbyjs and data none of the above will work anymore. Here is what I am working with on the converted Gatsbyjs page:
import React from 'react'
import PropTypes from 'prop-types'
import { StaticImage } from 'gatsby-plugin-image'
import Img from 'gatsby-image'
import { graphql } from 'gatsby'
import { useStyles } from './styles'
const ArtistsPage = ({ data }) => {
console.log(data)
const classes = useStyles()
// const { images } = props
return (
<section>
<article className={classes.artistsContainer}>
<div className={classes.flexContainer}>
{data.allArtistsJson.edges.map(({ node }, idx) => {
return (
<div className={classes.flexItem} key={idx}>
<div>
{images.map((img, idx) => (
<Img
key={idx}
fluid={img.node.childImageSharp.fluid}
/>
))}
</div>
<div className={classes.artistCardName}>
{`${node.firstName} ${node.lastName}`.toUpperCase()}
</div>
<div className={classes.artistCardText}>{node.city}</div>
<div className={classes.artistCardText}>{node.currentTeam}</div>
</div>
)
})}
</div>
</article>
</section>
)
}
export const pageQuery = graphql`
query {
headshots: allFile(filter: { absolutePath: { regex: "/headshots/" } }) {
edges {
node {
childImageSharp {
fluid(maxWidth: 600) {
...GatsbyImageSharpFluid
}
}
}
}
}
}
`
ArtistsPage.propTypes = {
firstName: PropTypes.string,
lastName: PropTypes.string,
currentTeam: PropTypes.string,
headshots: PropTypes.string,
dropdown: PropTypes.string,
data: PropTypes.array,
images: PropTypes.string,
}
export default ArtistsPage
I was trying to pull image data as props using
const { image } = props - but that throws an error so I am really confused as to what and how to map over this to pull my images in for the correct artist.
Also here is my config.js file for reference:
const path = require('path')
module.exports = {
siteMetadata: {
title: 'Platform Showcase',
},
plugins: [
'gatsby-plugin-gatsby-cloud',
'gatsby-plugin-image',
// {
// resolve: "gatsby-plugin-google-analytics",
// options: {
// trackingId: "",
// },
// },
'gatsby-plugin-react-helmet',
'gatsby-plugin-sitemap',
{
resolve: 'gatsby-plugin-manifest',
options: {
icon: 'src/images/icon.png',
},
},
'gatsby-plugin-mdx',
'gatsby-plugin-sharp',
'gatsby-transformer-sharp',
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'images',
path: './src/images/',
},
__key: 'images',
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `headshots`,
path: `${__dirname}/src/images/artists/headshots`,
},
__key: 'headshots',
},
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'pages',
path: './src/pages/',
},
__key: 'pages',
},
{
resolve: 'gatsby-plugin-google-fonts',
options: {
fonts: ['material icons', 'roboto:300,400,500,700'],
},
},
`gatsby-theme-material-ui`,
`gatsby-transformer-json`,
{
resolve: `gatsby-source-filesystem`,
options: {
path: `./src/data/`,
},
},
{
resolve: 'gatsby-plugin-root-import',
options: {
src: path.join(__dirname, 'src'),
containers: path.join(__dirname, 'src/containers'),
images: path.join(__dirname, 'src/images'),
},
},
],
}
Any help anyone is willing to give would be greatly appreciated.
Your data, when using page queries, is always under props.data so your nesting should look like:
{data.headshots.edges.map(({ node }, idx) => {
return (
<div className={classes.flexItem} key={idx}>
<Img fluid={node.childImageSharp.fluid} />
</div>
)
})}
Note: I'm assuming that your GraphQL query retrieves properly the data. Test it at localhost:8000/___graphql and tweak your filesystem if needed
Each image is each node itself (according to query structure) so, since your query is aliasing your allFile as headshots, your data is stored in props.data.headshots.

Code coverage is not working for some files in WebStorm

I want to write a UT for the target code below:
// dev code
import React, { useState } from 'react'
import { Dropdown } from 'antd'
import './style.scss'
import classnames from 'classnames'
import Menu from '#lib/Menu'
const DEFAULT_TRIGGER_MODE = 'contextMenu'
interface ContextMenuProps {
wrapClass?: string
menu?: { key: string; title: string }[]
children?: object
updateMenu?: (event) => { key: string; title: string }[]
visible?: boolean
triggerMode?: ('click' | 'hover' | 'contextMenu')[]
}
const updateMenuData =(eventObj, updateMenu, setMenu) => {
if (updateMenu) {
const newMenu = updateMenu(eventObj)
setMenu(newMenu)
}
}
const populateMenuNodes = (menuData) => {
if (menuData.length === 0) return <></>
return (
<Menu>
{menuData.map(({ key, title, onClick, disabled }, index) => (
key === 'menu.divider' ? <Menu.Divider key={key} /> : <Menu.Item key={key} onClick={onClick} disabled={disabled}>{title}</Menu.Item>
))}
</Menu>
)
}
const ContextMenu: React.FC<ContextMenuProps> = (props) => {
const [menu, setMenu] = useState(props.menu || [])
const { children, updateMenu, wrapClass, triggerMode = [DEFAULT_TRIGGER_MODE], ...rest } = props
return (
<div
className={classnames('mstr-as-context-menu', wrapClass)}
onContextMenu={(e) => {
updateMenuData(e, updateMenu, setMenu)
}}
>
<Dropdown overlay={populateMenuNodes(menu)} trigger={triggerMode} {...rest}>
{children}
</Dropdown>
</div>
)
}
export default ContextMenu
And I have written a UT for the target code above:
import React from 'react'
import { shallow } from 'enzyme'
import ContextMenu from '../index'
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn((menu) => [menu || []]),
}))
describe('ContextMenu/index.js', () => {
let component
let props
beforeEach(() => {
props = {
children: <div>Children</div>,
}
})
describe('Check rendered component: ', () => {
it('should render a component successfully', () => {
component = shallow(<ContextMenu {...props}/>)
})
it('should show Menu.Item', () => {
props = {
...props,
menu: [{key: '', title: 'title1'}]
}
component = shallow(<ContextMenu {...props}/>)
});
it('should show Menu.Divider', () => {
props = {
...props,
menu: [{key: 'menu.divider', title: 'title1'}]
}
component = shallow(<ContextMenu {...props}/>)
});
afterEach(() => {
expect(component).toMatchSnapshot()
})
})
})
I run the tests successfully.
However I fail to get the coverage data of the target code with WebStorm's Code Coverage feature below.
From the screenshot below, it seems that WebStorm does not calculate code coverage for the target code while it calculates code coverage for other files.
I am the person who proposals the question.
Investigation
I run the command below in the terminal and find that the code coverage is not calculated for the same files.
jest --coverage
So it is not a specific issue about WebStorm. It is just an issue about Jest config.
Solution
Find the Jest config file: jest.config.js
module.exports = {
verbose: true,
testMatch: ['<rootDir>src/**/*\\.test\\.(ts|tsx|js|jsx)'],
globals: { __DEV__: true },
modulePaths: ['./src'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'],
moduleNameMapper: {
'\\.(css|less|scss|gif)$': 'identity-obj-proxy',
'^.+\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules',
'\\.(css|less|scss|sass|gif)$': 'identity-obj-proxy',
'^antd/es': 'identity-obj-proxy',
'^#mstr/': 'identity-obj-proxy',
'^dnd-core$': 'dnd-core/dist/cjs',
'^react-dnd$': 'react-dnd/dist/cjs',
'^react-dnd-html5-backend$': 'react-dnd-html5-backend/dist/cjs',
'^react-dnd-touch-backend$': 'react-dnd-touch-backend/dist/cjs',
'^react-dnd-test-backend$': 'react-dnd-test-backend/dist/cjs',
'^react-dnd-test-utils$': 'react-dnd-test-utils/dist/cjs',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'identity-obj-proxy',
},
testResultsProcessor: './scripts/jest-results-processor.js',
coverageReporters: ['lcov', 'text', 'json-summary', 'cobertura'],
setupFiles: ["jest-canvas-mock"],
setupFilesAfterEnv: ['./jest.setup.js'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/assets/**/*.{js,jsx,json,ts,tsx}',
'!src/lib/!(Canvas)/**/*.{js,jsx,json,ts,tsx}',
'!**/node_modules/**',
'!**/vendor/**',
'!**/env/**',
],
testPathIgnorePatterns: ['<rootDir>/src/components/xx/__tests__/data'],
snapshotSerializers: ['enzyme-to-json/serializer'],
}
The key is the code below
'!src/lib/!(Canvas)/**/*.{js,jsx,json,ts,tsx}',
Change this line to:
'src/lib/**/*.{js,jsx,json,ts,tsx}',
So the code coverage of all files, including the files I am working on, under the lib directory will be calculated.

Images in markdown not loading in template in gatsbyjs

Just like this tutorial Working with Images in Markdown Posts and Pages; I am trying to load a blog post's image from my markdown(frontmatter) to my template header
featuredImage: "../images/hiplife.jpg"
My template component looks like this:
import React from "react"
import { graphql } from "gatsby"
import { Img } from "gatsby-image"
import Layout from "../../components/layout/layout"
import Navbar from "../../components/navbar/navbar"
import styles from "./blogposts.module.css"
import kasahare from "../../images/pwd.png"
export default ({ data }) => {
let post = data.markdownRemark
let featuredImgFluid = post.frontmatter.featuredImage.childImageSharp.fluid
return (
<Layout>
<Navbar />
<div className={styles.Header}>
<Img fluid={featuredImgFluid} />
</div>
<div className={styles.BlogPosts}>
<div className={styles.BlogPostsHolder}>
<div className={styles.authorside}>
<div className={styles.author}>
<div>
<img className={styles.authorImg} src={kasahare} alt="Logo" />
</div>
<div>{post.author}</div>
<div>{post.date}</div>
<div className={styles.tag}>{post.tag}</div>
</div>
</div>
<div
className={styles.blogpostcontent}
dangerouslySetInnerHTML={{ __html: data.markdownRemark.html }}
/>
</div>
</div>
</Layout>
)
}
export const pageQuery = graphql`
query($path: String!) {
markdownRemark(frontmatter: { path: { eq: $path } }) {
html
frontmatter {
date(formatString: "MMMM DD, YYYY")
author
title
tag
featuredImage {
childImageSharp {
fluid(maxWidth: 1050) {
base64
aspectRatio
src
srcSet
sizes
}
}
}
}
}
}
`
But I keep getting this error.
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.
My gatsby-config looks like
module.exports = {
/* Your site config here */
siteMetadata: {
title: "HipLife",
description: "The blog for hiplife culture",
author: "kaf",
},
plugins: [
`gatsby-plugin-sharp`,
`gatsby-transformer-sharp`,
`gatsby-transformer-remark`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `markdown`,
path: `${__dirname}/src/markdown`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images/`,
},
},
],
}
And my gatsby-node
const path = require(`path`)
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions
const blogPostTemplate = path.resolve(`src/templates/blogposts/blogposts.js`)
const result = await graphql(`
{
allMarkdownRemark(
sort: { order: DESC, fields: [frontmatter___date] }
limit: 1000
) {
edges {
node {
frontmatter {
path
}
}
}
}
}
`)
// Handle errors
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.frontmatter.path,
component: blogPostTemplate,
context: {}, // additional data can be passed via context
})
})
}
Found out I had curly braces around my gatsby-image import import { Img } from "gatsby-image"
Removing them got it working. Thanks

Resources