From d228014ced57797f9e577cdc8d7f303f5fa040d6 Mon Sep 17 00:00:00 2001 From: Jared Jolton Date: Tue, 14 Jul 2026 12:12:08 -0600 Subject: [PATCH 1/2] test: fail CI on dual-major react-query in RTV [AIS-33] Install-tree gate catches the field-editor-reference v4 vs shared/rich-text v5 split that Vite collapses to the wrong major at bundle time. Co-authored-by: Cursor --- apps/rich-text-versioning/AGENTS.md | 1 + .../test/reactQueryMajors.spec.ts | 50 +++++++++ .../test/reactQueryMajors.ts | 104 ++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 apps/rich-text-versioning/test/reactQueryMajors.spec.ts create mode 100644 apps/rich-text-versioning/test/reactQueryMajors.ts diff --git a/apps/rich-text-versioning/AGENTS.md b/apps/rich-text-versioning/AGENTS.md index 5db4157c88..2099f2790b 100644 --- a/apps/rich-text-versioning/AGENTS.md +++ b/apps/rich-text-versioning/AGENTS.md @@ -28,6 +28,7 @@ Standard Vite app. ## Sharp Edges & Invariants +- **`@tanstack/react-query` major uniqueness:** `field-editor-reference` hard-deps RQ `^4` while `field-editor-rich-text@6` / `field-editor-shared@4` peer-accept `^4 || ^5`. A dual-major install tree lets Vite bundle the wrong single major and crashes embedded entry/asset cards (`reading 'sys'`). `test/reactQueryMajors.spec.ts` fails CI if more than one major is present under `node_modules`. Do not remove the direct `@tanstack/react-query` pin without re-checking the install tree. - **`@contentful/field-editor-rich-text`** is embedded to provide the actual editing experience — this is the standard Contentful rich-text editor. Do not replace it with a custom editor. - **Versions stored as JSON** in a separate Contentful JSON field (not in the rich-text field itself). The versioning field must exist on the content type alongside the rich-text field — document this requirement clearly to users. - **Snapshot schema**: each snapshot has `{ id, timestamp, label, value: Document }`. Changing this schema requires migrating all existing version data. diff --git a/apps/rich-text-versioning/test/reactQueryMajors.spec.ts b/apps/rich-text-versioning/test/reactQueryMajors.spec.ts new file mode 100644 index 0000000000..aae44031c1 --- /dev/null +++ b/apps/rich-text-versioning/test/reactQueryMajors.spec.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + analyzeReactQueryMajors, + findReactQueryPackages, +} from './reactQueryMajors'; + +const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +describe('analyzeReactQueryMajors', () => { + it('passes when there are no copies (package not in tree)', () => { + const result = analyzeReactQueryMajors([]); + expect(result.ok).toBe(true); + expect(result.majors).toEqual([]); + }); + + it('passes when every copy shares one major', () => { + const result = analyzeReactQueryMajors([ + { version: '4.44.0', path: '/a/node_modules/@tanstack/react-query' }, + { version: '4.36.1', path: '/a/node_modules/nested/@tanstack/react-query' }, + ]); + expect(result.ok).toBe(true); + expect(result.majors).toEqual(['4']); + }); + + it('fails when majors 4 and 5 both appear (RTV crash-class tree)', () => { + const result = analyzeReactQueryMajors([ + { version: '5.101.2', path: '/a/node_modules/@tanstack/react-query' }, + { + version: '4.44.0', + path: '/a/node_modules/@contentful/field-editor-reference/node_modules/@tanstack/react-query', + }, + ]); + expect(result.ok).toBe(false); + expect(result.majors).toEqual(['4', '5']); + expect(result.message).toMatch(/multiple majors/i); + }); +}); + +describe('findReactQueryPackages (install tree)', () => { + it('finds only one major under this app node_modules', () => { + const copies = findReactQueryPackages(path.join(appRoot, 'node_modules')); + const result = analyzeReactQueryMajors(copies); + + expect(copies.length).toBeGreaterThan(0); + expect(result.ok, result.message).toBe(true); + expect(result.majors).toHaveLength(1); + }); +}); diff --git a/apps/rich-text-versioning/test/reactQueryMajors.ts b/apps/rich-text-versioning/test/reactQueryMajors.ts new file mode 100644 index 0000000000..d1a6d6e369 --- /dev/null +++ b/apps/rich-text-versioning/test/reactQueryMajors.ts @@ -0,0 +1,104 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export type ReactQueryCopy = { + version: string; + path: string; +}; + +export type ReactQueryMajorsResult = { + ok: boolean; + majors: string[]; + copies: ReactQueryCopy[]; + message: string; +}; + +export function analyzeReactQueryMajors(copies: ReactQueryCopy[]): ReactQueryMajorsResult { + const majors = [ + ...new Set(copies.map((copy) => copy.version.split('.')[0]).filter(Boolean)), + ].sort(); + + if (majors.length <= 1) { + return { + ok: true, + majors, + copies, + message: + majors.length === 0 + ? 'No @tanstack/react-query copies found.' + : `Single @tanstack/react-query major found: ${majors[0]}.`, + }; + } + + const listed = copies.map((copy) => ` ${copy.version} @ ${copy.path}`).join('\n'); + return { + ok: false, + majors, + copies, + message: [ + `Found multiple majors of @tanstack/react-query (${majors.join(', ')}).`, + 'Vite collapses these to a single major at bundle time; the wrong one', + 'breaks embedded entry/asset cards in field-editor-rich-text (sys crash).', + 'Ensure the install tree resolves to one major.', + listed, + ].join('\n'), + }; +} + +/** + * Walk node_modules for every physical @tanstack/react-query package.json. + * Nested installs under field-editor-reference are the interesting case. + */ +export function findReactQueryPackages(nodeModulesRoot: string): ReactQueryCopy[] { + const hits: ReactQueryCopy[] = []; + + function walk(dir: string, depth: number): void { + if (depth > 12) return; + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + // Skip Vite/Vitest caches and bin stubs + if (entry.name === '.bin' || entry.name === '.vite' || entry.name === '.cache') continue; + + const full = path.join(dir, entry.name); + + if (entry.name === '@tanstack') { + const pkgPath = path.join(full, 'react-query', 'package.json'); + if (fs.existsSync(pkgPath)) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { version?: string }; + if (pkg.version) { + hits.push({ version: pkg.version, path: path.dirname(pkgPath) }); + } + } catch { + // ignore unreadable package.json + } + } + continue; + } + + if (entry.name === 'node_modules' || dir.endsWith(`${path.sep}node_modules`) || entry.name.startsWith('@')) { + walk(full, depth + 1); + continue; + } + + const nested = path.join(full, 'node_modules'); + if (fs.existsSync(nested)) { + walk(nested, depth + 1); + } + } + } + + if (fs.existsSync(nodeModulesRoot)) { + walk(nodeModulesRoot, 0); + } + + return hits; +} From 02e371c70254f745a1cbff6247277ffc7a5fa3b1 Mon Sep 17 00:00:00 2001 From: Jared Jolton Date: Tue, 14 Jul 2026 12:15:29 -0600 Subject: [PATCH 2/2] style: prettier format react-query major uniqueness gate Co-authored-by: Cursor --- apps/rich-text-versioning/test/reactQueryMajors.spec.ts | 5 +---- apps/rich-text-versioning/test/reactQueryMajors.ts | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/rich-text-versioning/test/reactQueryMajors.spec.ts b/apps/rich-text-versioning/test/reactQueryMajors.spec.ts index aae44031c1..f6e8835f70 100644 --- a/apps/rich-text-versioning/test/reactQueryMajors.spec.ts +++ b/apps/rich-text-versioning/test/reactQueryMajors.spec.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from 'vitest'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { - analyzeReactQueryMajors, - findReactQueryPackages, -} from './reactQueryMajors'; +import { analyzeReactQueryMajors, findReactQueryPackages } from './reactQueryMajors'; const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); diff --git a/apps/rich-text-versioning/test/reactQueryMajors.ts b/apps/rich-text-versioning/test/reactQueryMajors.ts index d1a6d6e369..08d05f2c1f 100644 --- a/apps/rich-text-versioning/test/reactQueryMajors.ts +++ b/apps/rich-text-versioning/test/reactQueryMajors.ts @@ -84,7 +84,11 @@ export function findReactQueryPackages(nodeModulesRoot: string): ReactQueryCopy[ continue; } - if (entry.name === 'node_modules' || dir.endsWith(`${path.sep}node_modules`) || entry.name.startsWith('@')) { + if ( + entry.name === 'node_modules' || + dir.endsWith(`${path.sep}node_modules`) || + entry.name.startsWith('@') + ) { walk(full, depth + 1); continue; }