Skip to content

Instantly share code, notes, and snippets.

@JohnAlbin
Forked from clarkdave/createPages.ts
Last active March 18, 2024 09:25
Show Gist options
  • Star 54 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save JohnAlbin/2fc05966624dffb20f4b06b4305280f9 to your computer and use it in GitHub Desktop.
Save JohnAlbin/2fc05966624dffb20f4b06b4305280f9 to your computer and use it in GitHub Desktop.
TypeScript + Gatsby config and node API

README

  1. When Gatsby starts up, it will read gatsby-config.js first.
  2. As you can see below, we use that file to require('ts-node').register() which registers a TypeScript evaluator that will be used when Gatsby reads all other API Javascript files. In other words, we only need to do this once in our entire codebase and not in other Gatsby files like gatsby-node.js.
  3. Our gatsby-config.js re-exports all the exported variables available in gatsby-config.ts.
  4. Later, since the ts-node evaluator is still active, Gatsby will load gatsby-node.ts instead of gatsby-node.js.
  5. The same thing is true of other gatsby files; e.g. gatsby-browser.ts can be used instead of gatsby-browser.js.

Credits

I didn't come up with all of this on my own. I mentioned all the sources in the original gist.

// We register the TypeScript evaluator in gatsby-config so we don't need to do
// it in any other .js file. It automatically reads TypeScript config from
// tsconfig.json.
require('ts-node').register();
// Use a TypeScript version of gatsby-config.js.
module.exports = require('./gatsby-config.ts');
// All exported variables in this file will also used in gatsby-config.js.
export const siteMetadata = {
title: `My Gatsby Site`,
description: `An example site.`,
};
export const plugins = [
'gatsby-plugin-typescript',
'gatsby-plugin-postcss',
);
// Because we used ts-node in gatsby-config.js, this file will automatically be
// imported by Gatsby instead of gatsby-node.js.
// Use the type definitions that are included with Gatsby.
import { GatsbyNode } from 'gatsby';
import { resolve } from 'path';
export const createPages: GatsbyNode['createPages'] = async ({
actions,
graphql,
}) => {
const { createPage } = actions;
const allMarkdown: {
errors?: any;
data?: { allMarkdownRemark: { nodes: { fields: { slug?: string } }[] } };
} = await graphql(`
query allMarkdownQuery {
allMarkdownRemark(limit: 1000) {
nodes {
fields {
slug
}
}
}
}
`);
allMarkdown.data?.allMarkdownRemark.nodes.forEach(node => {
const { slug } = node.fields;
if (!slug) return;
// Type-safe `createPage` call.
createPage({
path: slug,
component: resolve(__dirname, '../src/templates/index.tsx'),
context: {
slug,
},
});
});
};
@weedgrease
Copy link

This is awesome! thanks so much for putting this together. I do see that there is no type defined for anything being returned in allMarkdown, though:

const allMarkdown: {
    errors?: any;
    data?: unknown;
}

@JohnAlbin
Copy link
Author

I do see that there is no type defined for anything being returned in allMarkdown, though:

Oh! Good catch. I've updated the gist to add the type definition for the allMarkdown const.

@leyanlo
Copy link

leyanlo commented Apr 2, 2020

Thanks for sharing! I’m trying this out and running into an eslint error:

gatsby-config.js
  0:0  error  Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.
The file does not match your project config: gatsby-config.js.
The file must be included in at least one of the projects provided

Have you seen something like this? Any tips on how to update my .eslintrc or tsconfig.json to handle gatsby-config.js?

I’ve tried adding include: ["gatsby-config.js"] or include: ["*.js", "*.ts", "src/**/*.ts", "src/**/*.tsx"] to tsconfig.json, but that does not resolve the error. I’ve added createDefaultProgram to parserOptions, which fixes the issue, but sounds like an anti-pattern.

UPDATE 2020-04-02

Seems like the lint issue is caused by gatsby-config.js and gatsby-config.ts sharing the same name. Renaming to gatsby-config-exports.ts resolves the issue.

