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 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..aa4ddbeba003a 100644 --- a/src/components/image.js +++ b/src/components/image.js @@ -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 .`); +}; + +const renderBasicImage = ({ publicURL, alt, imgStyle, ...rest }) => { + if (!publicURL) { + return null; + } + + 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 = childImageSharp.gatsbyImageData; + return images.find(image => image.name.toLowerCase() == serviceMesh); + + 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 capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); const ServiceMeshesAvailable = ({ serviceMeshes }) => serviceMeshes.map((sm, index) => { 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