Skip to content

Commit 6b39645

Browse files
authoredJul 2, 2020
feat(gatsby): Track static queries by template (#25120)
Avoid single char names Sort at traversal time Refactor reduce to forEach Refactor another reduce to forEach Fix type check
1 parent fae19ac commit 6b39645

35 files changed

+785
-8
lines changed
 

‎.circleci/config.yml

+8
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ jobs:
281281
- e2e-test:
282282
test_path: integration-tests/structured-logging
283283

284+
integration_tests_artifacts:
285+
executor: node
286+
steps:
287+
- e2e-test:
288+
test_path: integration-tests/artifacts
289+
284290
e2e_tests_path-prefix:
285291
<<: *e2e-executor
286292
environment:
@@ -570,6 +576,8 @@ workflows:
570576
<<: *e2e-test-workflow
571577
- integration_tests_structured_logging:
572578
<<: *e2e-test-workflow
579+
- integration_tests_artifacts:
580+
<<: *e2e-test-workflow
573581
- integration_tests_gatsby_cli:
574582
requires:
575583
- bootstrap

‎integration-tests/artifacts/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 gatsbyjs
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎integration-tests/artifacts/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Artifacts test suite
2+
3+
This integration test suite helps us assert some of the artifacts written out for a Gatsby build. The test suite runs a real Gatsby build and then checks the generated `public` directory for `page-data.json` files. Since we added static query hashes to `page-data.json` files, the tests in this suite assert whether the correct static query hashes are included in every respective page.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
const { spawn } = require(`child_process`)
2+
const path = require(`path`)
3+
const { murmurhash } = require(`babel-plugin-remove-graphql-queries`)
4+
const { readPageData } = require(`gatsby/dist/utils/page-data`)
5+
const { stripIgnoredCharacters } = require(`gatsby/graphql`)
6+
7+
jest.setTimeout(100000)
8+
9+
const publicDir = path.join(process.cwd(), `public`)
10+
11+
const gatsbyBin = path.join(`node_modules`, `.bin`, `gatsby`)
12+
13+
const titleQuery = `
14+
{
15+
site {
16+
siteMetadata {
17+
title
18+
}
19+
}
20+
}
21+
`
22+
23+
const authorQuery = `
24+
{
25+
site {
26+
siteMetadata {
27+
author
28+
}
29+
}
30+
}
31+
`
32+
33+
const githubQuery = `
34+
{
35+
site {
36+
siteMetadata {
37+
github
38+
}
39+
}
40+
}
41+
`
42+
43+
function hashQuery(query) {
44+
const text = stripIgnoredCharacters(query)
45+
const hash = murmurhash(text, `abc`)
46+
return String(hash)
47+
}
48+
49+
const globalQueries = [githubQuery]
50+
51+
describe(`Static Queries`, () => {
52+
beforeAll(async done => {
53+
const gatsbyProcess = spawn(gatsbyBin, [`build`], {
54+
stdio: [`inherit`, `inherit`, `inherit`, `inherit`],
55+
env: {
56+
...process.env,
57+
NODE_ENV: `production`,
58+
},
59+
})
60+
61+
gatsbyProcess.on(`exit`, exitCode => {
62+
done()
63+
})
64+
})
65+
66+
test(`are written correctly when inline`, async () => {
67+
const queries = [titleQuery, ...globalQueries]
68+
const pagePath = `/inline/`
69+
70+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
71+
72+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
73+
})
74+
75+
test(`are written correctly when imported`, async () => {
76+
const queries = [titleQuery, ...globalQueries]
77+
const pagePath = `/import/`
78+
79+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
80+
81+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
82+
})
83+
84+
test(`are written correctly when dynamically imported`, async () => {
85+
const queries = [titleQuery, ...globalQueries]
86+
const pagePath = `/dynamic/`
87+
88+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
89+
90+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
91+
})
92+
93+
test(`are written correctly in jsx`, async () => {
94+
const queries = [titleQuery, ...globalQueries]
95+
const pagePath = `/jsx/`
96+
97+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
98+
99+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
100+
})
101+
102+
test(`are written correctly in tsx`, async () => {
103+
const queries = [titleQuery, ...globalQueries]
104+
const pagePath = `/tsx/`
105+
106+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
107+
108+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
109+
})
110+
111+
test(`are written correctly in typescript`, async () => {
112+
const queries = [titleQuery, ...globalQueries]
113+
const pagePath = `/typescript/`
114+
115+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
116+
117+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
118+
})
119+
120+
test(`are written correctly when nesting imports`, async () => {
121+
const queries = [titleQuery, authorQuery, ...globalQueries]
122+
const pagePath = `/import-import/`
123+
124+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
125+
126+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
127+
})
128+
129+
test(`are written correctly when nesting dynamic imports`, async () => {
130+
const queries = [titleQuery, ...globalQueries]
131+
const pagePath = `/dynamic-dynamic/`
132+
133+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
134+
135+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
136+
})
137+
138+
test(`are written correctly when nesting a dynamic import in a regular import`, async () => {
139+
const queries = [titleQuery, authorQuery, ...globalQueries]
140+
const pagePath = `/import-dynamic/`
141+
142+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
143+
144+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
145+
})
146+
147+
test(`are written correctly when nesting a regular import in a dynamic import`, async () => {
148+
const queries = [titleQuery, ...globalQueries]
149+
const pagePath = `/dynamic-import/`
150+
151+
const { staticQueryHashes } = await readPageData(publicDir, pagePath)
152+
153+
expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
154+
})
155+
156+
// test(`are written correctly when using wrapRootElement`, async () => {
157+
// const queries = [titleQuery]
158+
// const pagePath = `/dynamic-import/`
159+
160+
// const { staticQueryHashes } = await readPageData(publicDir, pagePath)
161+
162+
// expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
163+
// })
164+
165+
// test(`are written correctly when using wrapPageElement`, async () => {
166+
// const queries = [titleQuery]
167+
// const pagePath = `/dynamic-import/`
168+
169+
// const { staticQueryHashes } = await readPageData(publicDir, pagePath)
170+
171+
// expect(staticQueryHashes.sort()).toEqual(queries.map(hashQuery).sort())
172+
// })
173+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const React = require(`react`)
2+
const Github = require(`./src/components/github`).default
3+
4+
exports.wrapPageElement = ({ element }) => (
5+
<>
6+
<Github />
7+
{element}
8+
</>
9+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
siteMetadata: {
3+
title: `Hello world`,
4+
author: `Sid Chatterjee`,
5+
twitter: `chatsidhartha`,
6+
github: `sidharthachatterjee`,
7+
},
8+
plugins: [],
9+
}

‎integration-tests/artifacts/gatsby-ssr.js

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
testPathIgnorePatterns: [`/node_modules/`, `__tests__/fixtures`, `.cache`],
3+
}
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "artifacts",
3+
"private": true,
4+
"author": "Sid Chatterjee",
5+
"description": "A simplified bare-bones starter for Gatsby",
6+
"version": "0.1.0",
7+
"license": "MIT",
8+
"scripts": {
9+
"build": "gatsby build",
10+
"develop": "gatsby develop",
11+
"serve": "gatsby serve",
12+
"clean": "gatsby clean",
13+
"test": "jest --config=jest.config.js --runInBand"
14+
},
15+
"dependencies": {
16+
"gatsby": "^2.22.0",
17+
"react": "^16.12.0",
18+
"react-dom": "^16.12.0"
19+
},
20+
"devDependencies": {
21+
"fs-extra": "^9.0.0",
22+
"jest": "^24.0.0",
23+
"jest-cli": "^24.0.0"
24+
},
25+
"repository": {
26+
"type": "git",
27+
"url": "https://github.com/gatsbyjs/gatsby-starter-hello-world"
28+
},
29+
"bugs": {
30+
"url": "https://github.com/gatsbyjs/gatsby/issues"
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react"
2+
import { useStaticQuery, graphql } from "gatsby"
3+
4+
export default function Author() {
5+
const { site } = useStaticQuery(graphql`
6+
{
7+
site {
8+
siteMetadata {
9+
author
10+
}
11+
}
12+
}
13+
`)
14+
return <div>{site.siteMetadata.author}</div>
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react"
2+
import { useStaticQuery, graphql } from "gatsby"
3+
4+
export default function Github() {
5+
const { site } = useStaticQuery(graphql`
6+
{
7+
site {
8+
siteMetadata {
9+
github
10+
}
11+
}
12+
}
13+
`)
14+
return <div>{site.siteMetadata.github}</div>
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react"
2+
import { useTitle } from "../hooks/use-title"
3+
4+
export default function Title() {
5+
const title = useTitle()
6+
return <div>{title}</div>
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react"
2+
import { useStaticQuery, graphql } from "gatsby"
3+
4+
export default function Twitter() {
5+
const { site } = useStaticQuery(graphql`
6+
{
7+
site {
8+
siteMetadata {
9+
twitter
10+
}
11+
}
12+
}
13+
`)
14+
return <div>{site.siteMetadata.twitter}</div>
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useStaticQuery, graphql } from "gatsby"
2+
3+
export const useTitle = () => {
4+
const { site } = useStaticQuery(graphql`
5+
{
6+
site {
7+
siteMetadata {
8+
title
9+
}
10+
}
11+
}
12+
`)
13+
return site.siteMetadata.title
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { Component } from "react"
2+
3+
export default class Dynamic extends Component {
4+
constructor(props) {
5+
super(props)
6+
this.state = { module: null }
7+
}
8+
componentDidMount() {
9+
import(`../pages/dynamic`).then(module =>
10+
this.setState({ module: module.default })
11+
)
12+
}
13+
render() {
14+
const { module: Component } = this.state
15+
return <div>{Component && <Component />}</div>
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { Component } from "react"
2+
3+
export default class Dynamic extends Component {
4+
constructor(props) {
5+
super(props)
6+
this.state = { module: null }
7+
}
8+
componentDidMount() {
9+
import(`../pages/import`).then(module =>
10+
this.setState({ module: module.default })
11+
)
12+
}
13+
render() {
14+
const { module: Component } = this.state
15+
return <div>{Component && <Component />}</div>
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { Component } from "react"
2+
3+
export default class Dynamic extends Component {
4+
constructor(props) {
5+
super(props)
6+
this.state = { module: null }
7+
}
8+
componentDidMount() {
9+
import(`../components/title`).then(module =>
10+
this.setState({ module: module.default })
11+
)
12+
}
13+
render() {
14+
const { module: Component } = this.state
15+
return <div>{Component && <Component />}</div>
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from "react"
2+
import Author from "../components/author"
3+
import Dynamic from "../pages/dynamic"
4+
5+
export default function ImportImport() {
6+
return (
7+
<>
8+
<Author />
9+
<Dynamic />
10+
</>
11+
)
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from "react"
2+
import Import from "./import"
3+
import Author from "../components/author"
4+
5+
export default function ImportImport() {
6+
return (
7+
<>
8+
<Author />
9+
<Import />
10+
</>
11+
)
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react"
2+
import { useTitle } from "../hooks/use-title"
3+
4+
export default function Import() {
5+
const title = useTitle()
6+
return <div>{title}</div>
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react"
2+
import { useStaticQuery, graphql } from "gatsby"
3+
4+
export default function Inline() {
5+
const { site } = useStaticQuery(graphql`
6+
{
7+
site {
8+
siteMetadata {
9+
title
10+
}
11+
}
12+
}
13+
`)
14+
return <div>{site.siteMetadata.title}</div>
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react"
2+
import { useTitle } from "../hooks/use-title"
3+
4+
export default function Jsx() {
5+
const title = useTitle()
6+
return <div>{title}</div>
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react"
2+
import { useTitle } from "../hooks/use-title"
3+
4+
export default function Tsx(): any {
5+
const title = useTitle()
6+
return <div>{title}</div>
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react"
2+
import { useTitle } from "../hooks/use-title"
3+
4+
export default function Ts(): any {
5+
const title = useTitle()
6+
return title
7+
}

‎packages/babel-plugin-remove-graphql-queries/src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -432,4 +432,5 @@ export {
432432
StringInterpolationNotAllowedError,
433433
EmptyGraphQLTagError,
434434
GraphQLSyntaxError,
435+
murmurhash,
435436
}

‎packages/gatsby/src/commands/build-javascript.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { Span } from "opentracing"
22
import webpack from "webpack"
3+
import { isEqual } from "lodash"
34
import flatMap from "lodash/flatMap"
45

56
import webpackConfig from "../utils/webpack.config"
7+
import { store } from "../redux"
8+
import { mapTemplatesToStaticQueryHashes } from "../utils/map-pages-to-static-query-hashes"
69

710
import { IProgram } from "./types"
811

@@ -23,7 +26,44 @@ export const buildProductionBundle = async (
2326
)
2427

2528
return new Promise((resolve, reject) => {
26-
webpack(compilerConfig).run((err, stats) => {
29+
const compiler = webpack(compilerConfig)
30+
31+
compiler.hooks.compilation.tap(`webpack-dep-tree-plugin`, compilation => {
32+
compilation.hooks.seal.tap(`webpack-dep-tree-plugin`, () => {
33+
const state = store.getState()
34+
const mapOfTemplatesToStaticQueryHashes = mapTemplatesToStaticQueryHashes(
35+
state,
36+
compilation
37+
)
38+
39+
mapOfTemplatesToStaticQueryHashes.forEach(
40+
(staticQueryHashes, componentPath) => {
41+
if (
42+
!isEqual(
43+
state.staticQueriesByTemplate.get(componentPath),
44+
staticQueryHashes.map(String)
45+
)
46+
) {
47+
store.dispatch({
48+
type: `ADD_PENDING_TEMPLATE_DATA_WRITE`,
49+
payload: {
50+
componentPath,
51+
},
52+
})
53+
store.dispatch({
54+
type: `SET_STATIC_QUERIES_BY_TEMPLATE`,
55+
payload: {
56+
componentPath,
57+
staticQueryHashes,
58+
},
59+
})
60+
}
61+
}
62+
)
63+
})
64+
})
65+
66+
compiler.run((err, stats) => {
2767
if (err) {
2868
return reject(err)
2969
}

‎packages/gatsby/src/commands/develop-process.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ module.exports = async (program: IProgram): Promise<void> => {
204204
})
205205
queryWatcher.startWatchDeletePage()
206206

207-
await startWebpackServer({ program, app, workerPool })
207+
await startWebpackServer({ program, app, workerPool, store })
208208
},
209209
},
210210
},

‎packages/gatsby/src/redux/__tests__/__snapshots__/index.js.snap

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Object {
5858
"pagePaths": Set {},
5959
"templatePaths": Set {},
6060
},
61+
"staticQueriesByTemplate": Map {},
6162
"staticQueryComponents": Map {},
6263
"status": Object {
6364
"PLUGINS_HASH": "",

‎packages/gatsby/src/redux/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export const saveState = (): void => {
8686
pageDataStats: state.pageDataStats,
8787
pageData: state.pageData,
8888
pendingPageDataWrites: state.pendingPageDataWrites,
89+
staticQueriesByTemplate: state.staticQueriesByTemplate,
8990
})
9091
}
9192

‎packages/gatsby/src/redux/reducers/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { flattenedPluginsReducer } from "./flattened-plugins"
2525
import { pendingPageDataWritesReducer } from "./pending-page-data-writes"
2626
import { schemaCustomizationReducer } from "./schema-customization"
2727
import { inferenceMetadataReducer } from "./inference-metadata"
28+
import { staticQueriesByTemplateReducer } from "./static-queries-by-template"
2829

2930
/**
3031
* @property exports.nodesTouched Set<string>
@@ -57,4 +58,5 @@ export {
5758
pageDataStatsReducer as pageDataStats,
5859
pageDataReducer as pageData,
5960
pendingPageDataWritesReducer as pendingPageDataWrites,
61+
staticQueriesByTemplateReducer as staticQueriesByTemplate,
6062
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ActionsUnion, IGatsbyState } from "../types"
2+
3+
export const staticQueriesByTemplateReducer = (
4+
state: IGatsbyState["staticQueriesByTemplate"] = new Map(),
5+
action: ActionsUnion
6+
): IGatsbyState["staticQueriesByTemplate"] => {
7+
switch (action.type) {
8+
case `REMOVE_TEMPLATE_COMPONENT`:
9+
state.delete(action.payload.componentPath)
10+
return state
11+
12+
case `SET_STATIC_QUERIES_BY_TEMPLATE`: {
13+
return state.set(
14+
action.payload.componentPath,
15+
action.payload.staticQueryHashes
16+
)
17+
}
18+
19+
default:
20+
return state
21+
}
22+
}

‎packages/gatsby/src/redux/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export interface IGatsbyState {
199199
IGatsbyStaticQueryComponents["id"],
200200
IGatsbyStaticQueryComponents
201201
>
202+
staticQueriesByTemplate: Map<SystemPath, Identifier[]>
202203
pendingPageDataWrites: {
203204
pagePaths: Set<string>
204205
templatePaths: Set<SystemPath>
@@ -255,6 +256,7 @@ export interface ICachedReduxState {
255256
webpackCompilationHash: IGatsbyState["webpackCompilationHash"]
256257
pageDataStats: IGatsbyState["pageDataStats"]
257258
pageData: IGatsbyState["pageData"]
259+
staticQueriesByTemplate: IGatsbyState["staticQueriesByTemplate"]
258260
pendingPageDataWrites: IGatsbyState["pendingPageDataWrites"]
259261
}
260262

@@ -304,6 +306,7 @@ export type ActionsUnion =
304306
| ICreateJobAction
305307
| ISetJobAction
306308
| IEndJobAction
309+
| ISetStaticQueriesByTemplateAction
307310
| IAddPendingPageDataWriteAction
308311
| IAddPendingTemplateDataWriteAction
309312
| IClearPendingPageDataWritesAction
@@ -579,6 +582,14 @@ export interface IRemoveTemplateComponentAction {
579582
}
580583
}
581584

585+
export interface ISetStaticQueriesByTemplateAction {
586+
type: `SET_STATIC_QUERIES_BY_TEMPLATE`
587+
payload: {
588+
componentPath: string
589+
staticQueryHashes: Identifier[]
590+
}
591+
}
592+
582593
export interface IAddPendingPageDataWriteAction {
583594
type: `ADD_PENDING_PAGE_DATA_WRITE`
584595
payload: {

‎packages/gatsby/src/services/start-webpack-server.ts

+42-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import report from "gatsby-cli/lib/reporter"
33
import formatWebpackMessages from "react-dev-utils/formatWebpackMessages"
44
import chalk from "chalk"
55
import { Compiler } from "webpack"
6+
import { isEqual } from "lodash"
67

78
import {
89
reportWebpackWarnings,
@@ -20,17 +21,19 @@ import {
2021
markWebpackStatusAsDone,
2122
} from "../utils/webpack-status"
2223
import { enqueueFlush } from "../utils/page-data"
24+
import { mapTemplatesToStaticQueryHashes } from "../utils/map-pages-to-static-query-hashes"
2325

2426
export async function startWebpackServer({
2527
program,
2628
app,
2729
workerPool,
30+
store,
2831
}: Partial<IBuildContext>): Promise<{
2932
compiler: Compiler
3033
websocketManager: WebsocketManager
3134
}> {
32-
if (!program || !app) {
33-
throw new Error(`Missing required params`)
35+
if (!program || !app || !store) {
36+
report.panic(`Missing required params`)
3437
}
3538
let { compiler, webpackActivity, websocketManager } = await startServer(
3639
program,
@@ -109,9 +112,45 @@ export async function startWebpackServer({
109112
webpackActivity.end()
110113
webpackActivity = null
111114
}
112-
enqueueFlush()
115+
116+
if (isSuccessful) {
117+
const state = store.getState()
118+
const mapOfTemplatesToStaticQueryHashes = mapTemplatesToStaticQueryHashes(
119+
state,
120+
stats.compilation
121+
)
122+
123+
mapOfTemplatesToStaticQueryHashes.forEach(
124+
(staticQueryHashes, componentPath) => {
125+
if (
126+
!isEqual(
127+
state.staticQueriesByTemplate.get(componentPath),
128+
staticQueryHashes.map(String)
129+
)
130+
) {
131+
store.dispatch({
132+
type: `ADD_PENDING_TEMPLATE_DATA_WRITE`,
133+
payload: {
134+
componentPath,
135+
},
136+
})
137+
store.dispatch({
138+
type: `SET_STATIC_QUERIES_BY_TEMPLATE`,
139+
payload: {
140+
componentPath,
141+
staticQueryHashes,
142+
},
143+
})
144+
}
145+
}
146+
)
147+
148+
enqueueFlush()
149+
}
150+
113151
markWebpackStatusAsDone()
114152
done()
153+
115154
resolve({ compiler, websocketManager })
116155
})
117156
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { uniqBy, List } from "lodash"
2+
import { IGatsbyState } from "../redux/types"
3+
import { Stats } from "webpack"
4+
5+
interface ICompilation {
6+
modules: IModule[]
7+
}
8+
9+
interface IReason extends Omit<Stats.Reason, "module"> {
10+
module: IModule
11+
}
12+
13+
interface IModule extends Omit<Stats.FnModules, "identifier" | "reasons"> {
14+
hasReasons: () => boolean
15+
resource?: string
16+
identifier: () => string
17+
reasons: IReason[]
18+
}
19+
20+
const mapComponentsToStaticQueryHashes = (
21+
staticQueryComponents: IGatsbyState["staticQueryComponents"]
22+
): Map<string, string> => {
23+
const map = new Map()
24+
25+
staticQueryComponents.forEach(({ componentPath, hash }) => {
26+
map.set(componentPath, hash)
27+
})
28+
29+
return map
30+
}
31+
32+
/* This function takes the current Redux state and a compilation
33+
* object from webpack and returns a map of unique templates
34+
* to static queries included in each (as hashes).
35+
*
36+
* This isn't super straightforward because templates may include
37+
* deep component trees with static queries present at any depth.
38+
* This is why it is necessary to map templates to all their (user land and node_modules)
39+
* dependencies first and then map those dependencies to known static queries.
40+
*
41+
* Also, Gatsby makes it possible to wrap an entire site or page with a layout
42+
* or other component(s) via the wrapRootElement and wrapPageElement APIs. These must
43+
* also be handled when computing static queries for a page.
44+
*
45+
* Let's go through the implementation step by step.
46+
*/
47+
export function mapTemplatesToStaticQueryHashes(
48+
reduxState: IGatsbyState,
49+
compilation: ICompilation
50+
): Map<string, Array<number>> {
51+
/* The `staticQueryComponents` slice of state is useful because
52+
* it is a pre extracted collection of all static queries found in a Gatsby site.
53+
* This lets us traverse upwards from those to templates that
54+
* may contain components that contain them.
55+
* Note that this upward traversal is much shallower (and hence more performant)
56+
* than an equivalent downward one from an entry point.
57+
*/
58+
const { components, staticQueryComponents } = reduxState
59+
const { modules } = compilation
60+
61+
/* When we traverse upwards, we need to know where to stop. We'll call these terminal nodes.
62+
* `async-requires.js` is the entry point for every page, while `api-runner-browser-plugins.js`
63+
* is the one for `gatsby-browser` (where one would use wrapRootElement or wrapPageElement APIs)
64+
*/
65+
const terminalNodes = [
66+
`.cache/api-runner-browser-plugins.js`,
67+
`.cache/async-requires.js`,
68+
]
69+
70+
/* We call the queries included above a page (via wrapRootElement or wrapPageElement APIs)
71+
* global queries. For now, we include these in every single page for simplicity. Overhead
72+
* here is not much since we are storing hashes (that reference separate result files)
73+
* as opposed to inlining results. We may move these to app-data perhaps in the future.
74+
*/
75+
const globalStaticQueries = new Set<string>()
76+
77+
/* This function takes a webpack module corresponding
78+
* to the file containing a static query and returns
79+
* a Set of strings, each an absolute path of a dependent
80+
* of this module
81+
*/
82+
const getDeps = (mod: IModule): Set<string> => {
83+
const staticQueryModuleComponentPath = mod.resource
84+
const result = new Set<string>()
85+
86+
// This is the body of the recursively called function
87+
const getDepsFn = (m: IModule): Set<string> => {
88+
// Reasons in webpack are literally reasons of why this module was included in the tree
89+
const hasReasons = m.hasReasons()
90+
91+
// Is this node one of our known terminal nodes? See explanation above
92+
const isTerminalNode = terminalNodes.some(terminalNode =>
93+
m?.resource?.includes(terminalNode)
94+
)
95+
96+
// Exit if we don't have any reasons or we have reached a possible terminal node
97+
if (!hasReasons || isTerminalNode) {
98+
return result
99+
}
100+
101+
// These are non terminal dependents and hence modules that need
102+
// further upward traversal
103+
const nonTerminalDependents: List<IModule> = m.reasons
104+
.filter(r => {
105+
const dependentModule = r.module
106+
const isTerminal = terminalNodes.some(terminalNode =>
107+
dependentModule?.resource?.includes(terminalNode)
108+
)
109+
return !isTerminal
110+
})
111+
.map(r => r.module)
112+
.filter(Boolean)
113+
114+
const uniqDependents = uniqBy(nonTerminalDependents, d => d?.identifier())
115+
116+
for (const uniqDependent of uniqDependents) {
117+
if (uniqDependent.resource) {
118+
result.add(uniqDependent.resource)
119+
}
120+
121+
if (
122+
uniqDependent?.resource?.includes(`gatsby-browser.js`) &&
123+
staticQueryModuleComponentPath
124+
) {
125+
globalStaticQueries.add(staticQueryModuleComponentPath)
126+
}
127+
getDepsFn(uniqDependent)
128+
}
129+
130+
return result
131+
}
132+
133+
return getDepsFn(mod)
134+
}
135+
136+
const mapOfStaticQueryComponentsToDependants = new Map()
137+
138+
// For every known static query, we get its dependents.
139+
staticQueryComponents.forEach(({ componentPath }) => {
140+
const staticQueryComponentModule = modules.find(
141+
m => m.resource === componentPath
142+
)
143+
144+
const dependants = staticQueryComponentModule
145+
? getDeps(staticQueryComponentModule)
146+
: new Set()
147+
148+
mapOfStaticQueryComponentsToDependants.set(componentPath, dependants)
149+
})
150+
151+
const mapOfComponentsToStaticQueryHashes = mapComponentsToStaticQueryHashes(
152+
staticQueryComponents
153+
)
154+
155+
const globalStaticQueryHashes: string[] = []
156+
157+
globalStaticQueries.forEach(q => {
158+
const hash = mapOfComponentsToStaticQueryHashes.get(q)
159+
if (hash) {
160+
globalStaticQueryHashes.push(hash)
161+
}
162+
})
163+
164+
// For every known page, we get queries
165+
const mapOfTemplatesToStaticQueryHashes = new Map()
166+
167+
components.forEach(page => {
168+
const staticQueryHashes = [...globalStaticQueryHashes]
169+
170+
// Does this page contain an inline static query?
171+
if (mapOfComponentsToStaticQueryHashes.has(page.componentPath)) {
172+
const hash = mapOfComponentsToStaticQueryHashes.get(page.componentPath)
173+
if (hash) {
174+
staticQueryHashes.push(hash)
175+
}
176+
}
177+
178+
// Check dependencies
179+
mapOfStaticQueryComponentsToDependants.forEach(
180+
(setOfDependants, staticQueryComponentPath) => {
181+
if (setOfDependants.has(page.componentPath)) {
182+
const hash = mapOfComponentsToStaticQueryHashes.get(
183+
staticQueryComponentPath
184+
)
185+
if (hash) {
186+
staticQueryHashes.push(hash)
187+
}
188+
}
189+
}
190+
)
191+
192+
mapOfTemplatesToStaticQueryHashes.set(
193+
page.componentPath,
194+
staticQueryHashes.sort()
195+
)
196+
})
197+
198+
return mapOfTemplatesToStaticQueryHashes
199+
}

‎packages/gatsby/src/utils/page-data.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface IPageData {
1111
componentChunkName: IGatsbyPage["componentChunkName"]
1212
matchPath?: IGatsbyPage["matchPath"]
1313
path: IGatsbyPage["path"]
14+
staticQueryHashes: string[]
1415
}
1516

1617
export interface IPageDataWithQueryResult extends IPageData {
@@ -55,7 +56,12 @@ export async function removePageData(
5556

5657
export async function writePageData(
5758
publicDir: string,
58-
{ componentChunkName, matchPath, path: pagePath }: IPageData
59+
{
60+
componentChunkName,
61+
matchPath,
62+
path: pagePath,
63+
staticQueryHashes,
64+
}: IPageData
5965
): Promise<IPageDataWithQueryResult> {
6066
const inputFilePath = path.join(
6167
publicDir,
@@ -71,6 +77,7 @@ export async function writePageData(
7177
path: pagePath,
7278
matchPath,
7379
result,
80+
staticQueryHashes,
7481
}
7582
const bodyStr = JSON.stringify(body)
7683
// transform asset size to kB (from bytes) to fit 64 bit to numbers
@@ -102,7 +109,13 @@ export async function flush(): Promise<void> {
102109
}
103110
isFlushPending = false
104111
isFlushing = true
105-
const { pendingPageDataWrites, components, pages, program } = store.getState()
112+
const {
113+
pendingPageDataWrites,
114+
components,
115+
pages,
116+
program,
117+
staticQueriesByTemplate,
118+
} = store.getState()
106119

107120
const { pagePaths, templatePaths } = pendingPageDataWrites
108121

@@ -127,9 +140,15 @@ export async function flush(): Promise<void> {
127140
// them, a page might not exist anymore щ(゚Д゚щ)
128141
// This is why we need this check
129142
if (page) {
143+
const staticQueryHashes =
144+
staticQueriesByTemplate.get(page.componentPath)?.map(String) || []
145+
130146
const result = await writePageData(
131147
path.join(program.directory, `public`),
132-
page
148+
{
149+
...page,
150+
staticQueryHashes,
151+
}
133152
)
134153

135154
if (program?._?.[0] === `develop`) {

0 commit comments

Comments
 (0)
Please sign in to comment.