Skip to content

Setting up a Gatsby Themes workspace with TypeScript, ESLint & Cypress

05/08/201911 min readCategory: Tutorial

The Gatsby team recommends developing themes with Yarn & Yarn workspaces which can be intimidating to users who are not yet familiar with workspaces. If you're one of these people or never heard of Gatsby themes before I'd highly recommend visiting the official documentation on Gatsby or watching Jason Lengstorf's "Authoring Gatsby Themes" egghead.io course. After going through that you might ask yourself how you could set up TypeScript or even ESLint & Cypress — this blogpost will explain to you how to exactly do that!

If you're looking for a guide on how to set up ESLint & Cypress with Gatsby themes and Yarn workspaces this article should work for you, too, if you skip the TypeScript portion and install other ESLint packages. Please leave a comment or contact me if you need help.

At the end of this tutorial you should have a Yarn workspace with ESLint linting + Cypress tests + TypeScript for both the example and theme. You can write your theme with TypeScript and have the same ESLint config everywhere. And if you wonder how people could use your theme if they don't use TypeScript: That's no problem at all! Gatsby's shadowing works with other filenames if you import modules without the filename extension (so people can shadow the file about.tsx with about.js or about.jsx).

#Basics

As mentioned above you should have some sort of understanding how Gatsby themes work and probably already used them a little bit before going through this tutorial. I assume that you have all necessary dependencies (e.g. Yarn) installed to run workspaces and Gatsby in general.

To not bore you with writing a boilerplate you'll use the "Gatsby theme workspace starter" to have a starting point. Clone this repository, go into the newly created directory and run yarn to install the dependencies. If you're stuck in between the steps of this tutorial or want to look at some code I encourage you to visit my fork of the aforementioned repository and skim through the commits (or have a look at the tutorial branch). You can also watch Amberley Romo's egghead.io video to see the setup.

Let's get started!

#TypeScript

At the time of writing this tutorial Gatsby doesn't have first-class TypeScript integration and most people use gatsby-plugin-typescript which uses Babel under the hood. In this article you'll only use this plugin, if you want to add type checking you should check out gatsby-plugin-typescript-checker. The former plugin allows you to write .ts/.tsx files (but not for gatsby-config, gatsby-node etc.).

Install the typescript plugin to your theme:

yarn workspace gatsby-theme-minimal add gatsby-plugin-typescript

Add the plugin to your theme's gatsby-config.js

gatsby-theme-minimal/gatsby-config.js
module.exports = {
  plugins: [
    `gatsby-plugin-typescript`
  ]
}

Additionally you'd also want to install the necessary types. But rather than installing them in every theme you should install them in the global workspace scope in the root. This way every workspace can use them.

yarn add -D -W @types/node @types/react @types/react-dom

The -D flag is for installing them as devDependencies, the -W flag tells yarn to install them in the workspace root.

To see things working add a new TypeScript file to your theme which you'll use in the example in just a second. Go to your theme and create a new file inside the components directory:

gatsby-theme-minimal/src/components/say-hello.tsx
import React from 'react'

type Props = {
  children: React.ReactNode
}

const Hello = ({ children }: Props) => {
  return (
    <div style={{ color: `red`, fontWeight: `bold` }}>
      SAY: {children}
    </div>
  )
}

export default Hello

Edit the index page of the example to use the newly created component:

example/src/pages/index.js
import React from "react"
import Hello from "gatsby-theme-minimal/src/components/say-hello"

export default () => <div>Homepage in a user's site <Hello>Hello!</Hello></div>

Make sure that it works by running the development server of the example.

yarn workspace example develop

You're greeted with the text "Homepage in a user's site SAY: Hello!"

Neat! It works 🎉

You can also try to shadow the file by creating a file called say-hello.js inside example/src/gatsby-theme-minimal/components.

#Type checking

