Skip to content
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
33 changes: 33 additions & 0 deletions gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,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);
}
};
Expand Down
19 changes: 15 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
141 changes: 141 additions & 0 deletions scripts/find-missing-gatsby-images.js
Original file line number Diff line number Diff line change
@@ -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 <GatsbyImage />");
}
}
});
}

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.");
}
67 changes: 52 additions & 15 deletions src/components/image.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,65 @@
import React from "react";
import { GatsbyImage } from "gatsby-plugin-image";

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) {
details.push(`alt="${alt}"`);
}
if (publicURL) {
details.push(`publicURL="${publicURL}"`);
}
const suffix = details.length ? ` (${details.join(", ")})` : "";
console.warn(`[Image] Missing gatsbyImageData${suffix}. Falling back to <img>.`);
};

const renderBasicImage = ({ publicURL, alt, imgStyle, ...rest }) => {
if (!publicURL) {
return null;
}

return (
<div className="old-gatsby-image-wrapper" style={{ width: "100%", height: "auto" }}>
<img
src={publicURL}
alt={alt || "Blog image"}
width="100%"
height="auto"
style={{
objectFit: imgStyle?.objectFit || "cover",
...imgStyle,
}}
loading="lazy"
{...rest}
/>
</div>
);
};

const Image = ({ childImageSharp, extension, publicURL, alt, imgStyle, ...rest }) => {

if (!childImageSharp && extension === "svg") {
return (
<div className="old-gatsby-image-wrapper" style={{ width: "100%", height: "auto" }}>
<img
src={publicURL}
alt={alt || "Blog image"}
width="100%"
height="auto"
style={{
objectFit: imgStyle?.objectFit || "cover",
...imgStyle
}}
/>
</div>
);
return renderBasicImage({ publicURL, alt, imgStyle, ...rest });
}

if (!childImageSharp?.gatsbyImageData) {
warnMissingImageData({ alt, publicURL });
return renderBasicImage({ publicURL, alt, imgStyle, ...rest });
}

const gatsbyImageData = childImageSharp.gatsbyImageData;

return <GatsbyImage
key={publicURL}
image={childImageSharp?.gatsbyImageData}
image={gatsbyImageData}
alt={alt || "Blog image"}
style={{
objectFit: imgStyle?.objectFit || "cover",
Expand Down
12 changes: 12 additions & 0 deletions src/sections/Learn-Layer5/Chapters/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ const Chapters = ({ chapterData, courseData, location, serviceMeshesList, TOCDat
};
const availableServiceMeshesArray = getAvailableServiceMeshes();

const missingServiceMeshImages = availableServiceMeshesArray
.filter(({ section }) => {
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);

Expand Down
Loading
Loading