@kara-ryli
Copy link

This is pretty awesome. Thanks so much for doing this. One thing that I'm considering is moving my gatsby API files into src/gatsby-api/*, which allows me a pattern that looks a bit more obvious (to me, at least).

gatsby-node.js

module.exports = require('./src/gatsby-api/node').default;

src/gatsby-api/node.ts

const NodeAPI: GatsbyNode = {
  async createPages({ actions, graphql }) { ... }
  // other methods
};
export default NodeAPI;

And lastly,

src/typings/GraphQL.d.ts

declare namespace GraphQL {
  namespace Model {
    interface Post {
      html: string;
      frontmatter: {
        date: string;
        path: string;
        title: string;
      };
    }
  }

  namespace Query {
    interface Posts {
      allMarkdownRemark: {
        edges: Array<{
          node: Model.Post[]
        }>;
      };
    }
  }
}

Which integrates well with Gatsby's built-in typings:

    const result = await graphql<GraphQL.Query.Posts>(query);
    if (result.errors) {
      console.error(result.errors);
    }
    result.data?.allMarkdownRemark.edges.forEach(({ node }) => {
      createPage({
        path: node.frontmatter.path,
        component: resolve(`src/templates/post.tsx`),
        context: {},
      });
    });

@fzembow
Copy link

fzembow commented May 18, 2020

I ran into some problems when setting this up, I ended up needing to set the following fields in my tsconfig.json in addition to installing @types/node

  "compilerOptions": {
    "module": "commonjs",
    "types": [
       "node"
    ]
  },

@ooloth
Copy link

ooloth commented May 22, 2020

I ran into some problems when setting this up, I ended up needing to set the following fields in my tsconfig.json in addition to installing @types/node

  "compilerOptions": {
    "module": "commonjs",
    "types": [
       "node"
    ]
  },

I needed "module": "commonjs" as well. Thanks!

@dandv
Copy link

dandv commented May 22, 2020

Interesting, what problems have you guys run into? @ooloth, @fzembow?

@ooloth
Copy link

ooloth commented May 22, 2020

When set to esnext, I was getting errors about trying to use ESM imports in a gatsby-* file (even though those were TS files). 🤷‍♂️

@dandv
Copy link

dandv commented May 22, 2020

Yeah, I gave up on esnext modules, tooling is just not there yet.

@jakebellacera
Copy link

jakebellacera commented May 27, 2020

For those that use the compilerOptions.paths option in tsconfig.json, the ts-node module will not follow those path mappings. Instead, you need to require tsconfig-paths in your gatsby-config.js file as well:

require("ts-node/register")
require("tsconfig-paths/register")

module.exports = require("./gatsby-config-exports.ts")

@mokyox
Copy link

mokyox commented Jun 3, 2020

Is anyone else getting issues an error Cannot query field "fields" on type "MarkdownRemark"?
I've tried rm -rf node_modules, restarting development servers and numerous other steps but I can't seem to find the fields field on my GraphiQL either.

Here's my files - repo link also here

gatsby-config.js


// We register the TypeScript evaluator in gatsby-config so we don't need to do
// it in any other .js file. It automatically reads TypeScript config from
// tsconfig.json.
/* eslint-disable */

require("ts-node").register();

// Use a TypeScript version of gatsby-config.js.
module.exports = require("./gatsby-config.ts");

gatsby-config.ts

export const plugins = [
  `gatsby-plugin-styled-components`,
  `gatsby-plugin-react-helmet`,
  {
    resolve: `gatsby-plugin-typescript`,
    options: {
      isTSX: true,
      jsxPragma: `React`,
      allExtensions: true,
    },
  },
  {
    resolve: `gatsby-source-filesystem`,
    options: {
      name: `blog`,
      path: `${__dirname}/src/content/blog`,
    },
  },
  `gatsby-transformer-remark`,
  `gatsby-transformer-sharp`,
  `gatsby-plugin-sharp`,
  {
    resolve: `gatsby-plugin-manifest`,
    options: {
      name: `Mo Jama`,
      start_url: `/`,
      background_color: `#1A202C`,
      theme_color: `#1A202C`,
      display: `minimal-ui`,
      icon: `src/assets/favicon.png`,
    },
  },
];

