diff --git a/packages/gatsby-link/.babelrc b/packages/gatsby-link/.babelrc index 0967ef424bce6..05686bb099fb6 100644 --- a/packages/gatsby-link/.babelrc +++ b/packages/gatsby-link/.babelrc @@ -1 +1,8 @@ -{} +{ + "presets": [["babel-preset-gatsby-package"]], + "overrides": [ + { + "presets": [["babel-preset-gatsby-package", { "browser": true }]] + } + ] +} diff --git a/packages/gatsby-link/.eslintrc.yaml b/packages/gatsby-link/.eslintrc.yaml deleted file mode 100644 index 6f3b6683ff79e..0000000000000 --- a/packages/gatsby-link/.eslintrc.yaml +++ /dev/null @@ -1,4 +0,0 @@ -env: - browser: true -globals: - ___loader: false diff --git a/packages/gatsby-link/index.d.ts b/packages/gatsby-link/index.d.ts index 6c9cfa9ec5473..92c90614dc221 100644 --- a/packages/gatsby-link/index.d.ts +++ b/packages/gatsby-link/index.d.ts @@ -1,6 +1,17 @@ import * as React from "react" import { NavigateFn, LinkProps } from "@reach/router" // These come from `@types/reach__router` +declare global { + /* eslint-disable @typescript-eslint/interface-name-prefix */ + interface Window { + ___push: (to: string) => void + ___replace: (to: string) => void + ___navigate: (to: string, options?: NavigateOptions<{}>) => void + ___loader: { enqueue: (arg0: string) => {}, hovering: (arg0: string) => {} } + } + /* eslint-disable @typescript-eslint/interface-name-prefix */ +} + // eslint-disable-next-line @typescript-eslint/naming-convention export interface GatsbyLinkProps extends LinkProps { /** A class to apply when this Link is active */ diff --git a/packages/gatsby-link/src/__tests__/index.js b/packages/gatsby-link/src/__tests__/index.tsx similarity index 88% rename from packages/gatsby-link/src/__tests__/index.js rename to packages/gatsby-link/src/__tests__/index.tsx index 3daa54d65c0b0..dc0914c708a14 100644 --- a/packages/gatsby-link/src/__tests__/index.js +++ b/packages/gatsby-link/src/__tests__/index.tsx @@ -8,9 +8,36 @@ import { createMemorySource, createHistory, LocationProvider, + NavigateOptions, } from "@reach/router" import { Link, navigate, withPrefix, withAssetPrefix } from "../" +type IntersectionObserverType = jest.Mock< + { + observe: (ref: Record) => void + unobserve: (ref: Record) => void + disconnect: () => void + trigger: (ref: Record) => void + }, + [cb: any] +> + +interface ICustomNodeJsGlobal extends NodeJS.Global { + __BASE_PATH__: string | undefined + __PATH_PREFIX__: string | undefined + ___navigate: ( + to: string, + options?: NavigateOptions> + ) => void + ___loader: { + enqueue: (arg0: string) => Record + hovering?: (arg0: string) => Record + } + IntersectionObserver: IntersectionObserverType +} + +declare const global: ICustomNodeJsGlobal + beforeEach(() => { global.__BASE_PATH__ = `` global.__PATH_PREFIX__ = `` @@ -18,39 +45,44 @@ beforeEach(() => { afterEach(cleanup) +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const getWithPrefix = (pathPrefix = ``) => { + global.__BASE_PATH__ = pathPrefix + return withPrefix +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type const getInstance = (props, pathPrefix = ``) => { getWithPrefix()(pathPrefix) return } +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type const getNavigate = () => { global.___navigate = jest.fn() return navigate } -const getWithPrefix = (pathPrefix = ``) => { - global.__BASE_PATH__ = pathPrefix - return withPrefix -} - +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type const getWithAssetPrefix = (prefix = ``) => { global.__PATH_PREFIX__ = prefix return withAssetPrefix } -const setup = ({ sourcePath = `/`, linkProps, pathPrefix = `` } = {}) => { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const setup = ({ sourcePath = `/`, pathPrefix = ``, linkProps = {} } = {}) => { const intersectionInstances = new WeakMap() // mock intersectionObserver global.IntersectionObserver = jest.fn(cb => { const instance = { - observe: ref => { + observe: (ref: Record): void => { intersectionInstances.set(ref, instance) }, - unobserve: ref => { + unobserve: (ref: Record): void => { intersectionInstances.delete(ref) }, - disconnect: () => {}, - trigger: ref => { + disconnect: (): void => {}, + trigger: (ref: Record): void => { cb([ { target: ref, @@ -62,6 +94,7 @@ const setup = ({ sourcePath = `/`, linkProps, pathPrefix = `` } = {}) => { return instance }) + global.__BASE_PATH__ = pathPrefix const source = createMemorySource(sourcePath) const history = createHistory(source) @@ -69,12 +102,12 @@ const setup = ({ sourcePath = `/`, linkProps, pathPrefix = `` } = {}) => { const utils = render( link @@ -386,7 +419,7 @@ describe(`ref forwarding`, () => { }) it(`handles a RefObject (React >=16.4)`, () => { - const ref = React.createRef(null) + const ref = React.createRef() setup({ linkProps: { ref } }) expect(ref.current).toEqual(expect.any(HTMLElement)) diff --git a/packages/gatsby-link/src/__tests__/is-local-link.js b/packages/gatsby-link/src/__tests__/is-local-link.ts similarity index 68% rename from packages/gatsby-link/src/__tests__/is-local-link.js rename to packages/gatsby-link/src/__tests__/is-local-link.ts index 79a52ae5fa53c..513723c7a98b0 100644 --- a/packages/gatsby-link/src/__tests__/is-local-link.js +++ b/packages/gatsby-link/src/__tests__/is-local-link.ts @@ -8,16 +8,24 @@ describe(`isLocalLink`, () => { expect(isLocalLink(`https://www.gatsbyjs.com`)).toBe(false) }) it(`returns undefined if input is undefined or not a string`, () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore purposefully wrong expect(isLocalLink(undefined)).toBeUndefined() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore purposefully wrong expect(isLocalLink(-1)).toBeUndefined() }) // TODO(v5): Unskip Tests it.skip(`throws TypeError if input is undefined`, () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore error case expect(() => isLocalLink(undefined)).toThrowError( `Expected a \`string\`, got \`undefined\`` ) }) it.skip(`throws TypeError if input is not a string`, () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore error case expect(() => isLocalLink(-1)).toThrowError( `Expected a \`string\`, got \`number\`` ) diff --git a/packages/gatsby-link/src/__tests__/parse-path.js b/packages/gatsby-link/src/__tests__/parse-path.ts similarity index 100% rename from packages/gatsby-link/src/__tests__/parse-path.js rename to packages/gatsby-link/src/__tests__/parse-path.ts diff --git a/packages/gatsby-link/src/__tests__/rewrite-link-path.js b/packages/gatsby-link/src/__tests__/rewrite-link-path.ts similarity index 86% rename from packages/gatsby-link/src/__tests__/rewrite-link-path.js rename to packages/gatsby-link/src/__tests__/rewrite-link-path.ts index 416c9d89e74cc..1e2fceb890406 100644 --- a/packages/gatsby-link/src/__tests__/rewrite-link-path.js +++ b/packages/gatsby-link/src/__tests__/rewrite-link-path.ts @@ -1,11 +1,23 @@ -import { rewriteLinkPath } from "../rewrite-link-path" +import type { TrailingSlash } from "gatsby-page-utils" + +import { rewriteLinkPath, type rewriteLinkPathType } from "../rewrite-link-path" + +interface ICustomNodeJsGlobal extends NodeJS.Global { + __TRAILING_SLASH__: TrailingSlash | undefined | string + __PATH_PREFIX__: string | undefined +} + +declare const global: ICustomNodeJsGlobal beforeEach(() => { global.__TRAILING_SLASH__ = `` global.__PATH_PREFIX__ = undefined }) -const getRewriteLinkPath = (option = `legacy`, pathPrefix = undefined) => { +const getRewriteLinkPath = ( + option: string = `legacy`, + pathPrefix: string | undefined = undefined +): rewriteLinkPathType => { global.__TRAILING_SLASH__ = option global.__PATH_PREFIX__ = pathPrefix return rewriteLinkPath diff --git a/packages/gatsby-link/src/index.js b/packages/gatsby-link/src/index.tsx similarity index 68% rename from packages/gatsby-link/src/index.js rename to packages/gatsby-link/src/index.tsx index 1024670e6d82e..819c6104478a7 100644 --- a/packages/gatsby-link/src/index.js +++ b/packages/gatsby-link/src/index.tsx @@ -1,6 +1,15 @@ import PropTypes from "prop-types" -import React from "react" -import { Link as ReachRouterLink, Location } from "@gatsbyjs/reach-router" +import React, { + MutableRefObject, + ReactElement, + HTMLAttributes, + CSSProperties, +} from "react" +import { + Link as ReachRouterLink, + Location, + NavigateOptions, +} from "@gatsbyjs/reach-router" import { parsePath } from "./parse-path" import { isLocalLink } from "./is-local-link" import { rewriteLinkPath } from "./rewrite-link-path" @@ -8,7 +17,7 @@ import { withPrefix, getGlobalPathPrefix } from "./prefix-helpers" export { parsePath, withPrefix } -export function withAssetPrefix(path) { +export function withAssetPrefix(path): string { return withPrefix(path, getGlobalPathPrefix()) } @@ -19,7 +28,10 @@ const NavLinkPropTypes = { } // Set up IntersectionObserver -const createIntersectionObserver = (el, cb) => { +const createIntersectionObserver = ( + el: Element, + cb: (arg0: boolean) => void +): { instance: IntersectionObserver; el: Element } => { const io = new window.IntersectionObserver(entries => { entries.forEach(entry => { if (el === entry.target) { @@ -36,30 +48,39 @@ const createIntersectionObserver = (el, cb) => { return { instance: io, el } } -function GatsbyLinkLocationWrapper(props) { - return ( - - {({ location }) => } - - ) +interface IGatsbyLinkProps extends HTMLAttributes { + _location: { + pathname: string + search: string + } + innerRef: MutableRefObject + to: string + target: string + partiallyActive: boolean + activeClassName: string + activeStyle: CSSProperties + getProps: (arg0: unknown) => HTMLAttributes | null + replace: boolean + state: Record } -class GatsbyLink extends React.Component { - constructor(props) { - super(props) - // Default to no support for IntersectionObserver - let IOSupported = false - if (typeof window !== `undefined` && window.IntersectionObserver) { - IOSupported = true - } +interface IGatsbyLinkState { + IOSupported: boolean +} - this.state = { - IOSupported, - } - this.abortPrefetch = null - this.handleRef = this.handleRef.bind(this) +class GatsbyLink extends React.Component { + readonly state: IGatsbyLinkState = { + IOSupported: + // Default to no support for IntersectionObserver + typeof window !== `undefined` && window.IntersectionObserver + ? true + : false, } + abortPrefetch: { abort: () => void } | null = null + io: { instance: IntersectionObserver; el: Element } | undefined + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type _prefetch() { let currentPath = window.location.pathname + window.location.search @@ -76,12 +97,13 @@ class GatsbyLink extends React.Component { // Prefetch is used to speed up next navigations. When you use it on the current navigation, // there could be a race-condition where Chrome uses the stale data instead of waiting for the network to complete if (currentPath !== newPathName) { - return ___loader.enqueue(newPathName) + return window.___loader.enqueue(newPathName) } return undefined } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type componentWillUnmount() { if (!this.io) { return @@ -96,6 +118,7 @@ class GatsbyLink extends React.Component { instance.disconnect() } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type handleRef(ref) { if ( this.props.innerRef && @@ -110,7 +133,7 @@ class GatsbyLink extends React.Component { // If IO supported and element reference found, setup Observer functionality this.io = createIntersectionObserver(ref, inViewPort => { if (inViewPort) { - this.abortPrefetch = this._prefetch() + this.abortPrefetch = this._prefetch() as { abort: () => void } | null } else { if (this.abortPrefetch) { this.abortPrefetch.abort() @@ -120,7 +143,10 @@ class GatsbyLink extends React.Component { } } - defaultGetProps = ({ isPartiallyCurrent, isCurrent }) => { + defaultGetProps = ({ + isPartiallyCurrent, + isCurrent, + }): HTMLAttributes | null => { if (this.props.partiallyActive ? isPartiallyCurrent : isCurrent) { return { className: [this.props.className, this.props.activeClassName] @@ -132,6 +158,7 @@ class GatsbyLink extends React.Component { return null } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type render() { const { to, @@ -167,14 +194,14 @@ class GatsbyLink extends React.Component { state={state} getProps={getProps} innerRef={this.handleRef} - onMouseEnter={e => { + onMouseEnter={(e): void => { if (onMouseEnter) { onMouseEnter(e) } const parsed = parsePath(prefixedTo) - ___loader.hovering(parsed.pathname + parsed.search) + window.___loader.hovering(parsed.pathname + parsed.search) }} - onClick={e => { + onClick={(e): boolean => { if (onClick) { onClick(e) } @@ -212,18 +239,26 @@ class GatsbyLink extends React.Component { } } -GatsbyLink.propTypes = { - ...NavLinkPropTypes, - onClick: PropTypes.func, - to: PropTypes.string.isRequired, - replace: PropTypes.bool, - state: PropTypes.object, +function GatsbyLinkLocationWrapper(props): ReactElement { + return ( + + {({ location }): ReactElement => ( + + )} + + ) +} + +export interface ILinkProps extends React.HTMLProps { + to: string + activeClassName: string + activeStyle: CSSProperties } -export const Link = React.forwardRef((props, ref) => ( +export const Link = React.forwardRef((props, ref) => ( )) -export const navigate = (to, options) => { +export const navigate = (to: string, options?: NavigateOptions): void => { window.___navigate(rewriteLinkPath(to, window.location.pathname), options) } diff --git a/packages/gatsby-link/src/is-local-link.js b/packages/gatsby-link/src/is-local-link.ts similarity index 72% rename from packages/gatsby-link/src/is-local-link.js rename to packages/gatsby-link/src/is-local-link.ts index b97c7a1915f5f..49cf7f0dceb0e 100644 --- a/packages/gatsby-link/src/is-local-link.js +++ b/packages/gatsby-link/src/is-local-link.ts @@ -1,8 +1,8 @@ // Copied from https://github.com/sindresorhus/is-absolute-url/blob/3ab19cc2e599a03ea691bcb8a4c09fa3ebb5da4f/index.js const ABSOLUTE_URL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/ -const isAbsolute = path => ABSOLUTE_URL_REGEX.test(path) +const isAbsolute = (path: string): boolean => ABSOLUTE_URL_REGEX.test(path) -export const isLocalLink = path => { +export const isLocalLink = (path: string): boolean | undefined => { if (typeof path !== `string`) { return undefined // TODO(v5): Re-Add TypeError diff --git a/packages/gatsby-link/src/parse-path.js b/packages/gatsby-link/src/parse-path.ts similarity index 86% rename from packages/gatsby-link/src/parse-path.js rename to packages/gatsby-link/src/parse-path.ts index 9e205e57e9690..11d1cfb288c8f 100644 --- a/packages/gatsby-link/src/parse-path.js +++ b/packages/gatsby-link/src/parse-path.ts @@ -1,4 +1,6 @@ -export function parsePath(path) { +import { Path } from "history" + +export function parsePath(path?: string): Path { let pathname = path || `/` let search = `` let hash = `` diff --git a/packages/gatsby-link/src/prefix-helpers.js b/packages/gatsby-link/src/prefix-helpers.ts similarity index 73% rename from packages/gatsby-link/src/prefix-helpers.js rename to packages/gatsby-link/src/prefix-helpers.ts index 054e417b314ca..4a6c079b0c626 100644 --- a/packages/gatsby-link/src/prefix-helpers.js +++ b/packages/gatsby-link/src/prefix-helpers.ts @@ -1,6 +1,9 @@ import { isLocalLink } from "./is-local-link" -export const getGlobalBasePrefix = () => +declare const __BASE_PATH__: string +declare const __PATH_PREFIX__: string + +export const getGlobalBasePrefix = (): string | undefined => process.env.NODE_ENV !== `production` ? typeof __BASE_PATH__ !== `undefined` ? __BASE_PATH__ @@ -9,14 +12,17 @@ export const getGlobalBasePrefix = () => // These global values are wrapped in typeof clauses to ensure the values exist. // This is especially problematic in unit testing of this component. -export const getGlobalPathPrefix = () => +export const getGlobalPathPrefix = (): string | undefined => process.env.NODE_ENV !== `production` ? typeof __PATH_PREFIX__ !== `undefined` ? __PATH_PREFIX__ : undefined : __PATH_PREFIX__ -export function withPrefix(path, prefix = getGlobalBasePrefix()) { +export function withPrefix( + path: string, + prefix = getGlobalBasePrefix() +): string { if (!isLocalLink(path)) { return path } diff --git a/packages/gatsby-link/src/rewrite-link-path.js b/packages/gatsby-link/src/rewrite-link-path.ts similarity index 65% rename from packages/gatsby-link/src/rewrite-link-path.js rename to packages/gatsby-link/src/rewrite-link-path.ts index e1b2b496716c7..3e33a21bc2e0d 100644 --- a/packages/gatsby-link/src/rewrite-link-path.js +++ b/packages/gatsby-link/src/rewrite-link-path.ts @@ -1,23 +1,28 @@ import { resolve } from "@gatsbyjs/reach-router" // Specific import to treeshake Node.js stuff -import { applyTrailingSlashOption } from "gatsby-page-utils/apply-trailing-slash-option" +import { applyTrailingSlashOption, type TrailingSlash } from "gatsby-page-utils" import { parsePath } from "./parse-path" import { isLocalLink } from "./is-local-link" import { withPrefix } from "./prefix-helpers" -const isAbsolutePath = path => path?.startsWith(`/`) +declare const __TRAILING_SLASH__: TrailingSlash | undefined -const getGlobalTrailingSlash = () => +const isAbsolutePath = (path: string): boolean => path?.startsWith(`/`) + +const getGlobalTrailingSlash = (): TrailingSlash | undefined => typeof __TRAILING_SLASH__ !== `undefined` ? __TRAILING_SLASH__ : undefined -function applyTrailingSlashOptionOnPathnameOnly(path, option) { +function applyTrailingSlashOptionOnPathnameOnly( + path: string, + option: TrailingSlash +): string { const { pathname, search, hash } = parsePath(path) const output = applyTrailingSlashOption(pathname, option) return `${output}${search}${hash}` } -function absolutify(path, current) { +function absolutify(path: string, current: string): string { // If it's already absolute, return as-is if (isAbsolutePath(path)) { return path @@ -33,7 +38,7 @@ function absolutify(path, current) { return absolutePath } -function applyPrefix(path) { +function applyPrefix(path: string): string { const prefixed = withPrefix(path) const option = getGlobalTrailingSlash() @@ -44,7 +49,9 @@ function applyPrefix(path) { return prefixed } -export const rewriteLinkPath = (path, relativeTo) => { +export type rewriteLinkPathType = (path: string, relativeTo: string) => string + +export const rewriteLinkPath: rewriteLinkPathType = (path, relativeTo) => { if (typeof path === `number`) { return path } diff --git a/packages/gatsby-link/tsconfig.json b/packages/gatsby-link/tsconfig.json new file mode 100644 index 0000000000000..4082f16a5d91c --- /dev/null +++ b/packages/gatsby-link/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +}