Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(gatsby): Migrate query-watcher to ts #27324

Merged
merged 4 commits into from
Oct 9, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,62 @@
* - Whenever a query changes, re-run all pages that rely on this query.
***/

const chokidar = require(`chokidar`)

const path = require(`path`)
const { slash } = require(`gatsby-core-utils`)
import chokidar, { FSWatcher } from "chokidar"
import { Span } from "opentracing"

import path from "path"
import { slash } from "gatsby-core-utils"

import { store, emitter } from "../redux/"
import { boundActionCreators } from "../redux/actions"
import { IGatsbyStaticQueryComponents } from "../redux/types"
import queryCompiler from "./query-compiler"
import report from "gatsby-cli/lib/reporter"
import queryUtil from "./"
import { getGatsbyDependents } from "../utils/gatsby-dependents"

const { store, emitter } = require(`../redux/`)
const { boundActionCreators } = require(`../redux/actions`)
const queryCompiler = require(`./query-compiler`).default
const report = require(`gatsby-cli/lib/reporter`)
const queryUtil = require(`./index`)
const debug = require(`debug`)(`gatsby:query-watcher`)
import { getGatsbyDependents } from "../utils/gatsby-dependents"

const getQueriesSnapshot = () => {
interface IComponent {
componentPath: string
query: string
pages: Set<string>
isInBootstrap: boolean
}

interface IQuery {
id: string
name: string
text: string
originalText: string
path: string
isHook: boolean
isStaticQuery: boolean
hash: string
}

interface IQuerySnapshot {
components: Map<string, IComponent>
staticQueryComponents: Map<string, IGatsbyStaticQueryComponents>
}

const getQueriesSnapshot = (): IQuerySnapshot => {
const state = store.getState()

const snapshot = {
components: new Map(state.components),
staticQueryComponents: new Map(state.staticQueryComponents),
const snapshot: IQuerySnapshot = {
components: new Map<string, IComponent>(state.components),
staticQueryComponents: new Map<string, IGatsbyStaticQueryComponents>(
state.staticQueryComponents
),
}

return snapshot
}

const handleComponentsWithRemovedQueries = (
{ components, staticQueryComponents },
queries
) => {
{ staticQueryComponents }: IQuerySnapshot,
queries: Map<string, IQuery>
): void => {
// If a component had static query and it doesn't have it
// anymore - update the store
staticQueryComponents.forEach(c => {
Expand All @@ -51,10 +79,10 @@ const handleComponentsWithRemovedQueries = (
}

const handleQuery = (
{ components, staticQueryComponents },
query,
component
) => {
{ staticQueryComponents }: IQuerySnapshot,
query: IQuery,
component: string
): boolean => {
// If this is a static query
// Add action / reducer + watch staticquery files
if (query.isStaticQuery) {
Expand All @@ -68,8 +96,8 @@ const handleQuery = (
// format query text, but it doesn't mean that compiled text will change.
if (
isNewQuery ||
oldQuery.hash !== query.hash ||
oldQuery.query !== query.text
oldQuery?.hash !== query.hash ||
oldQuery?.query !== query.text
) {
boundActionCreators.replaceStaticQuery({
name: query.name,
Expand All @@ -94,75 +122,58 @@ const handleQuery = (
return false
}

const updateStateAndRunQueries = (isFirstRun, { parentSpan } = {}) => {
const snapshot = getQueriesSnapshot()
return queryCompiler({ parentSpan }).then(queries => {
// If there's an error while extracting queries, the queryCompiler returns false
// or zero results.
// Yeah, should probably be an error but don't feel like threading the error
// all the way here.
if (!queries || queries.size === 0) {
return null
}
handleComponentsWithRemovedQueries(snapshot, queries)

// Run action for each component
const { components } = snapshot
components.forEach(c => {
const { isStaticQuery = false, text = `` } =
queries.get(c.componentPath) || {}

boundActionCreators.queryExtracted({
componentPath: c.componentPath,
query: isStaticQuery ? `` : text,
})
})
const filesToWatch = new Set<string>()
let watcher: FSWatcher

let queriesWillNotRun = false
queries.forEach((query, component) => {
const queryWillRun = handleQuery(snapshot, query, component)

if (queryWillRun) {
watchComponent(component)
// Check if this is a page component.
// If it is and this is our first run during bootstrap,
// show a warning about having a query in a non-page component.
} else if (isFirstRun && !snapshot.components.has(component)) {
report.warn(
`The GraphQL query in the non-page component "${component}" will not be run.`
)
queriesWillNotRun = true
}
})
const watch = async (rootDir: string): Promise<void> => {
if (watcher) return

if (queriesWillNotRun) {
report.log(report.stripIndent`
const modulesThatUseGatsby = await getGatsbyDependents()

Exported queries are only executed for Page components. It's possible you're
trying to create pages in your gatsby-node.js and that's failing for some
reason.
const packagePaths = modulesThatUseGatsby.map(module => {
const filesRegex = `*.+(t|j)s?(x)`
const pathRegex = `/{${filesRegex},!(node_modules)/**/${filesRegex}}`
return slash(path.join(module.path, pathRegex))
})

If the failing component(s) is a regular component and not intended to be a page
component, you generally want to use a <StaticQuery> (https://gatsbyjs.org/docs/static-query)
instead of exporting a page query.
watcher = chokidar
.watch(
[slash(path.join(rootDir, `/src/**/*.{js,jsx,ts,tsx}`)), ...packagePaths],
{ ignoreInitial: true }
)
.on(`change`, path => {
emitter.emit(`SOURCE_FILE_CHANGED`, path)
})
.on(`add`, path => {
emitter.emit(`SOURCE_FILE_CHANGED`, path)
})
.on(`unlink`, path => {
emitter.emit(`SOURCE_FILE_CHANGED`, path)
})

If you're more experienced with GraphQL, you can also export GraphQL
fragments from components and compose the fragments in the Page component
query and pass data down into the child component — https://graphql.org/learn/queries/#fragments
filesToWatch.forEach(filePath => watcher.add(filePath))
}

`)
const watchComponent = (componentPath: string): void => {
// We don't start watching until mid-way through the bootstrap so ignore
// new components being added until then. This doesn't affect anything as
// when extractQueries is called from bootstrap, we make sure that all
// components are being watched.
if (
process.env.NODE_ENV !== `production` &&
!filesToWatch.has(componentPath)
) {
filesToWatch.add(componentPath)
if (watcher) {
watcher.add(componentPath)
}

queryUtil.runQueuedQueries()

return null
})
}
}

/**
* Removes components templates that aren't used by any page from redux store.
*/
const clearInactiveComponents = () => {
const clearInactiveComponents = (): void => {
const { components, pages } = store.getState()

const activeTemplates = new Set()
Expand All @@ -184,72 +195,7 @@ const clearInactiveComponents = () => {
})
}

exports.extractQueries = ({ parentSpan } = {}) => {
// Remove template components that point to not existing page templates.
// We need to do this, because components data is cached and there might
// be changes applied when development server isn't running. This is needed
// only in initial run, because during development state will be adjusted.
clearInactiveComponents()

return updateStateAndRunQueries(true, { parentSpan }).then(() => {
// During development start watching files to recompile & run
// queries on the fly.

// TODO: move this into a spawned service
if (process.env.NODE_ENV !== `production`) {
watch(store.getState().program.directory)
}
})
}

const filesToWatch = new Set()
let watcher
const watchComponent = componentPath => {
// We don't start watching until mid-way through the bootstrap so ignore
// new components being added until then. This doesn't affect anything as
// when extractQueries is called from bootstrap, we make sure that all
// components are being watched.
if (
process.env.NODE_ENV !== `production` &&
!filesToWatch.has(componentPath)
) {
filesToWatch.add(componentPath)
if (watcher) {
watcher.add(componentPath)
}
}
}

const watch = async rootDir => {
if (watcher) return

const modulesThatUseGatsby = await getGatsbyDependents()

const packagePaths = modulesThatUseGatsby.map(module => {
const filesRegex = `*.+(t|j)s?(x)`
const pathRegex = `/{${filesRegex},!(node_modules)/**/${filesRegex}}`
return slash(path.join(module.path, pathRegex))
})

watcher = chokidar
.watch(
[slash(path.join(rootDir, `/src/**/*.{js,jsx,ts,tsx}`)), ...packagePaths],
{ ignoreInitial: true }
)
.on(`change`, path => {
emitter.emit(`SOURCE_FILE_CHANGED`, path)
})
.on(`add`, path => {
emitter.emit(`SOURCE_FILE_CHANGED`, path)
})
.on(`unlink`, path => {
emitter.emit(`SOURCE_FILE_CHANGED`, path)
})

filesToWatch.forEach(filePath => watcher.add(filePath))
}

exports.startWatchDeletePage = () => {
export const startWatchDeletePage = (): void => {
emitter.on(`DELETE_PAGE`, action => {
const componentPath = slash(action.payload.component)
const { pages } = store.getState()
Expand All @@ -271,4 +217,87 @@ exports.startWatchDeletePage = () => {
})
}

exports.updateStateAndRunQueries = updateStateAndRunQueries
export const updateStateAndRunQueries = async (
isFirstRun: boolean,
{ parentSpan }: { parentSpan?: Span } = {}
): Promise<void> => {
const snapshot = getQueriesSnapshot()
const queries: Map<string, IQuery> = await queryCompiler({ parentSpan })
// If there's an error while extracting queries, the queryCompiler returns false
// or zero results.
// Yeah, should probably be an error but don't feel like threading the error
// all the way here.
if (!queries || queries.size === 0) {
return
}
handleComponentsWithRemovedQueries(snapshot, queries)

// Run action for each component
const { components } = snapshot
components.forEach(c => {
const { isStaticQuery = false, text = `` } =
queries.get(c.componentPath) || {}

boundActionCreators.queryExtracted({
componentPath: c.componentPath,
query: isStaticQuery ? `` : text,
})
})

let queriesWillNotRun = false
queries.forEach((query, component) => {
const queryWillRun = handleQuery(snapshot, query, component)

if (queryWillRun) {
watchComponent(component)
// Check if this is a page component.
// If it is and this is our first run during bootstrap,
// show a warning about having a query in a non-page component.
} else if (isFirstRun && !snapshot.components.has(component)) {
report.warn(
`The GraphQL query in the non-page component "${component}" will not be run.`
)
queriesWillNotRun = true
}
})

if (queriesWillNotRun) {
report.log(report.stripIndent`

Exported queries are only executed for Page components. It's possible you're
trying to create pages in your gatsby-node.js and that's failing for some
reason.

If the failing component(s) is a regular component and not intended to be a page
component, you generally want to use a <StaticQuery> (https://gatsbyjs.org/docs/static-query)
instead of exporting a page query.

If you're more experienced with GraphQL, you can also export GraphQL
fragments from components and compose the fragments in the Page component
query and pass data down into the child component — https://graphql.org/learn/queries/#fragments

`)
}

queryUtil.runQueuedQueries()
}

export const extractQueries = ({
parentSpan,
}: { parentSpan?: Span } = {}): Promise<void> => {
// Remove template components that point to not existing page templates.
// We need to do this, because components data is cached and there might
// be changes applied when development server isn't running. This is needed
// only in initial run, because during development state will be adjusted.
clearInactiveComponents()

return updateStateAndRunQueries(true, { parentSpan }).then(() => {
// During development start watching files to recompile & run
// queries on the fly.

// TODO: move this into a spawned service
if (process.env.NODE_ENV !== `production`) {
watch(store.getState().program.directory)
}
})
}