diff --git a/CHANGELOG.md b/CHANGELOG.md index dab1952..ebfddc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [Unreleased] + +### Fixed +- **Index `.jsx`, `.ts` and `.tsx` block sources.** The save, edit and variations parsers only looked for `save.js`, `edit.js` and `variations.js`. Gutenberg renamed its JSX-bearing block sources to `.jsx` in [WordPress/gutenberg#80990](https://github.com/WordPress/gutenberg/pull/80990) (August 2026), so indexing Gutenberg `trunk` silently classified all 116 core blocks as dynamic, with 0 verified examples, and dropped every inspector control. Sources are now resolved in the order `.js`, `.jsx`, `.ts`, `.tsx`. On Gutenberg `trunk` the index goes from 0 to 54 static blocks and from 0 to 256 verified examples. Sources that only ship `.js` files (WordPress 7.1 and earlier) produce the same index and the same content hashes as before, so they are not re-indexed; stale `trunk` indexes are repaired by a plain `wp-blocks index`, no `--force` needed. + ## [1.1.1] - 2026-05-13 ### Docs diff --git a/src/indexer.js b/src/indexer.js index 5c37447..390b09a 100644 --- a/src/indexer.js +++ b/src/indexer.js @@ -4,7 +4,7 @@ */ import fg from 'fast-glob'; import { readFileSync, statSync } from 'node:fs'; -import { join, dirname } from 'node:path'; +import { join, dirname, basename } from 'node:path'; import { createHash } from 'node:crypto'; import { fetchSource } from './source-manager.js'; import { parseBlockJson } from './parsers/block-json-parser.js'; @@ -12,6 +12,7 @@ import { parseEditFile } from './parsers/edit-parser.js'; import { parseSaveFile } from './parsers/save-parser.js'; import { parseVariationsFile } from './parsers/variations-parser.js'; import { generateMarkupExamples, generateDynamicMarkup } from './utils/markup-generator.js'; +import { resolveSourceFile } from './utils/file-utils.js'; import { validateMarkup } from './validation/index.js'; import { listSources, @@ -118,7 +119,7 @@ async function indexSource(source, localPath, force, stats) { const fullBlockDir = join(localPath, blockDir); try { - // Compute content hash for the block directory (block.json + save.js + edit.js + variations.js) + // Compute content hash for the block directory (block.json + save, edit and variations sources) const dirHash = computeBlockDirHash(fullBlockDir); // Incremental skip @@ -345,20 +346,25 @@ function calculateConfidence(blockData) { /** * Compute a content hash for a block directory. - * Hashes the contents of key files: block.json, save.js, edit.js, variations.js + * Hashes the contents of key files: block.json, and the save, edit and + * variations sources whatever their extension (.js, .jsx, .ts, .tsx). */ function computeBlockDirHash(blockDir) { const hash = createHash('sha256'); - const files = ['block.json', 'save.js', 'edit.js', 'variations.js']; - - for (const file of files) { - const fullPath = join(blockDir, file); + const files = [ + join(blockDir, 'block.json'), + ...['save', 'edit', 'variations'].map( + (base) => resolveSourceFile(blockDir, base) ?? join(blockDir, `${base}.js`) + ), + ]; + + for (const fullPath of files) { try { const content = readFileSync(fullPath, 'utf-8'); hash.update(content); } catch { // File doesn't exist — that's fine, hash the absence - hash.update(`__missing__${file}`); + hash.update(`__missing__${basename(fullPath)}`); } } diff --git a/src/parsers/edit-parser.js b/src/parsers/edit-parser.js index e7be22e..2bf0647 100644 --- a/src/parsers/edit-parser.js +++ b/src/parsers/edit-parser.js @@ -4,7 +4,7 @@ import { parse } from '@babel/parser'; import traverse from '@babel/traverse'; import * as t from '@babel/types'; -import { readFile, fileExists } from '../utils/file-utils.js'; +import { readFile, resolveSourceFile } from '../utils/file-utils.js'; const CONTROL_PATTERNS = { AlignmentControl: { attribute: 'align', possibleValues: ['left', 'center', 'right', 'justify'] }, @@ -31,9 +31,9 @@ const CONTROL_PATTERNS = { * @returns {{ controls: object[], imports: object[] }} */ export function parseEditFile(blockPath) { - const editPath = `${blockPath}/edit.js`; + const editPath = resolveSourceFile(blockPath, 'edit'); - if (!fileExists(editPath)) { + if (!editPath) { return { controls: [], imports: [] }; } diff --git a/src/parsers/save-parser.js b/src/parsers/save-parser.js index a0284ee..a560c5d 100644 --- a/src/parsers/save-parser.js +++ b/src/parsers/save-parser.js @@ -9,7 +9,7 @@ import { parse } from '@babel/parser'; import traverse from '@babel/traverse'; import * as t from '@babel/types'; -import { readFile, fileExists } from '../utils/file-utils.js'; +import { readFile, fileExists, resolveSourceFile } from '../utils/file-utils.js'; /** * Parse a save.js file and return HTML output patterns. @@ -17,11 +17,11 @@ import { readFile, fileExists } from '../utils/file-utils.js'; * @returns {object} */ export function parseSaveFile(blockPath) { - const savePath = `${blockPath}/save.js`; + const savePath = resolveSourceFile(blockPath, 'save'); const hasRenderPhp = fileExists(`${blockPath}/render.php`); - if (!fileExists(savePath)) { - // No save.js — dynamic block if render.php exists, otherwise unknown + if (!savePath) { + // No save file — dynamic block if render.php exists, otherwise unknown return { hasCustomSave: false, returnsNull: true, diff --git a/src/parsers/variations-parser.js b/src/parsers/variations-parser.js index b53c96c..c4ad062 100644 --- a/src/parsers/variations-parser.js +++ b/src/parsers/variations-parser.js @@ -4,7 +4,7 @@ import { parse } from '@babel/parser'; import traverse from '@babel/traverse'; import * as t from '@babel/types'; -import { readFile, fileExists } from '../utils/file-utils.js'; +import { readFile, resolveSourceFile } from '../utils/file-utils.js'; /** * Parse a variations.js file and return extracted variations. @@ -12,9 +12,9 @@ import { readFile, fileExists } from '../utils/file-utils.js'; * @returns {object[]} */ export function parseVariationsFile(blockPath) { - const variationsPath = `${blockPath}/variations.js`; + const variationsPath = resolveSourceFile(blockPath, 'variations'); - if (!fileExists(variationsPath)) { + if (!variationsPath) { return []; } diff --git a/src/utils/file-utils.js b/src/utils/file-utils.js index d2857c4..1f4a611 100644 --- a/src/utils/file-utils.js +++ b/src/utils/file-utils.js @@ -12,3 +12,26 @@ export function readJSON(filePath) { export function readFile(filePath) { return fs.readFileSync(filePath, 'utf-8'); } + +/** + * Extensions a block source file can use, in lookup order. + * Gutenberg moved JSX-bearing files from .js to .jsx in August 2026 + * (WordPress/gutenberg#80990), and many third-party blocks ship .tsx. + */ +export const SOURCE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']; + +/** + * Find a block source file by base name, whatever its extension. + * @param {string} blockPath - Directory containing the block + * @param {string} baseName - File name without extension, e.g. 'save' + * @returns {string|null} Full path of the first match, or null + */ +export function resolveSourceFile(blockPath, baseName) { + for (const ext of SOURCE_EXTENSIONS) { + const candidate = `${blockPath}/${baseName}${ext}`; + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; +} diff --git a/tests/test-source-resolution.js b/tests/test-source-resolution.js new file mode 100644 index 0000000..1015539 --- /dev/null +++ b/tests/test-source-resolution.js @@ -0,0 +1,113 @@ +import { test, before, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { resolveSourceFile } from '../src/utils/file-utils.js'; +import { parseSaveFile } from '../src/parsers/save-parser.js'; +import { parseEditFile } from '../src/parsers/edit-parser.js'; +import { parseVariationsFile } from '../src/parsers/variations-parser.js'; + +// Gutenberg renamed its JSX-bearing block sources from .js to .jsx in +// WordPress/gutenberg#80990 (August 2026). A block that only ships .jsx +// files must be parsed exactly like one that ships .js files. + +let root; + +const SAVE = ` +import { useBlockProps, RichText } from '@wordpress/block-editor'; +export default function save( { attributes } ) { + return ( +
+