From 493228e6ecb62aede5bfea0d78f0f8a7288628c1 Mon Sep 17 00:00:00 2001 From: Kavya Katal Date: Thu, 4 Jun 2026 20:39:43 +0530 Subject: [PATCH] Add Gatsby image validation check Signed-off-by: Kavya Katal --- Makefile | 9 +- package.json | 3 +- scripts/find-missing-gatsby-images.js | 161 ++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 scripts/find-missing-gatsby-images.js diff --git a/Makefile b/Makefile index 5097d496b51de..135f9ba36977e 100644 --- a/Makefile +++ b/Makefile @@ -62,9 +62,16 @@ 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 site-full clean site-fast lint features check-images ## Analyze webpack bundle with FCP optimization site-analyze: @echo "🏗️ Building site with webpack bundle analyzer..." ANALYZE_BUNDLE=true npm run build + +## Verify GatsbyImage components have required image props. +check-images: + npm run check:images -- $(filter-out $@,$(MAKECMDGOALS)) + +%: + @: diff --git a/package.json b/package.json index 20edfbefc7787..e44f6685b8c85 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "noIndex": "gatsby build && echo 'User-agent: *\nDisallow: /' > public/robots.txt", "version": "gatsby --version", "postinstall": "patch-package", - "prepare": "husky || true" + "prepare": "husky || true", + "check:images": "node scripts/find-missing-gatsby-images.js" }, "dependencies": { "@emotion/is-prop-valid": "^1.4.0", diff --git a/scripts/find-missing-gatsby-images.js b/scripts/find-missing-gatsby-images.js new file mode 100644 index 0000000000000..59428977db39a --- /dev/null +++ b/scripts/find-missing-gatsby-images.js @@ -0,0 +1,161 @@ +#!/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 allowedExtensions = new Set([".js", ".jsx", ".ts", ".tsx"]); +const ignoredDirectories = new Set([ + "node_modules", + ".git", + ".cache", + "public", + "static", +]); + +const scanTargets = process.argv.slice(2); +const rootsToScan = scanTargets.length > 0 ? scanTargets : defaultRoots; +const issues = []; + +function collectFiles(targets) { + const files = []; + + for (const target of targets) { + const absoluteTarget = path.resolve(process.cwd(), target); + + if (!fs.existsSync(absoluteTarget)) { + console.warn(`Skipping missing path: ${target}`); + continue; + } + + const stat = fs.statSync(absoluteTarget); + + if (stat.isFile()) { + if (allowedExtensions.has(path.extname(absoluteTarget))) { + files.push(absoluteTarget); + } + continue; + } + + walkDirectory(absoluteTarget, files); + } + + return files; +} + +function walkDirectory(directory, files) { + const entries = fs.readdirSync(directory, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + if (!ignoredDirectories.has(entry.name)) { + walkDirectory(fullPath, files); + } + continue; + } + + if (entry.isFile() && allowedExtensions.has(path.extname(entry.name))) { + files.push(fullPath); + } + } +} + +function getJsxElementName(node) { + if (!node) { + return ""; + } + + if (node.type === "JSXIdentifier") { + return node.name; + } + + if (node.type === "JSXMemberExpression") { + return `${getJsxElementName(node.object)}.${getJsxElementName(node.property)}`; + } + + return ""; +} + +function hasImageProp(attributes) { + return attributes.some((attribute) => { + return ( + attribute.type === "JSXAttribute" && + attribute.name && + attribute.name.name === "image" + ); + }); +} + +function validateFile(filePath) { + const code = fs.readFileSync(filePath, "utf8"); + + let ast; + + try { + ast = parser.parse(code, { + sourceType: "unambiguous", + plugins: [ + "jsx", + "typescript", + "classProperties", + "classPrivateProperties", + "classPrivateMethods", + "dynamicImport", + "exportDefaultFrom", + "exportNamespaceFrom", + ], + }); + } catch (error) { + console.warn( + `Skipping ${path.relative(process.cwd(), filePath)}: ${error.message}`, + ); + return; + } + + traverse(ast, { + JSXOpeningElement(pathRef) { + const componentName = getJsxElementName(pathRef.node.name); + + if (componentName !== "GatsbyImage") { + return; + } + + if (!hasImageProp(pathRef.node.attributes)) { + const location = pathRef.node.loc + ? `${pathRef.node.loc.start.line}:${pathRef.node.loc.start.column + 1}` + : "unknown"; + + issues.push({ + filePath, + location, + }); + } + }, + }); +} + +const files = collectFiles(rootsToScan); + +files.forEach(validateFile); + +if (issues.length > 0) { + console.error("\nGatsby image validation failed.\n"); + console.error( + "The following components are missing an explicit `image` prop:\n", + ); + + for (const issue of issues) { + console.error( + `${path.relative(process.cwd(), issue.filePath)}:${issue.location}`, + ); + } + + console.error(`\nTotal issues found: ${issues.length}`); + process.exit(1); +} + +console.log(`Gatsby image validation passed. Checked ${files.length} file(s).`);