refactor(client): use schema snapshot to avoid costly inference (#65360)

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
Oliver Eyton-Williams
2026-01-21 17:22:20 +01:00
committed by GitHub
parent ec56d7a74e
commit 6770ee4c26
6 changed files with 501 additions and 197 deletions
+8 -1
View File
@@ -101,6 +101,13 @@ module.exports = {
}
}
},
'gatsby-plugin-remove-serviceworker'
'gatsby-plugin-remove-serviceworker',
{
resolve: 'gatsby-plugin-schema-snapshot',
options: {
path: 'schema.gql',
update: process.env.GATSBY_UPDATE_SCHEMA_SNAPSHOT === 'true'
}
}
]
};
-192
View File
@@ -290,195 +290,3 @@ exports.onCreatePage = async ({ page, actions }) => {
createPage(page);
}
};
// Take care to QA the challenges when modifying this. It has broken certain
// types of challenge in the past.
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;
const typeDefs = `
type ChallengeNode implements Node {
challenge: Challenge
}
type Challenge {
assignments: [String]
bilibiliIds: BilibiliIds
block: String
blockId: String
blockLayout: String
blockLabel: String
certification: String
challengeFiles: [FileContents]
challengeOrder: Int
challengeType: Int
chapter: String
dashedName: String
demoType: String
description: String
disableLoopProtectPreview: Boolean
disableLoopProtectTests: Boolean
explanation: String
fillInTheBlank: FillInTheBlank
forumTopicId: Int
hasEditableBoundaries: Boolean
helpCategory: String
hooks: Hooks
id: String
instructions: String
isLastChallengeInBlock: Boolean
isPrivate: Boolean
lang: String
module: String
msTrophyId: String
nodules: [Nodule]
notes: String
order: Int
prerequisites: [PrerequisiteChallenge]
questions: [Question]
quizzes: [Quiz]
required: [RequiredResource]
saveSubmissionToDB: Boolean
scene: Scene
solutions: [[FileContents]]
suborder: Int
superBlock: String
superOrder: Int
template: String
tests: [Test]
fields: ChallengeFields
title: String
transcript: String
translationPending: Boolean
url: String
usesMultifileEditor: Boolean
videoId: String
videoLocaleIds: VideoLocaleIds
videoUrl: String
}
type FileContents {
fileKey: String
ext: String
name: String
contents: String
head: String
tail: String
editableRegionBoundaries: [Int]
path: String
error: String
seed: String
id: String
history: [String]
}
type PrerequisiteChallenge {
id: String
title: String
}
type VideoLocaleIds {
espanol: String
italian: String
portuguese: String
}
type BilibiliIds {
aid: Int
bvid: String
cid: Int
}
type Question {
text: String
answers: [Answer]
solution: Int
}
type Answer {
answer: String
feedback: String
audioId: String
}
type RequiredResource {
link: String
raw: Boolean
src: String
crossDomain: Boolean
}
type Hooks {
beforeAll: String
beforeEach: String
afterAll: String
afterEach: String
}
type Test {
id: String
text: String
testString: String
title: String
}
type FillInTheBlank {
sentence: String
blanks: [Blank]
inputType: String
}
type Blank {
answer: String
feedback: String
}
type Scene {
setup: SceneSetup
commands: [SceneCommands]
}
type SceneSetup {
background: String
characters: [SetupCharacter]
audio: SetupAudio
alwaysShowDialogue: Boolean
}
type SetupCharacter {
character: String
position: CharacterPosition
opacity: Float
}
type SetupAudio {
filename: String
startTime: Float
startTimestamp: Float
finishTimestamp: Float
}
type SceneCommands {
background: String
character: String
position: CharacterPosition
opacity: Float
startTime: Float
finishTime: Float
dialogue: Dialogue
}
type Dialogue {
text: String
align: String
}
type CharacterPosition {
x: Float
y: Float
z: Float
}
type Quiz {
questions: [QuizQuestion]
}
type QuizQuestion {
text: String
distractors: [String]
answer: String
}
type Hooks {
beforeEach: String
afterEach: String
beforeAll: String
afterAll: String
}
type ChallengeFields {
slug: String
}
type Nodule {
type: String
data: JSON
}
`;
createTypes(typeDefs);
};
+1
View File
@@ -176,6 +176,7 @@
"dotenv": "16.4.5",
"eslint": "^9.39.1",
"eslint-plugin-flowtype": "^8.0.3",
"gatsby-plugin-schema-snapshot": "2.15.0",
"gatsby-plugin-webpack-bundle-analyser-v2": "1.1.32",
"i18next-fs-backend": "2.6.0",
"joi": "17.12.2",
+467
View File
@@ -0,0 +1,467 @@
### Type definitions saved at 2026-01-20T15:53:20.990Z ###
type File implements Node @dontInfer {
sourceInstanceName: String!
absolutePath: String!
relativePath: String!
extension: String!
size: Int!
prettySize: String!
modifiedTime: Date! @dateformat
accessTime: Date! @dateformat
changeTime: Date! @dateformat
birthTime: Date! @dateformat
root: String!
dir: String!
base: String!
ext: String!
name: String!
relativeDirectory: String!
dev: Int!
mode: Int!
nlink: Int!
uid: Int!
gid: Int!
rdev: Int!
ino: Float!
atimeMs: Float!
mtimeMs: Float!
ctimeMs: Float!
atime: Date! @dateformat
mtime: Date! @dateformat
ctime: Date! @dateformat
birthtime: Date @deprecated(reason: "Use `birthTime` instead")
birthtimeMs: Float @deprecated(reason: "Use `birthTime` instead")
blksize: Int
blocks: Int
}
type Directory implements Node @dontInfer {
sourceInstanceName: String!
absolutePath: String!
relativePath: String!
extension: String!
size: Int!
prettySize: String!
modifiedTime: Date! @dateformat
accessTime: Date! @dateformat
changeTime: Date! @dateformat
birthTime: Date! @dateformat
root: String!
dir: String!
base: String!
ext: String!
name: String!
relativeDirectory: String!
dev: Int!
mode: Int!
nlink: Int!
uid: Int!
gid: Int!
rdev: Int!
ino: Float!
atimeMs: Float!
mtimeMs: Float!
ctimeMs: Float!
atime: Date! @dateformat
mtime: Date! @dateformat
ctime: Date! @dateformat
birthtime: Date @deprecated(reason: "Use `birthTime` instead")
birthtimeMs: Float @deprecated(reason: "Use `birthTime` instead")
blksize: Int
blocks: Int
}
type Site implements Node @dontInfer {
buildTime: Date @dateformat
siteMetadata: SiteSiteMetadata
port: Int
host: String
flags: SiteFlags
pathPrefix: String
polyfill: Boolean
}
type SiteSiteMetadata {
title: String
description: String
siteUrl: String
}
type SiteFlags {
DEV_SSR: Boolean
}
type SiteFunction implements Node @dontInfer {
functionRoute: String!
pluginName: String!
originalAbsoluteFilePath: String!
originalRelativeFilePath: String!
relativeCompiledFilePath: String!
absoluteCompiledFilePath: String!
matchPath: String
}
type SitePage implements Node @dontInfer {
path: String!
component: String!
internalComponentName: String!
componentChunkName: String!
matchPath: String
}
type SitePlugin implements Node @dontInfer {
resolve: String
name: String
version: String
nodeAPIs: [String]
browserAPIs: [String]
ssrAPIs: [String]
pluginFilepath: String
pluginOptions: SitePluginPluginOptions
packageJson: SitePluginPackageJson
}
type SitePluginPluginOptions {
analyzerMode: String
postcssOptions: SitePluginPluginOptionsPostcssOptions
prefixes: [String]
name: String
curriculumPath: String
path: String
jsFrontmatterEngine: Boolean
identity: String
pathCheck: Boolean
allExtensions: Boolean
isTSX: Boolean
jsxPragma: String
}
type SitePluginPluginOptionsPostcssOptions {
config: String
}
type SitePluginPackageJson {
name: String
description: String
version: String
main: String
author: String
license: String
dependencies: [SitePluginPackageJsonDependencies]
devDependencies: [SitePluginPackageJsonDevDependencies]
peerDependencies: [SitePluginPackageJsonPeerDependencies]
keywords: [String]
}
type SitePluginPackageJsonDependencies {
name: String
version: String
}
type SitePluginPackageJsonDevDependencies {
name: String
version: String
}
type SitePluginPackageJsonPeerDependencies {
name: String
version: String
}
type SiteBuildMetadata implements Node @dontInfer {
buildTime: Date @dateformat
}
type MarkdownHeading {
id: String
value: String
depth: Int
}
enum MarkdownHeadingLevels {
h1
h2
h3
h4
h5
h6
}
enum MarkdownExcerptFormats {
PLAIN
HTML
MARKDOWN
}
type MarkdownWordCount {
paragraphs: Int
sentences: Int
words: Int
}
type MarkdownRemark implements Node
@childOf(mimeTypes: ["text/markdown", "text/x-markdown"], types: ["File"])
@dontInfer {
frontmatter: MarkdownRemarkFrontmatter
excerpt: String
rawMarkdownBody: String
fileAbsolutePath: String
fields: MarkdownRemarkFields
}
type MarkdownRemarkFrontmatter {
title: String
superBlock: String
certification: String
block: String
}
type MarkdownRemarkFields {
nodeIdentity: String
slug: String
}
type ChallengeNode implements Node @dontInfer {
challenge: Challenge
sourceInstanceName: String
}
type Challenge {
assignments: [String]
bilibiliIds: BilibiliIds
block: String
blockId: String
blockLayout: String
blockLabel: String
certification: String
challengeFiles: [FileContents]
challengeOrder: Int
challengeType: Int
chapter: String
dashedName: String
demoType: String
description: String
disableLoopProtectPreview: Boolean
disableLoopProtectTests: Boolean
explanation: String
fillInTheBlank: FillInTheBlank
forumTopicId: Int
hasEditableBoundaries: Boolean
helpCategory: String
hooks: Hooks
id: String
instructions: String
isLastChallengeInBlock: Boolean
isPrivate: Boolean
lang: String
module: String
msTrophyId: String
nodules: [Nodule]
notes: String
order: Int
prerequisites: [PrerequisiteChallenge]
questions: [Question]
quizzes: [Quiz]
required: [RequiredResource]
saveSubmissionToDB: Boolean
scene: Scene
solutions: [[FileContents]]
suborder: Int
superBlock: String
superOrder: Int
template: String
tests: [Test]
fields: ChallengeFields
title: String
transcript: String
translationPending: Boolean
url: String
usesMultifileEditor: Boolean
videoId: String
videoLocaleIds: VideoLocaleIds
videoUrl: String
isExam: Boolean
showSpeakingButton: Boolean
inputType: String
}
type BilibiliIds {
aid: Int
bvid: String
cid: Int
}
type FileContents {
fileKey: String
ext: String
name: String
contents: String
head: String
tail: String
editableRegionBoundaries: [Int]
path: String
error: String
seed: String
id: String
history: [String]
}
type FillInTheBlank {
sentence: String
blanks: [Blank]
inputType: String
}
type Blank {
answer: String
feedback: String
}
type Hooks {
beforeAll: String
beforeEach: String
afterAll: String
afterEach: String
}
type Nodule {
type: String
data: JSON
}
type PrerequisiteChallenge {
id: String
title: String
}
type Question {
text: String
answers: [Answer]
solution: Int
}
type Answer {
answer: String
feedback: String
audioId: String
}
type Quiz {
questions: [QuizQuestion]
}
type QuizQuestion {
text: String
distractors: [String]
answer: String
}
type RequiredResource {
link: String
raw: Boolean
src: String
crossDomain: Boolean
}
type Scene {
setup: SceneSetup
commands: [SceneCommands]
}
type SceneSetup {
background: String
characters: [SetupCharacter]
audio: SetupAudio
alwaysShowDialogue: Boolean
}
type SetupCharacter {
character: String
position: CharacterPosition
opacity: Float
}
type CharacterPosition {
x: Float
y: Float
z: Float
}
type SetupAudio {
filename: String
startTime: Float
startTimestamp: Float
finishTimestamp: Float
}
type SceneCommands {
background: String
character: String
position: CharacterPosition
opacity: Float
startTime: Float
finishTime: Float
dialogue: Dialogue
}
type Dialogue {
text: String
align: String
}
type Test {
id: String
text: String
testString: String
title: String
}
type ChallengeFields {
slug: String
blockHashSlug: String
}
type VideoLocaleIds {
espanol: String
italian: String
portuguese: String
}
type SuperBlockStructure implements Node @dontInfer {
blocks: [String]
superBlock: String
chapters: [SuperBlockStructureChapters]
}
type SuperBlockStructureChapters {
dashedName: String
modules: [SuperBlockStructureChaptersModules]
chapterType: String
comingSoon: Boolean
}
type SuperBlockStructureChaptersModules {
dashedName: String
blocks: [String]
moduleType: String
comingSoon: Boolean
}
type CertificateNode implements Node @dontInfer {
sourceInstanceName: String
challenge: CertificateNodeChallenge
}
type CertificateNodeChallenge {
id: String
title: String
certification: String
challengeType: Int
tests: [CertificateNodeChallengeTests]
}
type CertificateNodeChallengeTests {
id: String
title: String
}
+17 -4
View File
@@ -655,6 +655,9 @@ importers:
eslint-plugin-flowtype:
specifier: ^8.0.3
version: 8.0.3(@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.5))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.28.5))(eslint@9.39.2(jiti@2.6.1))
gatsby-plugin-schema-snapshot:
specifier: 2.15.0
version: 2.15.0(gatsby@3.15.0(@types/node@24.10.4)(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.9.3))
gatsby-plugin-webpack-bundle-analyser-v2:
specifier: 1.1.32
version: 1.1.32(gatsby@3.15.0(@types/node@24.10.4)(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.9.3))
@@ -8735,6 +8738,11 @@ packages:
gatsby-plugin-remove-serviceworker@1.0.0:
resolution: {integrity: sha512-8uQ/6PiM718BTZAgmQeEO6ULrJgLugmDVAkUGv5xxF0luBNrbboDgpsG0z1fbsotSDTzLWyULR0zzGNfWZaY7w==}
gatsby-plugin-schema-snapshot@2.15.0:
resolution: {integrity: sha512-98GZxFxvKaZaC9FQnDey5mnrU4OX/B5Fy8EILr2/Yvglb9vevr+SWSmcKTNrwaEix/MjfACsz/+sChn1ytxX5Q==}
peerDependencies:
gatsby: ^3.0.0-next.0
gatsby-plugin-typescript@3.15.0:
resolution: {integrity: sha512-xJYN9LqQnKCTCOXChAMSpG9fZXQ8e73SkJTkAaK3eKLGEN3olTSwCMov6rrK19dm/hfNin2RVomm1sahd3hefg==}
engines: {node: '>=12.13.0'}
@@ -21098,7 +21106,7 @@ snapshots:
'@types/yoga-layout@1.9.2': {}
'@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/experimental-utils': 4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
@@ -24076,7 +24084,7 @@ snapshots:
eslint-config-react-app@6.0.0(@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3))(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-flowtype@5.10.0(eslint@7.32.0))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0))(eslint-plugin-jsx-a11y@6.10.2(eslint@7.32.0))(eslint-plugin-react-hooks@4.6.0(eslint@7.32.0))(eslint-plugin-react@7.37.4(eslint@7.32.0))(eslint@7.32.0)(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
babel-eslint: 10.1.0(eslint@9.39.2(jiti@2.6.1))
confusing-browser-globals: 1.0.11
@@ -25180,6 +25188,11 @@ snapshots:
gatsby-plugin-remove-serviceworker@1.0.0: {}
gatsby-plugin-schema-snapshot@2.15.0(gatsby@3.15.0(@types/node@24.10.4)(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.9.3)):
dependencies:
'@babel/runtime': 7.27.3
gatsby: 3.15.0(@types/node@24.10.4)(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.9.3)
gatsby-plugin-typescript@3.15.0(gatsby@3.15.0(@types/node@24.10.4)(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.9.3)):
dependencies:
'@babel/core': 7.23.7
@@ -25187,7 +25200,7 @@ snapshots:
'@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.23.7)
'@babel/plugin-proposal-optional-chaining': 7.17.12(@babel/core@7.23.7)
'@babel/preset-typescript': 7.23.3(@babel/core@7.23.7)
'@babel/runtime': 7.23.9
'@babel/runtime': 7.27.3
babel-plugin-remove-graphql-queries: 3.15.0(@babel/core@7.23.7)(gatsby@3.15.0(@types/node@24.10.4)(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.9.3))
gatsby: 3.15.0(@types/node@24.10.4)(babel-eslint@10.1.0(eslint@9.39.2(jiti@2.6.1)))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.9.3)
transitivePeerDependencies:
@@ -25371,7 +25384,7 @@ snapshots:
'@nodelib/fs.walk': 1.2.8
'@pmmmwh/react-refresh-webpack-plugin': 0.4.3(react-refresh@0.9.0)(webpack@5.90.3)
'@types/http-proxy': 1.17.12
'@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 4.33.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@vercel/webpack-asset-relocator-loader': 1.7.3
address: 1.1.2
+8
View File
@@ -73,3 +73,11 @@ EMAIL_PROVIDER=nodemailer
SES_ID=
SES_SECRET=ses_secret_from_aws
SES_REGION=us-east-1
# ---------------------
# Client
# ---------------------
# Set to true if the Gatsby schema needs to be updated. E.g. you've added a new
# challenge property. After updating the schema (via pnpm develop) you can
# commit the changed schema and set this back to false.
GATSBY_UPDATE_SCHEMA_SNAPSHOT=false