From 0647b7b9b2015234fa5830bad1c85e4a30c86e36 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Thu, 4 Dec 2025 09:51:19 -0600 Subject: [PATCH 1/5] chore: Add image validation for Gatsby components and enhance error handling Signed-off-by: Lee Calcote --- Makefile | 6 +- package-lock.json | 19 ++- package.json | 3 + scripts/find-missing-gatsby-images.js | 141 ++++++++++++++++++ src/components/image.js | 24 ++- src/sections/Learn-Layer5/Chapters/index.js | 12 ++ .../Learn-Layer5/Course-Overview/index.js | 19 ++- 7 files changed, 215 insertions(+), 9 deletions(-) create mode 100644 scripts/find-missing-gatsby-images.js diff --git a/Makefile b/Makefile index adef118a1734f..3f2d201865b20 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,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 @@ -51,4 +55,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 clean site-fast lint features +.PHONY: setup build site clean site-fast lint check-images features 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 07798311ee970..bb26053e4d0c4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,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", @@ -123,6 +124,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 filesToInspect = collectFiles(roots); +filesToInspect.forEach(analyzeFile); + +if (issues.length) { + console.error("Found Gatsby image issues:\n"); + for (const issue of issues) { + const relative = path.relative(process.cwd(), issue.file); + console.error(`${relative}:${issue.location} - ${issue.message}`); + } + console.error(`\nTotal issues: ${issues.length}`); + process.exitCode = 1; +} else { + console.log("No missing 'image' props detected on GatsbyImage components."); +} diff --git a/src/components/image.js b/src/components/image.js index ab561d09b3486..e94936051fe5c 100644 --- a/src/components/image.js +++ b/src/components/image.js @@ -1,6 +1,26 @@ import React from "react"; import { GatsbyImage } from "gatsby-plugin-image"; +const formatDebugInfo = ({ alt, publicURL }) => { + const debugInfo = []; + if (alt) { + debugInfo.push(`alt="${alt}"`); + } + if (publicURL) { + debugInfo.push(`publicURL="${publicURL}"`); + } + return debugInfo.length ? ` (${debugInfo.join(", ")})` : ""; +}; + +const ensureGatsbyImageData = (imageData, context) => { + if (imageData) { + return imageData; + } + + const errorMessage = `[Image] Missing gatsbyImageData${formatDebugInfo(context)}. Ensure the GraphQL query returns childImageSharp.gatsbyImageData.`; + throw new Error(errorMessage); +}; + const Image = ({ childImageSharp, extension, publicURL, alt, imgStyle, ...rest }) => { if (!childImageSharp && extension === "svg") { @@ -20,9 +40,11 @@ const Image = ({ childImageSharp, extension, publicURL, alt, imgStyle, ...rest } ); } + const gatsbyImageData = ensureGatsbyImageData(childImageSharp?.gatsbyImageData, { alt, publicURL }); + return { + const meshImage = findServiceMeshImage(serviceMeshImages, section); + return !(meshImage && meshImage.imagepath?.childImageSharp?.gatsbyImageData); + }) + .map(({ section }) => section); + + if (missingServiceMeshImages.length > 0) { + const context = chapterData?.fields?.slug || "unknown-chapter"; + throw new Error(`[Chapters] Missing meshesYouLearn image data for: ${missingServiceMeshImages.join(", ")} (chapter: ${context}).`); + } + const findServiceMeshImage = (images, serviceMesh) => images.find(image => image.name.toLowerCase() == serviceMesh); const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); diff --git a/src/sections/Learn-Layer5/Course-Overview/index.js b/src/sections/Learn-Layer5/Course-Overview/index.js index ae2995d968594..ada039794efe2 100644 --- a/src/sections/Learn-Layer5/Course-Overview/index.js +++ b/src/sections/Learn-Layer5/Course-Overview/index.js @@ -41,10 +41,15 @@ const CourseOverview = ({ course, chapters, serviceMeshesList, children }) => { const ServiceMeshesAvailable = ({ serviceMeshes }) => serviceMeshes.map((sm, index) => { + const meshImage = findServiceMeshImage(serviceMeshImages, sm); + if (!meshImage || !meshImage.imagepath) { + return null; + } + return (
{sm} @@ -52,6 +57,16 @@ const CourseOverview = ({ course, chapters, serviceMeshesList, children }) => { ); }); + const missingServiceMeshImages = availableServiceMeshes.filter((mesh) => { + const meshImage = findServiceMeshImage(serviceMeshImages, mesh); + return !(meshImage && meshImage.imagepath?.childImageSharp?.gatsbyImageData); + }); + + if (missingServiceMeshImages.length > 0) { + const context = course?.fields?.slug || course?.frontmatter?.courseTitle || "unknown-course"; + throw new Error(`[CourseOverview] Missing meshesYouLearn image data for: ${missingServiceMeshImages.join(", ")} (course: ${context}).`); + } + useEffect(() => { let bookmarkPath = localStorage.getItem("bookmarkpath-" + course.fields.slug.split("/")[3]); if (bookmarkPath) { @@ -130,8 +145,6 @@ const CourseOverview = ({ course, chapters, serviceMeshesList, children }) => {
- {console.log("lenght of the service mesh array: ", availableServiceMeshes.length)} - {console.log("array: ", availableServiceMeshes)} {serviceMeshImages.length !== 0 && availableServiceMeshes.length != 0 && ( <>

Technologies You Can Learn

From cf22da64082e2db667e220621fe823d4354d3c4c Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Thu, 4 Dec 2025 10:06:42 -0600 Subject: [PATCH 2/5] chore: build-javascript stage to enforce stronger runtime chunking: a dedicated runtime chunk plus explicit framework and vendor cache groups Signed-off-by: Lee Calcote --- gatsby-node.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/gatsby-node.js b/gatsby-node.js index b25100c9bcd35..d61f63beb9e59 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -743,6 +743,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", + maxInitialRequests: existingSplitChunks.maxInitialRequests ?? 25, + minSize: existingSplitChunks.minSize ?? 20000, + ...existingSplitChunks, + 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); } }; From 210928e11d5ba0af1996e6e574fdb6d9a5a25033 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Thu, 4 Dec 2025 10:07:59 -0600 Subject: [PATCH 3/5] chore: enhance image component error handling for missing gatsbyImageData Signed-off-by: Lee Calcote --- src/components/image.js | 65 +++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/components/image.js b/src/components/image.js index e94936051fe5c..aa4ddbeba003a 100644 --- a/src/components/image.js +++ b/src/components/image.js @@ -1,46 +1,61 @@ import React from "react"; import { GatsbyImage } from "gatsby-plugin-image"; -const formatDebugInfo = ({ alt, publicURL }) => { - const debugInfo = []; +const loggedWarnings = new Set(); + +const warnMissingImageData = ({ alt, publicURL }) => { + const key = `${publicURL || "no-url"}-${alt || "no-alt"}`; + if (loggedWarnings.has(key)) { + return; + } + loggedWarnings.add(key); + + const details = []; if (alt) { - debugInfo.push(`alt="${alt}"`); + details.push(`alt="${alt}"`); } if (publicURL) { - debugInfo.push(`publicURL="${publicURL}"`); + details.push(`publicURL="${publicURL}"`); } - return debugInfo.length ? ` (${debugInfo.join(", ")})` : ""; + const suffix = details.length ? ` (${details.join(", ")})` : ""; + console.warn(`[Image] Missing gatsbyImageData${suffix}. Falling back to .`); }; -const ensureGatsbyImageData = (imageData, context) => { - if (imageData) { - return imageData; +const renderBasicImage = ({ publicURL, alt, imgStyle, ...rest }) => { + if (!publicURL) { + return null; } - const errorMessage = `[Image] Missing gatsbyImageData${formatDebugInfo(context)}. Ensure the GraphQL query returns childImageSharp.gatsbyImageData.`; - throw new Error(errorMessage); + return ( +
+ {alt +
+ ); }; const Image = ({ childImageSharp, extension, publicURL, alt, imgStyle, ...rest }) => { if (!childImageSharp && extension === "svg") { - return ( -
- {alt -
- ); + return renderBasicImage({ publicURL, alt, imgStyle, ...rest }); + } + + if (!childImageSharp?.gatsbyImageData) { + warnMissingImageData({ alt, publicURL }); + return renderBasicImage({ publicURL, alt, imgStyle, ...rest }); } - const gatsbyImageData = ensureGatsbyImageData(childImageSharp?.gatsbyImageData, { alt, publicURL }); + const gatsbyImageData = childImageSharp.gatsbyImageData; return Date: Sat, 6 Dec 2025 15:58:30 -0600 Subject: [PATCH 4/5] Update src/sections/Learn-Layer5/Chapters/index.js Co-authored-by: Lorenzo Croce <41270564+Fireentity@users.noreply.github.com> Signed-off-by: Lee Calcote --- src/sections/Learn-Layer5/Chapters/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sections/Learn-Layer5/Chapters/index.js b/src/sections/Learn-Layer5/Chapters/index.js index ddb7262ba5ccf..997aa4cd4389b 100644 --- a/src/sections/Learn-Layer5/Chapters/index.js +++ b/src/sections/Learn-Layer5/Chapters/index.js @@ -67,6 +67,8 @@ const Chapters = ({ chapterData, courseData, location, serviceMeshesList, TOCDat }; const availableServiceMeshesArray = getAvailableServiceMeshes(); + const findServiceMeshImage = (images, serviceMesh) => images.find(image => image.name.toLowerCase() == serviceMesh); + const missingServiceMeshImages = availableServiceMeshesArray .filter(({ section }) => { const meshImage = findServiceMeshImage(serviceMeshImages, section); @@ -78,8 +80,6 @@ const Chapters = ({ chapterData, courseData, location, serviceMeshesList, TOCDat const context = chapterData?.fields?.slug || "unknown-chapter"; throw new Error(`[Chapters] Missing meshesYouLearn image data for: ${missingServiceMeshImages.join(", ")} (chapter: ${context}).`); } - - const findServiceMeshImage = (images, serviceMesh) => images.find(image => image.name.toLowerCase() == serviceMesh); const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); const ServiceMeshesAvailable = ({ serviceMeshes }) => serviceMeshes.map((sm, index) => { From b5c09cc5becef7c876fbe8b615c65e2c3354c5b6 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Sat, 6 Dec 2025 15:59:09 -0600 Subject: [PATCH 5/5] Update gatsby-node.js Co-authored-by: Lorenzo Croce <41270564+Fireentity@users.noreply.github.com> Signed-off-by: Lee Calcote --- gatsby-node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gatsby-node.js b/gatsby-node.js index 23ad098824ee5..0a69b17b6b826 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -815,9 +815,9 @@ exports.onCreateWebpackConfig = ({ actions, stage, getConfig }) => { runtimeChunk: { name: "runtime" }, splitChunks: { chunks: "all", + ...existingSplitChunks, maxInitialRequests: existingSplitChunks.maxInitialRequests ?? 25, minSize: existingSplitChunks.minSize ?? 20000, - ...existingSplitChunks, cacheGroups: { ...existingCacheGroups, vendor: {