But now you might wonder: Hey, how do I get type checking if the plugin only uses Babel to compile the files? You need to add the typescript package, a tsconfig.json and run a npm script to start it.

yarn add -D -W typescript

Add a tsconfig.json in the root of your project with the following content:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "lib": ["dom", "es2015", "es2017"],
    "moduleResolution": "node",
    "strict": true,
    "noEmit": true, // Don't create files when running tsc
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": ["./gatsby-theme-minimal/src/**/*"]
}

Lastly add a script called type-check to your package.json (in the root of the project):

{
  "scripts": {
    "type-check": "tsc"
  }
}

#ESLint

I personally really like to use ESLint with Prettier so that's why these instructions will let you install both. Please deviate from the guide by leaving out the Prettier parts if you don't want to use it.

More or less recently tslint was deprecated in favour of a TypeScript parser for ESLint. You'll use the preset from AirBnB + Prettier. Please install the following packages:

yarn add -D -W @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-airbnb eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks prettier

Add an .eslintrc.js file to the root:

.eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser', // Specifies the ESLint parser
  extends: [
    "airbnb",
    "plugin:@typescript-eslint/recommended",
    "plugin:import/typescript",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  plugins: ["@typescript-eslint", "prettier", "react-hooks"],
  parserOptions: {
    ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
    ecmaFeatures: {
      jsx: true,
    },
    project: './tsconfig.json'
  },
  env: {
    browser: true,
    jest: true,
    node: true,
  },
  globals: {
    cy: true,
    Cypress: true,
  },
  rules: {
    "@typescript-eslint/no-unused-vars": [
      1,
      {
        argsIgnorePattern: "res|next|stage|^err|on|config|e"
      }
    ],
    "arrow-body-style": [2, "as-needed"],
    "no-param-reassign": [
      2,
      {
        "props": false
      }
    ],
    "no-unused-expressions": [
      1,
      {
        "allowTaggedTemplates": true
      }
    ],
    "@typescript-eslint/prefer-interface": 0,
    "@typescript-eslint/explicit-function-return-type": 0,
    "@typescript-eslint/no-use-before-define": 0,
    "@typescript-eslint/camelcase": 0,
    "@typescript-eslint/no-var-requires": 0,
    "@typescript-eslint/no-explicit-any": 0,
    "@typescript-eslint/no-non-null-assertion": 0,
    "no-console": 0,
    "spaced-comment": 0,
    "no-use-before-define": 0,
    "linebreak-style": 0,
    "consistent-return": 0,
    "import": 0,
    "func-names": 0,
    "import/no-extraneous-dependencies": 0,
    "import/prefer-default-export": 0,
    "import/no-cycle": 0,
    "space-before-function-paren": 0,
    "import/extensions": 0,
    "react/jsx-one-expression-per-line": 0,
    "react/no-danger": 0,
    "react/display-name": 1,
    "react/react-in-jsx-scope": 0,
    "react/jsx-uses-react": 1,
    "react/forbid-prop-types": 0,
    "react/no-unescaped-entities": 0,
    "react/prop-types": 0,
    "react/jsx-filename-extension": [
      1,
      {
        "extensions": [".js", ".jsx", ".tsx"]
      }
    ],
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    quotes: [
      2,
      "backtick",
      {
        "avoidEscape": true
      }
    ],
    indent: ["error", 2, { SwitchCase: 1 }],
    "prettier/prettier": [
      "error",
      {
        trailingComma: "es5",
        semi: false,
        singleQuote: false,
        printWidth: 120
      }
    ],
    "jsx-a11y/href-no-hash": "off",
    "jsx-a11y/anchor-is-valid": [
      "warn",
      {
        "aspects": ["invalidHref"]
      }
    ]
  }
}

Add two npm scripts:

{
  "scripts": {
    "lint": "eslint --ignore-path .gitignore . --ext ts --ext tsx --ext js --ext jsx",
    "lint:fix": "yarn lint --fix"
  }
}