gatsby-node.ts

//https://gist.github.com/JohnAlbin/2fc05966624dffb20f4b06b4305280f9

// Because we used ts-node in gatsby-config.js, this file will automatically be
// imported by Gatsby instead of gatsby-node.js.

// Use the type definitions that are included with Gatsby.
import { GatsbyNode } from "gatsby";
import { resolve } from "path";

export const createPages: GatsbyNode["createPages"] = async ({
  actions,
  graphql,
}) => {
  const { createPage } = actions;

  const allMarkdown: {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    errors?: any;
    data?: { allMarkdownRemark: { nodes: { fields: { slug?: string } }[] } };
  } = await graphql(`
    query allMarkdownQuery {
      allMarkdownRemark(limit: 1000) {
        nodes {
          fields {
            slug
          }
        }
      }
    }
  `);

  allMarkdown.data?.allMarkdownRemark.nodes.forEach((node) => {
    const { slug } = node.fields;
    if (!slug) return;
    console.log(slug);

    // Type-safe `createPage` call.
    createPage({
      path: slug,
      component: resolve(__dirname, "../src/templates/blog-post.tsx"),
      context: {
        slug,
      },
    });
  });
};

Pretty stumped so far!

@jakebellacera
Copy link

@mokyox you need to create the slug fields first. You can do this on the onCreateNode hook:

// gatsby-node.ts
import { GatsbyNode } from "gatsby"

export const onCreateNode: GatsbyNode["onCreateNode"] = ({
  node,
  getNode,
  actions,
}) => {
  const { createNodeField } = actions

  if (node.internal.type === "MarkdownRemark") {
    const slug = createFilePath({ node, getNode })

    createNodeField({
      node,
      name: "slug",
      value: slug
    })
  }
}

The createFilePath() action is provided by gatsby-source-filesystem and offers a couple options to generate the slug in case you want to change the base path (i.e. put it under a subfolder) or remove the trailing slash.

@mokyox
Copy link

mokyox commented Jun 5, 2020

@jakebellacera

Thanks so much for that - completely missed that fact I need to use the onCreateNode hook from the gatsby-source-filesystem! 😅.

It works perfectly now. Thanks again!

@jakebellacera
Copy link

@mokyox no worries! I'm still learning my way around Gatsby and that tripped me up as well! Glad that helped 😃

@dandv
Copy link

dandv commented Jun 8, 2020

So I've just learned that there's a plugin gatsby-plugin-ts-config that "write all of your config files in Typescript", and has gotten quite a bit of exposure in the issue about native TypeScript support for Gatsby.

@isaac-martin
Copy link

Anyone have this working with path aliases? I keep getting build errors because it can't resolve some stuff

@henricazottes
Copy link

It seems promising but can't get it working, I have this error running gatsby develop:

  TSError: ⨯ Unable to compile TypeScript:
  gatsby-node.ts:29:53 - error TS7006: Parameter 'node' implicitly has an 'any' type.
  29   allMarkdown.data?.allMarkdownRemark.nodes.forEach(node => {
                                                         ~~~~
  gatsby-node.ts:29:20 - error TS1109: Expression expected.
  29   allMarkdown.data?.allMarkdownRemark.nodes.forEach(node => {
                        ~
  gatsby-node.ts:41:5 - error TS1005: ':' expected.
  41   });
         ~

Which is weird cause VSCode shows me the inferred type when I move the pointer over the node argument:
image

@thepedroferrari
Copy link

Thank you, it works!

@bsgreenb
Copy link

bsgreenb commented Oct 5, 2020

Hey @JohnAlbin , wondering if you have any insight into this issue? I think it may be related to using ts-node/commonjs as required in this setup? https://stackoverflow.com/questions/64202523/exporting-global-styles-with-font-assets-from-a-typescript-commonjs-module

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment