Skip to content

Commit

Permalink
feat: Enable TS for gatsby-browser/gatsby-ssr (gatsbyjs#34692)
Browse files Browse the repository at this point in the history
  • Loading branch information
LekoArts authored and wardpeet committed Feb 8, 2022
1 parent 3413d03 commit 8847416
Show file tree
Hide file tree
Showing 18 changed files with 211 additions and 25 deletions.
126 changes: 116 additions & 10 deletions docs/docs/how-to/custom-configuration/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,133 @@ examples:
href: "https://github.com/gatsbyjs/gatsby/tree/master/examples/using-typescript"
---

## Introductory paragraph
## Introduction

[TypeScript](https://www.typescriptlang.org/) is a JavaScript superset which extends the language to include type definitions allowing codebases to be statically checked for soundness. Gatsby provides an integrated experience out of the box, similar to an IDE. If you are new to TypeScript, adoption can _and should_ be incremental. Since Gatsby natively supports JavaScript and TypeScript, you can change files from `.js` to `.tsx` at any point to start adding types and gaining the benefits of a type system.
[TypeScript](https://www.typescriptlang.org/) is a JavaScript superset which extends the language to include type definitions allowing codebases to be statically checked for soundness. Gatsby provides an integrated experience out of the box, similar to an IDE. If you are new to TypeScript, adoption can _and should_ be incremental. Since Gatsby natively supports JavaScript and TypeScript, you can change files from `.js`/`.jsx` to `.ts`/`.tsx` at any point to start adding types and gaining the benefits of a type system.

## Example
To see all of the types available and their generics look at our [TypeScript definition file](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/index.d.ts).

## `PageProps`

```tsx:title=src/pages/index.tsx
import React from "react"
import * as React from "react"
import { PageProps } from "gatsby"

export default function IndexRoute(props: PageProps) {
const IndexRoute = ({ path }: PageProps) => {
return (
<>
<h1>Path:</h1>
<p>{props.path}</p>
</>
<main>
<h1>Path: {path}</h1>
</main>
)
}

export default IndexRoute
```

The example above uses the power of TypeScript, in combination with exported types from Gatsby (`PageProps`) to tell this code what props is. This can greatly improve your developer experience by letting your IDE show you what properties are injected by Gatsby. To see all of the types available look at our [TypeScript definition file](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/index.d.ts).
The example above uses the power of TypeScript, in combination with exported types from Gatsby (`PageProps`) to tell this code what props is. This can greatly improve your developer experience by letting your IDE show you what properties are injected by Gatsby.

`PageProps` can receive a couple of [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html), most notably the `DataType` one. This way you can type the resulting `data` prop.

```tsx:title=src/pages/index.tsx
import * as React from "react"
import { graphql, PageProps } from "gatsby"

type DataProps = {
site: {
siteMetadata: {
title: string
}
}
}

const IndexRoute = ({ data: { site } }: PageProps<DataProps>) => {
return (
<main>
<h1>{site.siteMetadata.title}</h1>
</main>
)
}

export default IndexRoute

export const query = graphql`
{
site {
siteMetadata {
title
}
}
}
`
```

## `gatsby-browser.tsx` / `gatsby-ssr.tsx`

You can also write `gatsby-browser` and `gatsby-ssr` in TypeScript. You have the types `GatsbyBrowser` and `GatsbySSR` available to type your API functions. Here are two examples:

```tsx:title=gatsby-browser.tsx
import * as React from "react"
import { GatsbyBrowser } from "gatsby"

export const wrapPageElement: GatsbyBrowser["wrapPageElement"] = ({ element }) => {
return (
<div>
<h1>Hello World</h1>
{element}
</div>
)
}
```

```tsx:title=gatsby-ssr.tsx
import * as React from "react"
import { GatsbySSR } from "gatsby"

export const wrapPageElement: GatsbySSR["wrapPageElement"] = ({ element }) => {
return (
<div>
<h1>Hello World</h1>
{element}
</div>
)
}
```

## `getServerData`

You can use `GetServerData`, `GetServerDataProps`, and `GetServerDataReturn` for [`getServerData`](/docs/reference/rendering-options/server-side-rendering/).

```tsx:src/pages/ssr.tsx
import * as React from "react"
import { GetServerDataProps, GetServerDataReturn } from "gatsby"

type ServerDataProps = {
hello: string
}

const Page = () => <div>Hello World</div>
export default Page

export async function getServerData(
props: GetServerDataProps
): GetServerDataReturn<ServerDataProps> {
return {
status: 200,
headers: {},
props: {
hello: "world",
},
}
}
```

If you’re using an anonymous function, you can also use the shorthand `GetServerData` type like this:

```tsx
const getServerData: GetServerData<ServerDataProps> = async props => {
// your function body
}
```

## Other resources

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
describe(`gatsby-browser.tsx`, () => {
it(`works`, () => {
cy.visit(`/`).waitForRouteChange()
cy.get(`.gatsby-browser-tsx`).should(`have.attr`, `data-content`, `TSX with gatsby-browser works`)
})
})
1 change: 1 addition & 0 deletions e2e-tests/development-runtime/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module.exports = {
`gatsby-source-fake-data`,
`gatsby-source-pinc-data`,
`gatsby-source-query-on-demand-data`,
`gatsby-browser-tsx`,
`gatsby-transformer-sharp`,
`gatsby-transformer-json`,
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from "react"
import { GatsbyBrowser } from "gatsby"

export const wrapPageElement: GatsbyBrowser["wrapPageElement"] = ({ element }) => {
return (
<>
<div className="gatsby-browser-tsx" data-content="TSX with gatsby-browser works" />
{element}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
describe(`gatsby-ssr.tsx`, () => {
it(`works`, () => {
cy.visit(`/`).waitForRouteChange()
cy.get(`.gatsby-ssr-tsx`).should(`have.attr`, `data-content`, `TSX with gatsby-ssr works`)
})
})
1 change: 1 addition & 0 deletions e2e-tests/production-runtime/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
},
},
`gatsby-plugin-local-worker`,
`gatsby-ssr-tsx`,
`gatsby-plugin-image`,
`gatsby-plugin-sharp`,
`gatsby-plugin-sass`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from "react"
import { GatsbyBrowser } from "gatsby"

export const wrapPageElement: GatsbyBrowser["wrapPageElement"] = ({ element }) => {
return (
<>
<div className="gatsby-ssr-tsx" data-content="TSX with gatsby-ssr works" />
{element}
</>
)
}
11 changes: 11 additions & 0 deletions e2e-tests/production-runtime/plugins/gatsby-ssr-tsx/gatsby-ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from "react"
import { GatsbySSR } from "gatsby"

export const wrapPageElement: GatsbySSR["wrapPageElement"] = ({ element }) => {
return (
<>
<div className="gatsby-ssr-tsx" data-content="TSX with gatsby-ssr works" />
{element}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// noop
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "gatsby-ssr-tsx",
"version": "1.0.0",
"main": "index.js"
}
1 change: 1 addition & 0 deletions packages/gatsby/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"detect-port": "^1.3.0",
"devcert": "^1.2.0",
"dotenv": "^8.6.0",
"enhanced-resolve": "^5.8.3",
"eslint": "^7.32.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.10.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ jest.mock(`gatsby-cli/lib/reporter`, () => {
}
})

const fs = require(`fs`)
const reporter = require(`gatsby-cli/lib/reporter`)
const { resolveModuleExports } = require(`../resolve-module-exports`)
import * as fs from "fs-extra"
import reporter from "gatsby-cli/lib/reporter"
import { resolveModuleExports } from "../resolve-module-exports"
let resolver

describe(`Resolve module exports`, () => {
Expand Down Expand Up @@ -129,10 +129,12 @@ describe(`Resolve module exports`, () => {

beforeEach(() => {
resolver = jest.fn(arg => arg)
// @ts-ignore
fs.readFileSync.mockImplementation(file => {
const existing = MOCK_FILE_INFO[file]
return existing
})
// @ts-ignore
reporter.panic.mockClear()
})

Expand All @@ -149,6 +151,7 @@ describe(`Resolve module exports`, () => {
it(`Show meaningful error message for invalid JavaScript`, () => {
resolveModuleExports(`/bad/file`, { resolver })
expect(
// @ts-ignore
reporter.panic.mock.calls.map(c =>
// Remove console colors + trim whitespace
// eslint-disable-next-line
Expand Down
14 changes: 7 additions & 7 deletions packages/gatsby/src/bootstrap/resolve-module-exports.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import fs from "fs"
import * as fs from "fs-extra"
import * as t from "@babel/types"
import traverse from "@babel/traverse"
import { codeFrameColumns, SourceLocation } from "@babel/code-frame"
import { babelParseToAst } from "../utils/babel-parse-to-ast"
import report from "gatsby-cli/lib/reporter"

import { babelParseToAst } from "../utils/babel-parse-to-ast"
import { testRequireError } from "../utils/test-require-error"
import { resolveModule } from "../utils/module-resolver"

const staticallyAnalyzeExports = (
modulePath: string,
resolver = require.resolve
resolver = resolveModule
): Array<string> => {
let absPath: string | undefined
const exportNames: Array<string> = []

try {
absPath = resolver(modulePath)
absPath = resolver(modulePath) as string
} catch (err) {
return exportNames // doesn't exist
}
Expand Down Expand Up @@ -188,12 +188,12 @@ https://gatsby.dev/no-mixed-modules
*/
export const resolveModuleExports = (
modulePath: string,
{ mode = `analysis`, resolver = require.resolve } = {}
{ mode = `analysis`, resolver = resolveModule } = {}
): Array<string> => {
if (mode === `require`) {
let absPath: string | undefined
try {
absPath = resolver(modulePath)
absPath = require.resolve(modulePath)
return Object.keys(require(modulePath)).filter(
exportName => exportName !== `__esModule`
)
Expand Down
9 changes: 5 additions & 4 deletions packages/gatsby/src/services/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { detectLmdbStore } from "../datastore"
import { loadConfigAndPlugins } from "../bootstrap/load-config-and-plugins"
import type { InternalJob } from "../utils/jobs/types"
import { enableNodeMutationsDetection } from "../utils/detect-node-mutations"
import { resolveModule } from "../utils/module-resolver"

interface IPluginResolution {
resolve: string
Expand Down Expand Up @@ -517,16 +518,16 @@ export async function initialize({
// a handy place to include global styles and other global imports.
try {
if (env === `browser`) {
return slash(
require.resolve(path.join(plugin.resolve, `gatsby-${env}`))
)
const modulePath = path.join(plugin.resolve, `gatsby-${env}`)
return slash(resolveModule(modulePath) as string)
}
} catch (e) {
// ignore
}

if (envAPIs && Array.isArray(envAPIs) && envAPIs.length > 0) {
return slash(path.join(plugin.resolve, `gatsby-${env}`))
const modulePath = path.join(plugin.resolve, `gatsby-${env}`)
return slash(resolveModule(modulePath) as string)
}
return undefined
}
Expand Down
21 changes: 21 additions & 0 deletions packages/gatsby/src/utils/module-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as fs from "fs"
import enhancedResolve, { CachedInputFileSystem } from "enhanced-resolve"

type ModuleResolver = (modulePath: string) => string | false
type ResolveType = (context?: any, path?: any, request?: any) => string | false

export const resolveModule: ModuleResolver = modulePath => {
let resolve: ResolveType

try {
resolve = enhancedResolve.create.sync({
fileSystem: new CachedInputFileSystem(fs, 5000),
extensions: [`.ts`, `.tsx`, `.js`, `.jsx`],
})
} catch (err) {
// ignore
}

// @ts-ignore - See https://github.com/microsoft/TypeScript/issues/9568
return resolve({}, modulePath, modulePath)
}
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9290,7 +9290,7 @@ engine.io@~4.1.0:
engine.io-parser "~4.0.0"
ws "~7.4.2"

enhanced-resolve@^5.8.2, enhanced-resolve@^5.8.3:
enhanced-resolve@^5.8.3:
version "5.8.3"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0"
integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==
Expand Down

0 comments on commit 8847416

Please sign in to comment.