The lint command will run eslint on all files (with the ts/tsx/js/jsx file ending) except the ones defined in .gitignore. The --fix flag tries to automatically fix your errors. When running yarn lint you should get some errors from Prettier. Run yarn lint:fix to clean that up!

#Cypress

The popular End to End testing framework is a great tool to test themes in real-world scenarios like using the theme with its different options in an example or using it together with other themes.
And that is exactly what you'll set up now: A Cypress test suite to run tests against the example site.

First, you need to install the necessary packages. Besides the obvious one – cypress – you'll also install @testing-library/cypress and gatsby-cypress. Both extend the commands of Cypress, the former improves the process of selecting elements, the latter provides a useful helper function.

yarn add -D -W @testing-library/cypress cross-env cypress gatsby-cypress start-server-and-test

The package start-server-and-test enables you to first run the development server of Gatsby (or build command) and then the fitting Cypress command. It's a really handy little tool! Both @testing-library and cypress ship with their own TypeScript typings. That is important to know when writing the Cypress tests in TypeScript.

Once again you need to add some scripts to your root package.json:

{
  "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cross-env CYPRESS_baseUrl=http://localhost:9000 cypress run"
  }
}

Run yarn cy:open to initialize Cypress. It will automatically add files to your repository, like a cypress.json or the cypress folder. Quit the app before continuing this tutorial.

Please note: At the time of writing this guide WSL (Linux subsystem on Windows 10) can't open the Cypress electron app when running cypress open. You'll need to download the executable and run it yourself.

Delete the contents of cypress/integration (it contains example data) and rename it to e2e. Edit the cypress.json file:

cypress.json
{
  "baseUrl": "http://localhost:8000",
  "integrationFolder": "cypress/e2e/build",
  "viewportHeight": 900,
  "viewportWidth": 1440
}

As TypeScript will compile the tests and output it to cypress/e2e/build you have to tell Cypress to look into that folder. If you don't use TypeScript it would be cypress/e2e.

In order to use the installed Cypress packages (and also add a custom command) you have to edit those two files:

cypress/support/index.js
// Import commands.js using ES2015 syntax:
import "@testing-library/cypress/add-commands"
import "gatsby-cypress/commands"
import "./commands"
cypress/support/commands.js
Cypress.Commands.add(`assertRoute`, route => {
  cy.url().should(`equal`, `${window.location.origin}${route}`)
})

The assertRoute command let's you check the current URL.

Last but not least the project that should be tested — the example — has to be prepped. To enable Gatsby test utilities and ultimately enable gatsby-cypress to work the Gatsby CLI commands have to be run with an environment variable. Add cross-env to the example (to ensure cross-platform compatibility):

yarn workspace example add -D cross-env

And edit the example's package.json:

example/package.json
{
  "scripts": {
    "develop": "gatsby develop",
    "build": "gatsby build",
    "serve": "gatsby serve",
    "develop:cypress": "cross-env CYPRESS_SUPPORT=y yarn develop",
    "build:cypress": "cross-env CYPRESS_SUPPORT=y yarn build"
  }
}

#Using Cypress with TypeScript

After the basic setup is done you now can proceed to the final steps before writing tests! Keep it up :)

Create a tsconfig.json file inside the cypress directory:

cypress/tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "../node_modules",
    "outDir": "e2e/build",
    "strict": true,
    "sourceMap": false,
    "target": "es5",
    "lib": ["es2015", "es2017", "dom"],
    "types": ["cypress", "@testing-library/cypress/typings"]
  },
  "include": ["e2e/*.ts", "support/*.ts", "../node_modules/cypress"]
}

When later running the tsc scripts this tsconfig will be used, not the one in the root of the repository.

The custom command you added + the command coming from gatsby-cypress don't have TypeScript typing yet — let's change that! Create a new file inside cypress/support:

cypress/support/index.d.ts
/// <reference types="cypress" />

