-
Notifications
You must be signed in to change notification settings - Fork 10.3k
/
query-watcher.ts
296 lines (257 loc) · 8.78 KB
/
query-watcher.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
/** *
* Jobs of this module
* - Maintain the list of components in the Redux store. So monitor new components
* and add/remove components.
* - Watch components for query changes and extract these and update the store.
* - Ensure all page queries are run as part of bootstrap and report back when
* this is done
* - Whenever a query changes, re-run all pages that rely on this query.
***/
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 { getGatsbyDependents } from "../utils/gatsby-dependents"
const debug = require(`debug`)(`gatsby:query-watcher`)
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: IQuerySnapshot = {
components: new Map<string, IComponent>(state.components),
staticQueryComponents: new Map<string, IGatsbyStaticQueryComponents>(
state.staticQueryComponents
),
}
return snapshot
}
const handleComponentsWithRemovedQueries = (
{ 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 => {
if (c.query !== `` && !queries.has(c.componentPath)) {
debug(`Static query was removed from ${c.componentPath}`)
store.dispatch({
type: `REMOVE_STATIC_QUERY`,
payload: c.id,
})
}
})
}
const handleQuery = (
{ staticQueryComponents }: IQuerySnapshot,
query: IQuery,
component: string
): boolean => {
// If this is a static query
// Add action / reducer + watch staticquery files
if (query.isStaticQuery) {
const oldQuery = staticQueryComponents.get(query.id)
const isNewQuery = !oldQuery
// Compare query text because text is compiled query with any attached
// fragments and we want to rerun queries if fragments are edited.
// Compare hash because hash is used for identyfing query and
// passing data to component in development. Hash can change if user will
// format query text, but it doesn't mean that compiled text will change.
if (
isNewQuery ||
oldQuery?.hash !== query.hash ||
oldQuery?.query !== query.text
) {
boundActionCreators.replaceStaticQuery({
name: query.name,
componentPath: query.path,
id: query.id,
query: query.text,
hash: query.hash,
})
debug(
`Static query in ${component} ${
isNewQuery ? `was added` : `has changed`
}.`
)
}
return true
}
return false
}
const filesToWatch = new Set<string>()
let watcher: FSWatcher
const watch = async (rootDir: string): Promise<void> => {
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))
}
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)
}
}
}
/**
* Removes components templates that aren't used by any page from redux store.
*/
const clearInactiveComponents = (): void => {
const { components, pages } = store.getState()
const activeTemplates = new Set()
pages.forEach(page => {
// Set will guarantee uniqueness of entries
activeTemplates.add(slash(page.component))
})
components.forEach(component => {
if (!activeTemplates.has(component.componentPath)) {
debug(
`${component.componentPath} component was removed because it isn't used by any page`
)
store.dispatch({
type: `REMOVE_STATIC_QUERIES_BY_TEMPLATE`,
payload: component,
})
}
})
}
export const startWatchDeletePage = (): void => {
emitter.on(`DELETE_PAGE`, action => {
const componentPath = slash(action.payload.component)
const { pages } = store.getState()
let otherPageWithTemplateExists = false
for (const page of pages.values()) {
if (slash(page.component) === componentPath) {
otherPageWithTemplateExists = true
break
}
}
if (!otherPageWithTemplateExists) {
store.dispatch({
type: `REMOVE_STATIC_QUERIES_BY_TEMPLATE`,
payload: {
componentPath,
},
})
}
})
}
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
`)
}
}
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)
}
})
}