From 53f638a34a44d59c3cc42ae7cd400f6cf58ae580 Mon Sep 17 00:00:00 2001 From: Pete Date: Fri, 19 Dec 2025 15:46:52 -0600 Subject: [PATCH 1/7] Removing vercel redirect-check and associated files --- .github/workflows/test.yml | 3 - .husky/pre-commit | 7 +- README.md | 4 - package.json | 1 - scripts/check-redirects.test.ts | 635 ----------------------------- scripts/check-redirects.ts | 687 -------------------------------- 6 files changed, 4 insertions(+), 1333 deletions(-) delete mode 100644 scripts/check-redirects.test.ts delete mode 100644 scripts/check-redirects.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 373aa59650..b6363b4739 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,3 @@ jobs: - name: Run tests run: yarn test - - - name: Check for missing redirects - run: yarn tsx scripts/check-redirects.ts --ci diff --git a/.husky/pre-commit b/.husky/pre-commit index edc7718e65..5d4fecc221 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -128,9 +128,10 @@ main() { log_info "📁 Found $(echo "$staged_files" | wc -l | tr -d ' ') staged files" # 1. Fast redirect validation (project-specific) - log_step "Validating redirects..." - time_command yarn check-redirects || exit_with_error "Redirect validation failed. Fix redirect issues before committing." - log_success "Redirect validation passed" + # log_step "Validating redirects..." + # Will need to replace the 'check-redirects' line below when we replace it + # time_command yarn check-redirects || exit_with_error "Redirect validation failed. Fix redirect issues before committing." + # log_success "Redirect validation passed" # 2. Submodule updates (only if submodules are staged or .gitmodules changed) if echo "$staged_files" | grep -E "(\.gitmodules|submodules/)" >/dev/null 2>&1; then diff --git a/README.md b/README.md index ff10554875..42d887c7ff 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,3 @@ This part will update the glossary. ### Formatting 1. Run `yarn format` from the root directory. - -### Redirects - -1. From the root directory, run `yarn check-redirects`. diff --git a/package.json b/package.json index 480232ce9c..b3702de3c7 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "typecheck": "tsc", "test": "vitest run", "test:watch": "vitest", - "check-redirects": "tsx scripts/check-redirects.ts", "check-releases": "ts-node scripts/check-releases.ts", "notion:update": "tsx scripts/notion-update.ts", "notion:verify-quicklooks": "tsx scripts/notion-verify-quicklooks.ts", diff --git a/scripts/check-redirects.test.ts b/scripts/check-redirects.test.ts deleted file mode 100644 index a71e26e04e..0000000000 --- a/scripts/check-redirects.test.ts +++ /dev/null @@ -1,635 +0,0 @@ -import { execSync } from 'child_process'; -import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'fs'; -import { dirname, resolve } from 'path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { RedirectChecker, type RedirectCheckResult } from './check-redirects.js'; - -describe('RedirectChecker', () => { - const TEST_DIR = resolve(__dirname, 'test-redirect-checker'); - const VERCEL_JSON_PATH = resolve(TEST_DIR, 'vercel.json'); - const DOCS_DIR = resolve(TEST_DIR, 'docs'); - const PAGES_DIR = resolve(TEST_DIR, 'pages'); - - beforeEach(() => { - // Create test directories - mkdirSync(TEST_DIR, { recursive: true }); - mkdirSync(DOCS_DIR, { recursive: true }); - mkdirSync(PAGES_DIR, { recursive: true }); - mkdirSync(resolve(PAGES_DIR, 'docs'), { recursive: true }); - - // Initialize git repo - execSync('git init', { cwd: TEST_DIR }); - execSync('git config user.email "test@example.com"', { cwd: TEST_DIR }); - execSync('git config user.name "Test User"', { cwd: TEST_DIR }); - execSync('git commit --allow-empty -m "Initial commit"', { cwd: TEST_DIR }); - }); - - afterEach(() => { - rmSync(TEST_DIR, { recursive: true, force: true }); - }); - - describe('URL Path Handling', () => { - it('should handle index files correctly', () => { - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - // Create and stage files - writeFileSync(resolve(PAGES_DIR, 'index.md'), 'content'); - writeFileSync(resolve(PAGES_DIR, 'docs/index.mdx'), 'content'); - execSync('git add .', { cwd: TEST_DIR }); - - // Test index file paths - const rootResult = (checker as any).getUrlFromPath('pages/index.md'); - const nestedResult = (checker as any).getUrlFromPath('pages/docs/index.mdx'); - - expect(rootResult).toBe('/'); - expect(nestedResult).toBe('/(docs/?)'); - }); - - it('should handle numbered prefixes in file paths', () => { - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - const result = (checker as any).getUrlFromPath('pages/01-intro/02-getting-started.md'); - expect(result).toBe('/(intro/getting-started/?)'); - }); - - it('should normalize URLs consistently', () => { - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - const testCases = [ - { input: '/path/to/doc/', expected: '/path/to/doc' }, - { input: '(path/to/doc)', expected: '/path/to/doc' }, - { input: '//path//to//doc//', expected: '/path/to/doc' }, - { input: '/(path/to/doc/?)', expected: '/path/to/doc' }, - ]; - - testCases.forEach(({ input, expected }) => { - const result = (checker as any).normalizeUrl(input); - expect(result).toBe(expected); - }); - }); - }); - - describe('Mode-specific Behavior', () => { - it('should create vercel.json in commit-hook mode if missing', async () => { - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - try { - await checker.check(); - } catch (error) { - expect(error.message).toBe( - 'vercel.json was created. Please review and stage the file before continuing.', - ); - } - - expect(existsSync(VERCEL_JSON_PATH)).toBe(true); - const content = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf8')); - expect(content).toEqual({ redirects: [] }); - }); - - it('should throw error in CI mode if vercel.json is missing', async () => { - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'ci', - }); - - const result = await checker.check(); - expect(result.error).toBe(`vercel.json not found at ${VERCEL_JSON_PATH}`); - }); - - it('should detect moved files differently in CI mode', async () => { - // Setup initial commit - writeFileSync(resolve(PAGES_DIR, 'old.md'), 'content'); - execSync('git add .', { cwd: TEST_DIR }); - execSync('git commit -m "initial"', { cwd: TEST_DIR }); - - // Move file - renameSync(resolve(PAGES_DIR, 'old.md'), resolve(PAGES_DIR, 'new.md')); - execSync('git add .', { cwd: TEST_DIR }); - execSync('git commit -m "move file"', { cwd: TEST_DIR }); - - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'ci', - }); - - // Create properly formatted vercel.json using prettier - const prettier = require('prettier'); - const options = await prettier.resolveConfig(process.cwd()) || {}; - const formattedContent = prettier.format(JSON.stringify({ redirects: [] }), { - ...options, - parser: 'json', - filepath: VERCEL_JSON_PATH, - }); - writeFileSync(VERCEL_JSON_PATH, formattedContent); - - const result = await checker.check(); - - expect(result.hasMissingRedirects).toBe(true); - expect(result.missingRedirects).toHaveLength(1); - expect(result.missingRedirects[0]).toEqual({ - from: '/(old/?)', - to: '/(new/?)', - }); - }); - }); - - describe('Redirect Management', () => { - it('should detect missing redirects for moved files', async () => { - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - // Setup vercel.json - writeFileSync(VERCEL_JSON_PATH, JSON.stringify({ redirects: [] }, null, 2)); - - // Create and move a file - writeFileSync(resolve(PAGES_DIR, 'old.md'), 'content'); - execSync('git add .', { cwd: TEST_DIR }); - execSync('git commit -m "add file"', { cwd: TEST_DIR }); - - renameSync(resolve(PAGES_DIR, 'old.md'), resolve(PAGES_DIR, 'new.md')); - execSync('git add .', { cwd: TEST_DIR }); - - try { - await checker.check(); - } catch (error) { - expect(error.message).toBe( - 'New redirects added to vercel.json. Please review and stage the changes before continuing.', - ); - } - - const config = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf8')); - expect(config.redirects).toHaveLength(1); - expect(config.redirects[0]).toEqual({ - source: '/(old/?)', - destination: '/(new/?)', - permanent: false, - }); - }); - - it('should not add duplicate redirects', async () => { - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - // Setup vercel.json with existing redirect - writeFileSync( - VERCEL_JSON_PATH, - JSON.stringify({ - redirects: [ - { - source: '/(old/?)', - destination: '/(new/?)', - permanent: false, - }, - ], - }), - ); - - // Create and move a file - writeFileSync(resolve(PAGES_DIR, 'old.md'), 'content'); - execSync('git add .', { cwd: TEST_DIR }); - execSync('git commit -m "add file"', { cwd: TEST_DIR }); - - renameSync(resolve(PAGES_DIR, 'old.md'), resolve(PAGES_DIR, 'new.md')); - execSync('git add .', { cwd: TEST_DIR }); - - const result = await checker.check(); - expect(result.hasMissingRedirects).toBe(false); - - const config = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf8')); - expect(config.redirects).toHaveLength(1); - }); - - it('should handle multiple file moves in one commit', async () => { - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - // Setup vercel.json - writeFileSync(VERCEL_JSON_PATH, JSON.stringify({ redirects: [] }, null, 2)); - - // Create and move multiple files - writeFileSync(resolve(PAGES_DIR, 'old1.md'), 'content'); - writeFileSync(resolve(PAGES_DIR, 'old2.md'), 'content'); - execSync('git add .', { cwd: TEST_DIR }); - execSync('git commit -m "add files"', { cwd: TEST_DIR }); - - renameSync(resolve(PAGES_DIR, 'old1.md'), resolve(PAGES_DIR, 'new1.md')); - renameSync(resolve(PAGES_DIR, 'old2.md'), resolve(PAGES_DIR, 'new2.md')); - execSync('git add .', { cwd: TEST_DIR }); - - try { - await checker.check(); - } catch (error) { - expect(error.message).toBe( - 'New redirects added to vercel.json. Please review and stage the changes before continuing.', - ); - } - - const config = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf8')); - expect(config.redirects).toHaveLength(2); - expect(config.redirects).toEqual([ - { - source: '/(old1/?)', - destination: '/(new1/?)', - permanent: false, - }, - { - source: '/(old2/?)', - destination: '/(new2/?)', - permanent: false, - }, - ]); - }); - - it('should not create redirects for newly added files', async () => { - // Setup vercel.json and commit it - writeFileSync(VERCEL_JSON_PATH, JSON.stringify({ redirects: [] }, null, 2)); - execSync('git add vercel.json', { cwd: TEST_DIR }); - execSync('git commit -m "Add empty vercel.json for new file test"', { cwd: TEST_DIR }); - - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - // Create and stage a new file - const newFilePath = resolve(PAGES_DIR, 'brand-new-file.md'); - writeFileSync(newFilePath, 'content'); - // Ensure the path used in git add is relative to TEST_DIR - execSync('git add pages/brand-new-file.md', { cwd: TEST_DIR }); - - let result: RedirectCheckResult | undefined; - try { - result = await checker.check(); - } catch (e: any) { - // Fail if it's the specific error we want to avoid - if ( - e.message === - 'New redirects added to vercel.json. Please review and stage the changes before continuing.' - ) { - throw new Error('Test failed: Redirects were unexpectedly added for a new file.'); - } - // Re-throw other unexpected errors - throw e; - } - - // If checker.check() did not throw the specific "New redirects added..." error, - // result should be defined. - expect(result).toBeDefined(); - // No other errors (like vercel.json not found/malformed, though unlikely here) should occur. - expect(result!.error).toBeUndefined(); - // No missing redirects should be flagged for a new file. - expect(result!.hasMissingRedirects).toBe(false); - - // Verify that vercel.json was not modified - const finalConfig = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf8')); - expect(finalConfig.redirects).toHaveLength(0); // Assuming it started empty - }); - - it('should not create a redirect if source and destination are the same after normalization', async () => { - // Setup vercel.json and commit it - writeFileSync(VERCEL_JSON_PATH, JSON.stringify({ redirects: [] }, null, 2)); - execSync('git add vercel.json', { cwd: TEST_DIR }); - execSync('git commit -m "Add empty vercel.json for self-redirect test"', { cwd: TEST_DIR }); - - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - // Simulate a move that results in the same normalized URL - // e.g. pages/old-path/index.md -> pages/old-path.md - // Both getUrlFromPath might resolve to something like /(old-path/?) - const oldFileDir = resolve(PAGES_DIR, 'self-redirect-test'); - mkdirSync(oldFileDir, { recursive: true }); - const oldFilePath = resolve(oldFileDir, 'index.md'); - const newFilePath = resolve(PAGES_DIR, 'self-redirect-test.md'); - - writeFileSync(oldFilePath, 'content'); - execSync('git add pages/self-redirect-test/index.md', { cwd: TEST_DIR }); - execSync('git commit -m "Add file for self-redirect test"', { cwd: TEST_DIR }); - - renameSync(oldFilePath, newFilePath); - // Add both old (now deleted) and new paths to staging for git to detect as a rename - execSync('git add pages/self-redirect-test/index.md pages/self-redirect-test.md', { - cwd: TEST_DIR, - }); - - let result: RedirectCheckResult | undefined; - try { - result = await checker.check(); - } catch (e: any) { - if ( - e.message === - 'New redirects added to vercel.json. Please review and stage the changes before continuing.' - ) { - throw new Error( - 'Test failed: Redirects were unexpectedly added when source and destination were the same.', - ); - } - throw e; - } - - expect(result).toBeDefined(); - expect(result!.error).toBeUndefined(); - expect(result!.hasMissingRedirects).toBe(false); - - const finalConfig = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf8')); - expect(finalConfig.redirects).toHaveLength(0); - }); - - it('should handle sorting and adding redirects in commit-hook mode', async () => { - const unsortedRedirects = [ - { - source: '/(zebra/?)', - destination: '/(zoo/?)', - permanent: false, - }, - { - source: '/(apple/?)', - destination: '/(fruit-basket/?)', - permanent: false, - }, - ]; - // Create an initially unsorted vercel.json - writeFileSync(VERCEL_JSON_PATH, JSON.stringify({ redirects: unsortedRedirects }, null, 2)); - execSync('git add vercel.json', { cwd: TEST_DIR }); // Stage it initially - execSync('git commit -m "add unsorted vercel.json"', { cwd: TEST_DIR }); - - let checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - // --- Step 1: Test re-sorting of an existing unsorted file --- - try { - await checker.check(); // This call should trigger loadVercelConfig - } catch (error: any) { - expect(error.message).toBe( - 'vercel.json was re-sorted and/or re-formatted. Please review and stage the changes before continuing.', - ); - } - // Verify the file is now sorted on disk - let currentConfig = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf8')); - expect(currentConfig.redirects).toEqual([ - { - source: '/(apple/?)', - destination: '/(fruit-basket/?)', - permanent: false, - }, - { - source: '/(zebra/?)', - destination: '/(zoo/?)', - permanent: false, - }, - ]); - - // --- Step 2: Simulate staging the re-sorted file and adding a new redirect --- - execSync('git add vercel.json', { cwd: TEST_DIR }); // Stage the sorted vercel.json - // No commit needed here, just need it staged for the next check() - - // Create and move a file to add a new redirect - writeFileSync(resolve(PAGES_DIR, 'old-banana.md'), 'content'); - execSync('git add pages/old-banana.md', { cwd: TEST_DIR }); - execSync('git commit -m "add banana file"', { cwd: TEST_DIR }); - - renameSync(resolve(PAGES_DIR, 'old-banana.md'), resolve(PAGES_DIR, 'new-yellow-fruit.md')); - execSync('git add pages/old-banana.md pages/new-yellow-fruit.md', { cwd: TEST_DIR }); - - // Re-initialize checker or ensure its internal state is fresh if necessary, - // though for this test, a new instance works fine. - checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - try { - await checker.check(); - } catch (error: any) { - expect(error.message).toBe( - 'New redirects added to vercel.json. Please review and stage the changes before continuing.', - ); - } - - // Verify the file on disk has the new redirect and is still sorted - currentConfig = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf8')); - expect(currentConfig.redirects).toEqual([ - { - source: '/(apple/?)', - destination: '/(fruit-basket/?)', - permanent: false, - }, - { - source: '/(old-banana/?)', - destination: '/(new-yellow-fruit/?)', - permanent: false, - }, - { - source: '/(zebra/?)', - destination: '/(zoo/?)', - permanent: false, - }, - ]); - }); - - it('should error in CI mode if vercel.json is unsorted', async () => { - const unsortedRedirects = [ - { source: '/(b/?)', destination: '/(c/?)', permanent: false }, - { source: '/(a/?)', destination: '/(d/?)', permanent: false }, - ]; - writeFileSync(VERCEL_JSON_PATH, JSON.stringify({ redirects: unsortedRedirects }, null, 2)); - // In CI, we assume vercel.json is part of the committed state. - - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'ci', - }); - - const result = await checker.check(); - expect(result.error).toBe( - 'vercel.json is not correctly sorted/formatted. Please run the pre-commit hook locally to fix and commit the changes.', - ); - // Ensure the file was not modified in CI mode - const fileContent = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf8')); - expect(fileContent.redirects).toEqual(unsortedRedirects); - }); - - it('should error if vercel.json is malformed', async () => { - writeFileSync(VERCEL_JSON_PATH, 'this is not json'); - execSync('git add vercel.json', { cwd: TEST_DIR }); // Stage the malformed file - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - const result = await checker.check(); - expect(result.error).toContain('Error parsing'); - expect(result.error).toContain('Please fix the JSON format and try again.'); - }); - - it('should handle cross-platform line endings and trailing whitespace', async () => { - const redirects = [ - { - source: '/(zebra/?)', - destination: '/(zoo/?)', - permanent: false, - }, - { - source: '/(apple/?)', - destination: '/(fruit/?)', - permanent: false, - }, - ]; - - // Create vercel.json files with different line ending styles but in wrong order (to trigger formatting) - const testCases = [ - { - name: 'CRLF line endings with trailing spaces', - content: JSON.stringify({ redirects }, null, 2).replace(/\n/g, '\r\n') + ' \r\n', - }, - { - name: 'LF line endings with trailing spaces', - content: JSON.stringify({ redirects }, null, 2) + ' \n', - }, - { - name: 'CR line endings with trailing spaces', - content: JSON.stringify({ redirects }, null, 2).replace(/\n/g, '\r') + ' \r', - }, - { - name: 'mixed line endings with various trailing whitespace', - content: JSON.stringify({ redirects }, null, 2).replace(/\n/g, '\r\n') + '\t \n ', - }, - ]; - - for (const testCase of testCases) { - // Write file with problematic formatting (unsorted to trigger reformatting) - writeFileSync(VERCEL_JSON_PATH, testCase.content); - execSync('git add vercel.json', { cwd: TEST_DIR }); - execSync(`git commit -m "Add vercel.json with ${testCase.name}"`, { cwd: TEST_DIR }); - - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - - const result = await checker.check(); - expect(result.hasMissingRedirects).toBe(false); - expect(result.error).toBeUndefined(); - - // Verify the file content was normalized properly after auto-formatting - const normalizedContent = readFileSync(VERCEL_JSON_PATH, 'utf8'); - // Prettier formats the JSON, so we just need to verify it's properly formatted and sorted - const parsedContent = JSON.parse(normalizedContent); - expect(parsedContent.redirects).toEqual([redirects[1], redirects[0]]); // Should be sorted - expect(normalizedContent.endsWith('\n')).toBe(true); - expect(normalizedContent.includes('\r')).toBe(false); - expect(/\s+$/.test(normalizedContent.replace(/\n$/, ''))).toBe(false); // No trailing spaces except final newline - } - }); - }); - - // Original tests - it('should pass when no files are moved', async () => { - // Setup: Create empty vercel.json and commit it - writeFileSync(VERCEL_JSON_PATH, JSON.stringify({ redirects: [] }, null, 2)); - execSync('git add vercel.json', { cwd: TEST_DIR }); - execSync('git commit -m "Add empty vercel.json"', { cwd: TEST_DIR }); - - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - const result = await checker.check(); - expect(result.hasMissingRedirects).toBe(false); - }); - - it('should pass when moved file has matching redirect', async () => { - // Setup: Add a redirect that matches what we'll test - const vercelJson = { - redirects: [ - { - source: '/(old-page/?)', - destination: '/(new-page/?)', - permanent: false, - }, - ], - }; - writeFileSync(VERCEL_JSON_PATH, JSON.stringify(vercelJson, null, 2)); - execSync('git add vercel.json', { cwd: TEST_DIR }); - execSync('git commit -m "Add test vercel.json"', { cwd: TEST_DIR }); - - // Create and move a test file - const oldPath = resolve(TEST_DIR, 'pages/old-page.mdx'); - const newPath = resolve(TEST_DIR, 'pages/new-page.mdx'); - mkdirSync(resolve(TEST_DIR, 'pages'), { recursive: true }); - writeFileSync(oldPath, 'test content'); - execSync('git add pages/old-page.mdx', { cwd: TEST_DIR }); - execSync('git commit -m "Add test file"', { cwd: TEST_DIR }); - - // Move the file and stage it - mkdirSync(dirname(newPath), { recursive: true }); - renameSync(oldPath, newPath); - execSync('git add pages/old-page.mdx pages/new-page.mdx', { cwd: TEST_DIR }); - - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - const result = await checker.check(); - expect(result.hasMissingRedirects).toBe(false); - }); - - it('should fail when vercel.json changes are not staged', async () => { - // Setup: Create empty vercel.json and commit it - writeFileSync(VERCEL_JSON_PATH, JSON.stringify({ redirects: [] }, null, 2)); - execSync('git add vercel.json', { cwd: TEST_DIR }); - execSync('git commit -m "Add empty vercel.json"', { cwd: TEST_DIR }); - - // Create unstaged changes - writeFileSync( - VERCEL_JSON_PATH, - JSON.stringify( - { - redirects: [ - { - source: '/test', - destination: '/test2', - permanent: false, - }, - ], - }, - null, - 2, - ), - ); - - // Verify that vercel.json is modified but not staged - const status = execSync('git status --porcelain', { cwd: TEST_DIR, encoding: 'utf8' }); - expect(status).toContain(' M vercel.json'); - - const checker = new RedirectChecker({ - vercelJsonPath: VERCEL_JSON_PATH, - mode: 'commit-hook', - }); - const result = await checker.check(); - expect(result.error).toBe( - 'Unstaged changes to vercel.json. Please review and stage the changes before continuing.', - ); - }); -}); diff --git a/scripts/check-redirects.ts b/scripts/check-redirects.ts deleted file mode 100644 index 91132b500a..0000000000 --- a/scripts/check-redirects.ts +++ /dev/null @@ -1,687 +0,0 @@ -import { execSync } from 'child_process'; -import { readFileSync, existsSync, writeFileSync } from 'fs'; -import { resolve, dirname, basename, parse } from 'path'; - -interface Redirect { - source: string; - destination: string; - permanent: boolean; -} - -interface VercelConfig { - redirects: Redirect[]; -} - -interface MovedFile { - oldPath: string; - newPath: string; -} - -export interface RedirectCheckResult { - hasMissingRedirects: boolean; - missingRedirects: Array<{ - from: string; - to: string; - }>; - error?: string; -} - -interface RedirectCheckerOptions { - vercelJsonPath?: string; - mode: 'commit-hook' | 'ci'; - gitCommand?: string; -} - -interface RedirectIndices { - bySource: Map; - byDestination: Map; -} - -/** - * RedirectChecker manages URL redirects in vercel.json when markdown files are moved or renamed. - * It ensures that existing links to documentation pages remain accessible after restructuring. - * - * Key features: - * 1. Creates vercel.json if it doesn't exist and requires review - * 2. Detects unstaged changes to vercel.json and prevents further processing - * 3. Uses git to detect moved/renamed .md(x) files in staged changes - * 4. Automatically adds non-permanent redirects for moved files - * 5. Flattens redirect chains while preserving all entry points (SEO-friendly) - * 6. Detects and prevents circular redirects - * 7. Requires manual review and staging of any changes to vercel.json - * - * The workflow: - * 1. Checks/creates vercel.json and validates its state - * 2. Detects file moves using git diff on staged changes - * 3. For each moved markdown file: - * - Converts file paths to URL paths (stripping extensions and prefixes) - * - Checks for existing matching redirects - * - Checks for circular redirect patterns - * - Flattens any existing chains (e.g., A→B becomes A→C when adding B→C) - * - Adds new redirects if needed (always non-permanent) - * - Result: All old URLs redirect directly to final destination (no chains) - * 4. If redirects are added: - * - Updates vercel.json - * - Requires manual review and staging before continuing - * - * Redirect chain handling example: - * - Initial state: A → B (existing redirect) - * - File move detected: B → C - * - Result: A → C, B → C (flattened, both entry points preserved) - * - * This ensures a controlled process for maintaining URL backwards compatibility - * while requiring human oversight of redirect changes and optimizing for SEO. - */ -export class RedirectChecker { - private vercelJsonPath: string; - private mode: 'commit-hook' | 'ci'; - private gitCommand?: string; - - constructor(options: RedirectCheckerOptions) { - this.vercelJsonPath = options.vercelJsonPath || resolve(process.cwd(), 'vercel.json'); - this.mode = options.mode; - this.gitCommand = options.gitCommand; - } - - /** - * Find the git repository root by looking for .git directory - * Cross-platform compatible (works on Windows, Unix, macOS) - */ - private findGitRoot(): string { - let currentDir = dirname(this.vercelJsonPath); - const { root } = parse(currentDir); - - while (currentDir !== root) { - if (existsSync(resolve(currentDir, '.git'))) { - return currentDir; - } - currentDir = dirname(currentDir); - } - throw new Error('Not in a git repository'); - } - - /** - * Build index maps for O(1) redirect lookups - * This significantly improves performance from O(n²) to O(n) for redirect operations - */ - private buildRedirectIndices(config: VercelConfig): RedirectIndices { - const bySource = new Map(); - const byDestination = new Map(); - - for (const redirect of config.redirects) { - const normalizedSource = this.normalizeUrl(redirect.source); - const normalizedDest = this.normalizeUrl(redirect.destination); - - bySource.set(normalizedSource, redirect); - - if (!byDestination.has(normalizedDest)) { - byDestination.set(normalizedDest, []); - } - byDestination.get(normalizedDest)!.push(redirect); - } - - return { bySource, byDestination }; - } - - /** - * Sorts the redirects array alphabetically by source, then by destination. - */ - private sortRedirects(redirects: Redirect[]): void { - redirects.sort((a, b) => { - const sourceA = a.source.toLowerCase(); - const sourceB = b.source.toLowerCase(); - if (sourceA < sourceB) return -1; - if (sourceA > sourceB) return 1; - - const destA = a.destination.toLowerCase(); - const destB = b.destination.toLowerCase(); - if (destA < destB) return -1; - if (destA > destB) return 1; - - return 0; - }); - } - - /** - * Normalize a URL by removing parentheses, trailing slashes, and ensuring single leading slash - */ - private normalizeUrl(url: string): string { - return ( - '/' + - url - .replace(/[()]/g, '') // Remove parentheses - .replace(/^\/+/, '') // Remove leading slashes before adding a single one - .replace(/\/+/g, '/') // Replace multiple slashes with single slash - .replace(/\/\?$/, '') // Remove optional trailing slash - .replace(/\/$/, '') - ); // Remove trailing slash - } - - /** - * Get moved files from git diff based on mode - */ - private getMovedFiles(): MovedFile[] { - const defaultCommands = { - 'commit-hook': 'git diff --cached --name-status -M100% --find-renames', - 'ci': 'git diff --name-status --diff-filter=R HEAD~1 HEAD', - }; - - const command = this.gitCommand || defaultCommands[this.mode]; - - try { - const output = execSync(command, { - encoding: 'utf8', - cwd: this.findGitRoot(), - }); - return !output.trim() - ? [] - : output - .trim() - .split('\n') - .map((line) => { - const match = line.match(/^R\d+\s+(.+?)\s+(.+?)$/); - if (match && (match[1].endsWith('.md') || match[1].endsWith('.mdx'))) { - return { - oldPath: match[1].trim(), - newPath: match[2].trim(), - }; - } - return null; - }) - .filter((file): file is MovedFile => file !== null); - } catch (error) { - return []; - } - } - - /** - * Format JSON content using prettier with project configuration - */ - private async formatJsonContent(content: string): Promise { - try { - const prettier = require('prettier'); - const options = (await prettier.resolveConfig(process.cwd())) || {}; - - return prettier.format(content, { - ...options, - parser: 'json', - filepath: this.vercelJsonPath, - }); - } catch (error) { - // Fallback to basic JSON formatting if prettier fails - console.warn('⚠️ Prettier formatting failed, using basic JSON formatting:', error); - return JSON.stringify(JSON.parse(content), null, 2) + '\n'; - } - } - - /** - * Normalize file content by handling cross-platform line endings and trailing whitespace - */ - private normalizeFileContent(content: string): string { - return content - .replace(/\r\n/g, '\n') // Convert CRLF to LF - .replace(/\r/g, '\n') // Convert CR to LF - .trim(); // Remove leading/trailing whitespace - } - - /** - * Load and parse the vercel.json configuration file - */ - private async loadVercelConfig(): Promise { - if (!existsSync(this.vercelJsonPath)) { - if (this.mode === 'commit-hook') { - const newConfig: VercelConfig = { redirects: [] }; - this.sortRedirects(newConfig.redirects); - const rawContent = JSON.stringify(newConfig); - const formattedContent = await this.formatJsonContent(rawContent); - writeFileSync(this.vercelJsonPath, formattedContent, 'utf8'); - throw new Error( - 'vercel.json was created. Please review and stage the file before continuing.', - ); - } else { - // ci mode - throw new Error(`vercel.json not found at ${this.vercelJsonPath}`); - } - } - - const originalFileContent = readFileSync(this.vercelJsonPath, 'utf8'); - let config: VercelConfig; - try { - config = JSON.parse(originalFileContent) as VercelConfig; - } catch (e) { - throw new Error( - `Error parsing ${this.vercelJsonPath}: ${ - (e as Error).message - }. Please fix the JSON format and try again.`, - ); - } - - if (!config.redirects || !Array.isArray(config.redirects)) { - config.redirects = []; // Ensure redirects array exists and is an array for safety - } - - // Sort the in-memory representation - this.sortRedirects(config.redirects); - - const rawContent = JSON.stringify(config); - const newFileContent = await this.formatJsonContent(rawContent); - - // Normalize both contents for comparison to handle cross-platform differences - const normalizedOriginal = this.normalizeFileContent(originalFileContent); - const normalizedNew = this.normalizeFileContent(newFileContent); - - // If in commit-hook mode and the content changed due to sorting or formatting - if (this.mode === 'commit-hook' && normalizedOriginal !== normalizedNew) { - writeFileSync(this.vercelJsonPath, newFileContent, 'utf8'); - // Auto-stage the reformatted vercel.json to prevent commit loop - try { - const gitRoot = this.findGitRoot(); - const relativePath = this.vercelJsonPath.replace(gitRoot + '/', ''); - execSync(`git add ${relativePath}`, { - cwd: gitRoot, - encoding: 'utf8', - }); - console.log('📝 vercel.json was automatically formatted and staged.'); - } catch (error) { - throw new Error( - 'vercel.json was re-sorted and/or re-formatted. Please review and stage the changes before continuing.', - ); - } - } - - // In CI mode, if the file was not sorted/formatted correctly, it's an error. - if (this.mode === 'ci' && normalizedOriginal !== normalizedNew) { - throw new Error( - `vercel.json is not correctly sorted/formatted. Please run the pre-commit hook locally to fix and commit the changes.`, - ); - } - - return config; // Return the (potentially modified in memory, and possibly written to disk) config - } - - /** - * Convert a file path to its corresponding URL path - */ - private getUrlFromPath(filePath: string): string { - // Remove the file extension and convert to URL path - let urlPath = filePath - .replace(/^(pages|arbitrum-docs)\//, '') // Remove leading directory - .replace(/\d{2,3}-/g, '') // Remove leading numbers like '01-', '02-', etc. - .replace(/\.mdx?$/, '') // Remove .md or .mdx extension - .replace(/\/index$/, ''); // Convert /index to / for cleaner URLs - - // Handle empty path (root index) - if (!urlPath || urlPath === 'index') { - return '/'; - } - - // Format URL to match existing patterns - return `/(${urlPath}/?)`; // Add parentheses and optional trailing slash - } - - /** - * Check if a redirect exists in the config - */ - private hasRedirect(config: VercelConfig, oldUrl: string, newUrl: string): boolean { - return config.redirects.some((redirect) => { - const normalizedSource = this.normalizeUrl(redirect.source); - const normalizedOldUrl = this.normalizeUrl(oldUrl); - const normalizedNewUrl = this.normalizeUrl(redirect.destination); - const normalizedDestination = this.normalizeUrl(newUrl); - - return normalizedSource === normalizedOldUrl && normalizedNewUrl === normalizedDestination; - }); - } - - /** - * Find all redirects that point TO a given URL (normalized comparison) - * Uses index-based lookup for O(1) performance - */ - private findRedirectsPointingTo(indices: RedirectIndices, url: string): Redirect[] { - const normalized = this.normalizeUrl(url); - return indices.byDestination.get(normalized) || []; - } - - /** - * Follow a redirect chain to find the ultimate destination - * Returns the final destination URL, detects circular references, and enforces max depth - * Uses index-based lookup for O(1) performance - */ - private resolveRedirectChain( - indices: RedirectIndices, - startUrl: string, - maxDepth: number = 10, - ): { destination: string; isCircular: boolean; depth: number } { - const visited = new Set(); - let current = this.normalizeUrl(startUrl); - let depth = 0; - - while (depth < maxDepth) { - if (visited.has(current)) { - // Found a circular reference - return { destination: current, isCircular: true, depth }; - } - - visited.add(current); - - // Find the next redirect in the chain using O(1) lookup - const nextRedirect = indices.bySource.get(current); - - if (!nextRedirect) { - // End of chain - current is the final destination - return { destination: current, isCircular: false, depth }; - } - - current = this.normalizeUrl(nextRedirect.destination); - depth++; - } - - // Max depth exceeded - likely indicates a problem - console.warn(`⚠️ Redirect chain exceeded max depth (${maxDepth}): ${startUrl}`); - return { destination: current, isCircular: true, depth }; - } - - /** - * Check if adding a redirect would create a circular reference - * Uses index-based lookup for O(1) performance - */ - private wouldCreateCircularRedirect( - indices: RedirectIndices, - source: string, - destination: string, - ): boolean { - const visited = new Set(); - let current = this.normalizeUrl(destination); - const normalizedSource = this.normalizeUrl(source); - - while (current) { - if (visited.has(current)) { - // Found a cycle in the existing redirects - return true; - } - if (current === normalizedSource) { - // Would create a loop back to source - return true; - } - - visited.add(current); - - // Find next redirect in chain using O(1) lookup - const next = indices.bySource.get(current); - - if (!next) break; - current = this.normalizeUrl(next.destination); - } - - return false; - } - - /** - * Flatten redirect chains while preserving all entry points - * This is the key method that implements the correct SEO-friendly approach: - * - Updates all redirects pointing to oldUrl to point to newUrl (flattens chains) - * - Does NOT remove the intermediate redirect (preserves all entry points) - * - Prevents creation of redirect chains (A→B→C becomes A→C, B→C) - * Uses index-based lookup for O(1) performance - */ - private flattenRedirectChains( - config: VercelConfig, - indices: RedirectIndices, - oldUrl: string, - newUrl: string, - ): void { - const normalizedOldUrl = this.normalizeUrl(oldUrl); - const normalizedNewUrl = this.normalizeUrl(newUrl); - - // Find all redirects that currently point to oldUrl using O(1) lookup - const redirectsPointingToOld = this.findRedirectsPointingTo(indices, oldUrl); - - // Update each redirect to point directly to newUrl (flatten the chain) - for (const redirect of redirectsPointingToOld) { - console.log( - ` 🔄 Flattening chain: ${redirect.source} → ${redirect.destination} becomes ${redirect.source} → ${newUrl}`, - ); - redirect.destination = newUrl; - } - - // Remove any existing redirect from oldUrl (will be replaced with the new one) - // This prevents having both A→B and A→C for the same source - const existingRedirect = indices.bySource.get(normalizedOldUrl); - - if (existingRedirect) { - const existingRedirectIndex = config.redirects.indexOf(existingRedirect); - if (existingRedirectIndex !== -1) { - console.log( - ` 🗑️ Removing old redirect: ${config.redirects[existingRedirectIndex].source} → ${config.redirects[existingRedirectIndex].destination}`, - ); - config.redirects.splice(existingRedirectIndex, 1); - } - } - } - - /** - * Detect and report existing redirect chains in the configuration - * Returns an array of chains found (for logging/debugging purposes) - * Uses index-based lookup for O(1) performance - */ - private detectExistingChains(indices: RedirectIndices): string[][] { - const chains: string[][] = []; - const processed = new Set(); - - // Convert Map keys to array for iteration - const sources = Array.from(indices.bySource.keys()); - - for (const source of sources) { - if (processed.has(source)) continue; - - const chain = [source]; - let current = this.normalizeUrl(indices.bySource.get(source)!.destination); - - // Follow the chain - while (true) { - chain.push(current); - - const nextRedirect = indices.bySource.get(current); - - if (!nextRedirect) break; - - const next = this.normalizeUrl(nextRedirect.destination); - if (chain.includes(next)) { - // Circular reference detected - chain.push(next); - chains.push(chain); - break; - } - - current = next; - } - - // Mark all URLs in this chain as processed - for (const url of chain) { - processed.add(url); - } - - // Only report if it's a chain (length > 2) or circular (last === first) - if (chain.length > 2) { - chains.push(chain); - } - } - - return chains; - } - - /** - * Add a new redirect to the config with chain flattening - * This method implements the SEO-friendly approach: - * 1. Checks for circular redirects - * 2. Flattens any existing chains (updates A→B to A→C when adding B→C) - * 3. Adds the new redirect (B→C) - * 4. Result: A→C, B→C (no chains, all entry points preserved) - * Uses index-based lookups for O(1) performance - */ - private async addRedirect(oldUrl: string, newUrl: string, config: VercelConfig): Promise { - const normalizedOldUrl = this.normalizeUrl(oldUrl); - const normalizedNewUrl = this.normalizeUrl(newUrl); - - // Skip if source and destination are the same - if (normalizedOldUrl === normalizedNewUrl) { - console.log(` ⏭️ Skipping self-redirect: ${oldUrl} → ${newUrl}`); - return; - } - - // Build indices for efficient lookups - const indices = this.buildRedirectIndices(config); - - // Check for circular redirects - if (this.wouldCreateCircularRedirect(indices, oldUrl, newUrl)) { - console.warn( - ` ⚠️ Warning: Skipping redirect ${oldUrl} → ${newUrl} - would create circular chain`, - ); - return; - } - - // Flatten any existing chains pointing to oldUrl - console.log(` ➕ Adding redirect: ${oldUrl} → ${newUrl}`); - this.flattenRedirectChains(config, indices, oldUrl, newUrl); - - // Add the new redirect - config.redirects.push({ - source: oldUrl, - destination: newUrl, - permanent: false, - }); - - this.sortRedirects(config.redirects); - const rawContent = JSON.stringify(config); - const formattedContent = await this.formatJsonContent(rawContent); - writeFileSync(this.vercelJsonPath, formattedContent, 'utf8'); - } - - /** - * Check for missing redirects and optionally update vercel.json - */ - public async check(): Promise { - try { - // In commit-hook mode, check for unstaged changes - if (this.mode === 'commit-hook') { - const gitRoot = this.findGitRoot(); - const initialStatus = execSync('git status --porcelain', { - cwd: gitRoot, - encoding: 'utf8', - }); - const vercelJsonStatus = initialStatus - .split('\n') - .find((line) => line.includes(basename(this.vercelJsonPath))); - - if ( - vercelJsonStatus && - (vercelJsonStatus.startsWith(' M') || vercelJsonStatus.startsWith('??')) - ) { - throw new Error( - 'Unstaged changes to vercel.json. Please review and stage the changes before continuing.', - ); - } - } - - const config = await this.loadVercelConfig(); - const movedFiles = this.getMovedFiles(); - - if (movedFiles.length === 0) { - return { - hasMissingRedirects: false, - missingRedirects: [], - }; - } - - const missingRedirects: Array<{ from: string; to: string }> = []; - let redirectsAdded = false; - - for (const { oldPath, newPath } of movedFiles) { - const oldUrl = this.getUrlFromPath(oldPath); - const newUrl = this.getUrlFromPath(newPath); - - if (newUrl.includes('archive')) { - // Skip archived files - continue; - } - - if (!this.hasRedirect(config, oldUrl, newUrl)) { - missingRedirects.push({ from: oldUrl, to: newUrl }); - - // Only add redirects in commit-hook mode - if (this.mode === 'commit-hook') { - const countBeforeAdd = config.redirects.length; - await this.addRedirect(oldUrl, newUrl, config); // addRedirect might not add if old/new are same - if (config.redirects.length > countBeforeAdd) { - redirectsAdded = true; - } - } - } - } - - if (this.mode === 'commit-hook' && redirectsAdded) { - throw new Error( - 'New redirects added to vercel.json. Please review and stage the changes before continuing.', - ); - } - - // Determine final hasMissingRedirects status - let finalHasMissingRedirects = false; - if (this.mode === 'ci') { - finalHasMissingRedirects = missingRedirects.length > 0; - // In CI, also filter out self-redirects from the reported missingRedirects array - // as these aren't actionable and shouldn't fail CI if they are the only thing found. - const actionableMissingRedirects = missingRedirects.filter((r) => { - return this.normalizeUrl(r.from) !== this.normalizeUrl(r.to); - }); - finalHasMissingRedirects = actionableMissingRedirects.length > 0; - // Return the actionable list for CI to report - return { - hasMissingRedirects: finalHasMissingRedirects, - missingRedirects: actionableMissingRedirects, - }; - } else { - // commit-hook - finalHasMissingRedirects = redirectsAdded; - } - - return { - hasMissingRedirects: finalHasMissingRedirects, - missingRedirects, // In commit-hook, this list might contain self-redirects, but hasMissingRedirects guides action - }; - } catch (error) { - return { - hasMissingRedirects: false, - missingRedirects: [], - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - } -} - -// CLI entry point -if (require.main === module) { - const mode = process.argv.includes('--ci') ? 'ci' : 'commit-hook'; - const checker = new RedirectChecker({ mode }); - - checker.check().then((result) => { - if (result.error) { - console.error('Error:', result.error); - process.exit(1); - } - - if (result.hasMissingRedirects) { - console.error('❌ Missing redirects found:'); - for (const redirect of result.missingRedirects) { - console.error(` From: ${redirect.from}`); - console.error(` To: ${redirect.to}`); - } - process.exit(1); - } - - if (mode === 'ci') { - console.log('✅ All necessary redirects are in place'); - } - process.exit(0); - }); -} From b8c0a7eb44a487600699da00e7917a197808304f Mon Sep 17 00:00:00 2001 From: Pete Date: Fri, 26 Dec 2025 12:20:16 -0600 Subject: [PATCH 2/7] Adding check-markdown.ts script to find deleted md or mdx files --- .github/workflows/test.yml | 3 +++ package.json | 1 + scripts/check-markdown.ts | 40 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 scripts/check-markdown.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6363b4739..8cf462c259 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,3 +27,6 @@ jobs: - name: Run tests run: yarn test + + - name: Check for deleted markdown files + run: yarn tsx scripts/check-markdown.ts --ci \ No newline at end of file diff --git a/package.json b/package.json index b3702de3c7..6a31b9593e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "test": "vitest run", "test:watch": "vitest", "check-releases": "ts-node scripts/check-releases.ts", + "check-markdown": "tsx scripts/check-markdown.ts", "notion:update": "tsx scripts/notion-update.ts", "notion:verify-quicklooks": "tsx scripts/notion-verify-quicklooks.ts", "lint:markdown": "markdownlint \"docs/**/*.{md,mdx}\" --ignore \"docs/sdk/**\"", diff --git a/scripts/check-markdown.ts b/scripts/check-markdown.ts new file mode 100644 index 0000000000..1fd79237bb --- /dev/null +++ b/scripts/check-markdown.ts @@ -0,0 +1,40 @@ +import { execSync } from 'child_process'; +import { exit } from 'process'; + +// Function to check for staged deletions of .md or .mdx files +function checkStagedMarkdownDeletions(): void { + try { + // Run git diff --cached --name-status to get staged changes + const output = execSync('git diff --cached --name-status').toString().trim(); + + // Split the output into lines + const lines = output.split('\n'); + + // Filter for deletions (D) of .md or .mdx files + const deletedMarkdownFiles = lines + .filter((line) => { + const parts = line.split('\t'); + const status = parts[0]; + const file = parts[1]; + return status === 'D' && (file.endsWith('.md') || file.endsWith('.mdx')); + }) + .map((line) => line.split('\t')[1]); // Extract the file names + + if (deletedMarkdownFiles.length > 0) { + console.error('Error: The following Markdown files are staged for deletion:'); + deletedMarkdownFiles.forEach((file) => console.error('- ${file}')); + console.error('Please unstage these deletions or remove them if unintended.'); + exit(1); + } else { + console.log('No staged deletions of Markdown files found.'); + exit(0); + } + } catch (error) { + console.error('Failed to execute git command. Ensure this is run in a git repository.'); + console.error(error.message); + exit(1); + } +} + +// Run the check +checkStagedMarkdownDeletions(); From 895297bf0049bd846af4a79a989ecb7d16b57fd3 Mon Sep 17 00:00:00 2001 From: Pete Date: Fri, 26 Dec 2025 12:27:13 -0600 Subject: [PATCH 3/7] Fixing code to display filenames that have been deleted. --- scripts/check-markdown.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/check-markdown.ts b/scripts/check-markdown.ts index 1fd79237bb..8de72af3f3 100644 --- a/scripts/check-markdown.ts +++ b/scripts/check-markdown.ts @@ -21,10 +21,14 @@ function checkStagedMarkdownDeletions(): void { .map((line) => line.split('\t')[1]); // Extract the file names if (deletedMarkdownFiles.length > 0) { + console.error('************************************************************'); + console.error('************************************************************'); console.error('Error: The following Markdown files are staged for deletion:'); - deletedMarkdownFiles.forEach((file) => console.error('- ${file}')); + deletedMarkdownFiles.forEach((file) => console.error(`- ${file}`)); console.error('Please unstage these deletions or remove them if unintended.'); - exit(1); + console.error('************************************************************'); + console.error('************************************************************'); + exit(0); } else { console.log('No staged deletions of Markdown files found.'); exit(0); From cecff8fe18bb6832167640a926c37dcfeb2907c5 Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 30 Dec 2025 13:11:59 -0600 Subject: [PATCH 4/7] Expanding script to capture not only deleted files; but moved/renamed Markdown files --- scripts/check-markdown.ts | 60 +++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/scripts/check-markdown.ts b/scripts/check-markdown.ts index 8de72af3f3..d0eda4667a 100644 --- a/scripts/check-markdown.ts +++ b/scripts/check-markdown.ts @@ -1,8 +1,8 @@ import { execSync } from 'child_process'; import { exit } from 'process'; -// Function to check for staged deletions of .md or .mdx files -function checkStagedMarkdownDeletions(): void { +// Function to check for staged deletions, moves, or renames or .md and .mdx files +function checkStagedMarkdownChanges(): void { try { // Run git diff --cached --name-status to get staged changes const output = execSync('git diff --cached --name-status').toString().trim(); @@ -10,27 +10,45 @@ function checkStagedMarkdownDeletions(): void { // Split the output into lines const lines = output.split('\n'); - // Filter for deletions (D) of .md or .mdx files - const deletedMarkdownFiles = lines - .filter((line) => { - const parts = line.split('\t'); - const status = parts[0]; + // Array to hold details of affected files + const affectedFiles: string[] = []; + + lines.forEach((line) => { + if (!line.trim()) return; + + const parts = line.split('\t'); + const status = parts[0]; + + if (status === 'D') { const file = parts[1]; - return status === 'D' && (file.endsWith('.md') || file.endsWith('.mdx')); - }) - .map((line) => line.split('\t')[1]); // Extract the file names - - if (deletedMarkdownFiles.length > 0) { - console.error('************************************************************'); - console.error('************************************************************'); - console.error('Error: The following Markdown files are staged for deletion:'); - deletedMarkdownFiles.forEach((file) => console.error(`- ${file}`)); - console.error('Please unstage these deletions or remove them if unintended.'); - console.error('************************************************************'); - console.error('************************************************************'); + if (file.endsWith('.md') || file.endsWith('.mdx')) { + affectedFiles.push('Deleted: ${file}'); + } + } else if (status.startsWith('R')) { + // For renames: parts[0] is Rxxx, parts[1] is old file, parts[2] is new file + const oldFile = parts[1]; + const newFile = parts[2]; + if (oldFile.endsWith('.md') || oldFile.endsWith('.mdx')) { + affectedFiles.push('Renamed/Moved: ${oldFile} -> ${newFile}'); + } + } + }); + + if (affectedFiles.length > 0) { + console.error( + '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #', + ); + console.error( + 'Error: The following Markdown files are staged for deletion, were moved or renamed:', + ); + affectedFiles.forEach((detail) => console.error(`- ${detail}`)); + console.error('Please unstage these changes or review them if unintended.'); + console.error( + '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #', + ); exit(0); } else { - console.log('No staged deletions of Markdown files found.'); + console.log('No staged deletions, moved or renamed Markdown files.'); exit(0); } } catch (error) { @@ -41,4 +59,4 @@ function checkStagedMarkdownDeletions(): void { } // Run the check -checkStagedMarkdownDeletions(); +checkStagedMarkdownChanges; From 1c7e6a6781ea3dbd280e2bec83e506ff2b469b60 Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 30 Dec 2025 13:18:38 -0600 Subject: [PATCH 5/7] rolling back for further testing --- scripts/check-markdown.ts | 60 ++++++++++++++------------------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/scripts/check-markdown.ts b/scripts/check-markdown.ts index d0eda4667a..8de72af3f3 100644 --- a/scripts/check-markdown.ts +++ b/scripts/check-markdown.ts @@ -1,8 +1,8 @@ import { execSync } from 'child_process'; import { exit } from 'process'; -// Function to check for staged deletions, moves, or renames or .md and .mdx files -function checkStagedMarkdownChanges(): void { +// Function to check for staged deletions of .md or .mdx files +function checkStagedMarkdownDeletions(): void { try { // Run git diff --cached --name-status to get staged changes const output = execSync('git diff --cached --name-status').toString().trim(); @@ -10,45 +10,27 @@ function checkStagedMarkdownChanges(): void { // Split the output into lines const lines = output.split('\n'); - // Array to hold details of affected files - const affectedFiles: string[] = []; - - lines.forEach((line) => { - if (!line.trim()) return; - - const parts = line.split('\t'); - const status = parts[0]; - - if (status === 'D') { + // Filter for deletions (D) of .md or .mdx files + const deletedMarkdownFiles = lines + .filter((line) => { + const parts = line.split('\t'); + const status = parts[0]; const file = parts[1]; - if (file.endsWith('.md') || file.endsWith('.mdx')) { - affectedFiles.push('Deleted: ${file}'); - } - } else if (status.startsWith('R')) { - // For renames: parts[0] is Rxxx, parts[1] is old file, parts[2] is new file - const oldFile = parts[1]; - const newFile = parts[2]; - if (oldFile.endsWith('.md') || oldFile.endsWith('.mdx')) { - affectedFiles.push('Renamed/Moved: ${oldFile} -> ${newFile}'); - } - } - }); - - if (affectedFiles.length > 0) { - console.error( - '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #', - ); - console.error( - 'Error: The following Markdown files are staged for deletion, were moved or renamed:', - ); - affectedFiles.forEach((detail) => console.error(`- ${detail}`)); - console.error('Please unstage these changes or review them if unintended.'); - console.error( - '# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #', - ); + return status === 'D' && (file.endsWith('.md') || file.endsWith('.mdx')); + }) + .map((line) => line.split('\t')[1]); // Extract the file names + + if (deletedMarkdownFiles.length > 0) { + console.error('************************************************************'); + console.error('************************************************************'); + console.error('Error: The following Markdown files are staged for deletion:'); + deletedMarkdownFiles.forEach((file) => console.error(`- ${file}`)); + console.error('Please unstage these deletions or remove them if unintended.'); + console.error('************************************************************'); + console.error('************************************************************'); exit(0); } else { - console.log('No staged deletions, moved or renamed Markdown files.'); + console.log('No staged deletions of Markdown files found.'); exit(0); } } catch (error) { @@ -59,4 +41,4 @@ function checkStagedMarkdownChanges(): void { } // Run the check -checkStagedMarkdownChanges; +checkStagedMarkdownDeletions(); From 4c133a3f9da2a1ae596739377a0abd61bdb279fe Mon Sep 17 00:00:00 2001 From: Pete Date: Tue, 30 Dec 2025 13:30:11 -0600 Subject: [PATCH 6/7] adding rename/move --- scripts/check-markdown.ts | 53 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/scripts/check-markdown.ts b/scripts/check-markdown.ts index 8de72af3f3..f3bab2bda2 100644 --- a/scripts/check-markdown.ts +++ b/scripts/check-markdown.ts @@ -21,13 +21,11 @@ function checkStagedMarkdownDeletions(): void { .map((line) => line.split('\t')[1]); // Extract the file names if (deletedMarkdownFiles.length > 0) { - console.error('************************************************************'); - console.error('************************************************************'); + console.error('# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # '); console.error('Error: The following Markdown files are staged for deletion:'); deletedMarkdownFiles.forEach((file) => console.error(`- ${file}`)); console.error('Please unstage these deletions or remove them if unintended.'); - console.error('************************************************************'); - console.error('************************************************************'); + console.error('# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # '); exit(0); } else { console.log('No staged deletions of Markdown files found.'); @@ -40,5 +38,52 @@ function checkStagedMarkdownDeletions(): void { } } +// Function to check for staged renames or moves of .md or .mdx files +function checkStagedMarkdownRenames(): void { + try { + // Run git diff --cached --name-status to get staged changes + const output = execSync('git diff --cached --name-status').toString().trim(); + + // Split the output into lines + const lines = output.split('\n'); + + // Array to hold details of renamed/moved files + const renamedMarkdownFiles: string[] = []; + + lines.forEach((line) => { + if (!line.trim()) return; + + const parts = line.split('\t'); + const status = parts[0]; + + if (status.startsWith('R')) { + // For renames: parts[0] is Rxxx, parts[1] is old file, parts[2] is new file + const oldFile = parts[1]; + const newFile = parts[2]; + if (oldFile.endsWith('.md') || oldFile.endsWith('.mdx')) { + renamedMarkdownFiles.push(`${oldFile} -> ${newFile}`); + } + } + }); + + if (renamedMarkdownFiles.length > 0) { + console.error('# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # '); + console.error('Error: The following Markdown files are staged for rename or move:'); + renamedMarkdownFiles.forEach((detail) => console.error(`- ${detail}`)); + console.error('Please unstage these changes or review them if unintended.'); + console.error('# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # '); + exit(0); + } else { + console.log('No staged renames or moves of Markdown files found.'); + exit(0); + } + } catch (error) { + console.error('Failed to execute git command. Ensure this is run in a git repository.'); + console.error(error.message); + exit(1); + } +} + // Run the check checkStagedMarkdownDeletions(); +checkStagedMarkdownRenames(); From 669359c9ee64bebbef33e7a2ea8f2d840295fa52 Mon Sep 17 00:00:00 2001 From: Pete Date: Wed, 31 Dec 2025 12:55:28 -0600 Subject: [PATCH 7/7] Fixing issue of combining scripts into a single file --- scripts/check-markdown.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/scripts/check-markdown.ts b/scripts/check-markdown.ts index f3bab2bda2..b3dce3e4ea 100644 --- a/scripts/check-markdown.ts +++ b/scripts/check-markdown.ts @@ -2,14 +2,12 @@ import { execSync } from 'child_process'; import { exit } from 'process'; // Function to check for staged deletions of .md or .mdx files -function checkStagedMarkdownDeletions(): void { +function checkStagedMarkdownDeletions() { try { // Run git diff --cached --name-status to get staged changes const output = execSync('git diff --cached --name-status').toString().trim(); - // Split the output into lines const lines = output.split('\n'); - // Filter for deletions (D) of .md or .mdx files const deletedMarkdownFiles = lines .filter((line) => { @@ -19,17 +17,15 @@ function checkStagedMarkdownDeletions(): void { return status === 'D' && (file.endsWith('.md') || file.endsWith('.mdx')); }) .map((line) => line.split('\t')[1]); // Extract the file names - if (deletedMarkdownFiles.length > 0) { console.error('# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # '); console.error('Error: The following Markdown files are staged for deletion:'); deletedMarkdownFiles.forEach((file) => console.error(`- ${file}`)); console.error('Please unstage these deletions or remove them if unintended.'); console.error('# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # '); - exit(0); } else { console.log('No staged deletions of Markdown files found.'); - exit(0); + // No exit here, to allow continuing to the next check } } catch (error) { console.error('Failed to execute git command. Ensure this is run in a git repository.'); @@ -39,7 +35,7 @@ function checkStagedMarkdownDeletions(): void { } // Function to check for staged renames or moves of .md or .mdx files -function checkStagedMarkdownRenames(): void { +function checkStagedMarkdownRenames() { try { // Run git diff --cached --name-status to get staged changes const output = execSync('git diff --cached --name-status').toString().trim(); @@ -72,10 +68,9 @@ function checkStagedMarkdownRenames(): void { renamedMarkdownFiles.forEach((detail) => console.error(`- ${detail}`)); console.error('Please unstage these changes or review them if unintended.'); console.error('# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # '); - exit(0); } else { console.log('No staged renames or moves of Markdown files found.'); - exit(0); + // No exit here, to allow the script to complete } } catch (error) { console.error('Failed to execute git command. Ensure this is run in a git repository.'); @@ -84,6 +79,6 @@ function checkStagedMarkdownRenames(): void { } } -// Run the check +// Run both checks checkStagedMarkdownDeletions(); checkStagedMarkdownRenames();