declare namespace Cypress {
  interface Chainable<Subject> {
    /**
     * Assert the current URL
     * @param route
     * @example cy.assertRoute('/page-2')
     */
    assertRoute(route: string): Chainable<any>

    /**
     * Waits for Gatsby to finish the route change, in order to ensure event handlers are properly setup
     */
    waitForRouteChange(): Chainable<any>
  }
}

You can already add a small test file:

cypress/e2e/smoke.ts
/// <reference types="../support/index" />
/// <reference types="cypress" />
/// <reference types="@testing-library/cypress/typings" />

describe(`app`, () => {
  it(`should work`, () => {
    cy.visit(`/`)
      .waitForRouteChange()
      .assertRoute(`/`)
  })
})

The /// means that this file should reference the TypeScript typings from these places/packages. Also: Try to hover over the waitForRouteChange or assertRoute function. Your IDE should display the typings & description. That's what you added in the previous step. Isn't that cool? 😎

Add the package concurrently to run multiple commands concurrently 😅

yarn add -D -W concurrently

Edit the root package.json to add both the TypeScript compilation of the files inside cypress/e2e and the final scripts you'll use to run the tests:

{
  "name": "gatsby-starter-theme-workspace",
  "private": true,
  "version": "0.0.1",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "tsc:compile": "tsc --project cypress/tsconfig.json",
    "tsc:compile:watch": "tsc --watch --project cypress/tsconfig.json",
    "example:cy:dev": "yarn workspace example develop:cypress",
    "example:cy:build": "yarn workspace example build:cypress",
    "example:serve": "yarn workspace example serve",
    "ssat:example:dev": "start-server-and-test example:cy:dev http://localhost:8000 cy:open",
    "ssat:example:serve": "start-server-and-test example:serve http://localhost:9000 cy:run",
    "e2e:dev": "concurrently --kill-others 'yarn tsc:compile:watch' 'yarn ssat:example:dev'",
    "e2e:ci": "yarn tsc:compile && yarn example:cy:build && yarn ssat:example:serve"
  }
}

This can look overwhelming, don't worry. Here are some explanations:

  • TypeScript should use the cypress/tsconfig.json which in turn tells it to compile the files inside cypress/e2e. The --watch flag enables automatic re-compilation when saving a file
  • start-server-and-test expects the server script, then the URL it should listen to and finally the test script
  • For CI purposes the example site is not run in development mode but built and served. That mimics the final site the best I think

#Writing tests

Finally you're able to write tests! Let's just do that:

cypress/e2e/home.ts
/// <reference types="../support/index" />
/// <reference types="cypress" />
/// <reference types="@testing-library/cypress/typings" />

describe(`example`, () => {
  it(`contains keyword`, () => {
    cy.visit(`/`)
      .waitForRouteChange()
      .getByText(/say: hello!/i)
  })
})

Run yarn e2e:dev, click on home.js in the Cypress electron app and hopefully see the test working 🎉

#Where to go from here

Yeah, you finished this guide 👍🏻Thanks for following through – I want to give you some ideas on what to do next or how you can adapt this guide for other usecases. As mentioned at the beginning you can see all commits in this repository which you can of course also use as a template.

Some ideas:

  • Run the linting and tests on a CI provider (e.g. CircleCI) to have more confidence into your PRs
  • Modify the ESLint config to your liking as it's certainly opinionated to my preferred code style
  • Add Cypress tests testing the theme options
  • Add husky + lint-staged to run the linter before commiting your files
  • Add more themes (+ examples) => Create a monorepo of themes. The themes of themes.lekoarts.de are organized in this GitHub repository. Take a look at it if you want to see how you could adapt the above guide to multiple themes.

Thanks for reading and I hope this article was helpful to you. Please leave a comment if you have questions or contact me on Twitter.

Sparked your interest? Read all posts in the category Tutorial

More posts