diff --git a/Makefile b/Makefile
index efbd7cde3594a..2d23c26f72e67 100644
--- a/Makefile
+++ b/Makefile
@@ -46,6 +46,10 @@ clean:
lint:
npm run lint
+## Verify Gatsby image usage has required data.
+check-images:
+ npm run check:images
+
## Kill process running the site
kill:
lsof -ti:8000 | xargs kill -9 2>/dev/null || true
@@ -56,4 +60,4 @@ features:
node .github/build/features-to-json.js .github/build/spreadsheet.csv src/sections/Pricing/feature_data.json
rm .github/build/spreadsheet.csv
-.PHONY: setup build site site-full clean site-fast lint features
+.PHONY: setup build site clean site-fast lint check-images features
diff --git a/gatsby-node.js b/gatsby-node.js
index f75c3b023dc85..22c972306003b 100644
--- a/gatsby-node.js
+++ b/gatsby-node.js
@@ -843,6 +843,39 @@ exports.onCreateWebpackConfig = ({ actions, stage, getConfig }) => {
miniCssExtractPlugin.options.ignoreOrder = true;
}
+ if (stage === "build-javascript") {
+ const existingSplitChunks = config.optimization?.splitChunks || {};
+ const existingCacheGroups = existingSplitChunks.cacheGroups || {};
+
+ config.optimization = {
+ ...config.optimization,
+ runtimeChunk: { name: "runtime" },
+ splitChunks: {
+ chunks: "all",
+ ...existingSplitChunks,
+ maxInitialRequests: existingSplitChunks.maxInitialRequests ?? 25,
+ minSize: existingSplitChunks.minSize ?? 20000,
+ cacheGroups: {
+ ...existingCacheGroups,
+ vendor: {
+ test: /[\\/]node_modules[\\/]/,
+ name: "vendor",
+ chunks: "all",
+ enforce: true,
+ priority: 10,
+ },
+ framework: {
+ test: /[\\/](react|react-dom|gatsby|@emotion|styled-components)[\\/]/,
+ name: "framework",
+ chunks: "all",
+ enforce: true,
+ priority: 20,
+ },
+ },
+ },
+ };
+ }
+
actions.replaceWebpackConfig(config);
}
};
diff --git a/package-lock.json b/package-lock.json
index 8409ea2ae9e06..8b891f6a28f95 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -106,6 +106,8 @@
"@babel/cli": "^7.28.3",
"@babel/core": "^7.28.5",
"@babel/eslint-parser": "^7.28.5",
+ "@babel/parser": "^7.28.5",
+ "@babel/traverse": "^7.28.5",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.1",
"babel-plugin-module-resolver": "^5.0.0",
@@ -2979,7 +2981,9 @@
}
},
"node_modules/@expo/devcert/node_modules/glob": {
- "version": "10.4.5",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
@@ -17371,7 +17375,9 @@
}
},
"node_modules/gatsby/node_modules/js-yaml": {
- "version": "3.14.1",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -17960,7 +17966,9 @@
}
},
"node_modules/gray-matter/node_modules/js-yaml": {
- "version": "3.14.1",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -24080,7 +24088,10 @@
}
},
"node_modules/min-document": {
- "version": "2.19.0",
+ "version": "2.19.2",
+ "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz",
+ "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==",
+ "license": "MIT",
"dependencies": {
"dom-walk": "^0.1.0"
}
diff --git a/package.json b/package.json
index 2c575475bd11f..fe516c4a854c3 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"serve": "gatsby serve",
"lint": "eslint --fix .",
"checklint": "eslint .",
+ "check:images": "node scripts/find-missing-gatsby-images.js",
"pretest": "eslint --ignore-path .gitignore .",
"preload-fonts": "gatsby-preload-fonts",
"deploy": "gatsby build && gh-pages -d public -b master",
@@ -124,6 +125,8 @@
"devDependencies": {
"@babel/cli": "^7.28.3",
"@babel/core": "^7.28.5",
+ "@babel/parser": "^7.28.5",
+ "@babel/traverse": "^7.28.5",
"@babel/eslint-parser": "^7.28.5",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.1",
diff --git a/scripts/find-missing-gatsby-images.js b/scripts/find-missing-gatsby-images.js
new file mode 100644
index 0000000000000..abe5549b143fc
--- /dev/null
+++ b/scripts/find-missing-gatsby-images.js
@@ -0,0 +1,141 @@
+#!/usr/bin/env node
+
+const fs = require("fs");
+const path = require("path");
+const parser = require("@babel/parser");
+const traverse = require("@babel/traverse").default;
+
+const defaultRoots = ["src", "content-learn"];
+const extensions = new Set([".js", ".jsx", ".ts", ".tsx"]);
+const ignoreDirs = new Set([
+ "node_modules",
+ ".git",
+ ".cache",
+ "public",
+ "static",
+ "scripts",
+ "__generated__"
+]);
+
+const targets = process.argv.slice(2);
+const roots = targets.length ? targets : defaultRoots;
+const issues = [];
+
+const parserOptions = {
+ sourceType: "unambiguous",
+ errorRecovery: true,
+ plugins: [
+ "jsx",
+ "typescript",
+ "classProperties",
+ "classPrivateProperties",
+ "classPrivateMethods",
+ ["decorators", { decoratorsBeforeExport: true }],
+ "dynamicImport",
+ "exportDefaultFrom",
+ "exportNamespaceFrom",
+ "importAssertions",
+ "topLevelAwait"
+ ]
+};
+
+function isSkippableDir(name) {
+ return ignoreDirs.has(name) || name.startsWith(".");
+}
+
+function collectFiles(entry) {
+ const files = [];
+ for (const root of entry) {
+ const absRoot = path.resolve(process.cwd(), root);
+ if (!fs.existsSync(absRoot)) {
+ continue;
+ }
+ traverseDir(absRoot, files);
+ }
+ return files;
+}
+
+function traverseDir(dir, bucket) {
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (entry.isDirectory()) {
+ if (isSkippableDir(entry.name)) {
+ continue;
+ }
+ traverseDir(path.join(dir, entry.name), bucket);
+ } else if (entry.isFile()) {
+ const ext = path.extname(entry.name);
+ if (extensions.has(ext)) {
+ bucket.push(path.join(dir, entry.name));
+ }
+ }
+ }
+}
+
+function hasAttribute(attrs, attrName) {
+ return attrs.some((attr) => {
+ if (attr.type !== "JSXAttribute" || !attr.name) {
+ return false;
+ }
+ return attr.name.name === attrName;
+ });
+}
+
+function getJsxName(node) {
+ if (!node) {
+ return null;
+ }
+ if (node.type === "JSXIdentifier") {
+ return node.name;
+ }
+ if (node.type === "JSXMemberExpression") {
+ return getJsxName(node.object) + "." + getJsxName(node.property);
+ }
+ if (node.type === "JSXNamespacedName") {
+ return `${node.namespace.name}:${node.name.name}`;
+ }
+ return null;
+}
+
+function report(file, node, message) {
+ const location = node.loc ? `${node.loc.start.line}:${node.loc.start.column + 1}` : "unknown";
+ issues.push({ file, location, message });
+}
+
+function analyzeFile(file) {
+ const code = fs.readFileSync(file, "utf8");
+ let ast;
+ try {
+ ast = parser.parse(code, { ...parserOptions, sourceFilename: file });
+ } catch (err) {
+ console.warn(`Skipping ${path.relative(process.cwd(), file)}: ${err.message}`);
+ return;
+ }
+
+ traverse(ast, {
+ JSXOpeningElement(pathRef) {
+ const name = getJsxName(pathRef.node.name);
+ if (!name) {
+ return;
+ }
+ if (name === "GatsbyImage" && !hasAttribute(pathRef.node.attributes, "image")) {
+ report(file, pathRef.node, "Missing required 'image' prop on .`);
+};
+
+const renderBasicImage = ({ publicURL, alt, imgStyle, ...rest }) => {
+ if (!publicURL) {
+ return null;
+ }
+
+ return (
+