Skip to content

Conversation

fredrikekelund
Copy link
Contributor

@fredrikekelund fredrikekelund commented Feb 4, 2025

Related issues

Proposed Changes

Change all relative import paths to be absolute. Also take jest.mock calls into account. I used the following script (written in large parts by Cursor) to do this:

Click to view the script
import * as fs from 'fs/promises';
import * as path from 'path';
import * as ts from 'typescript';

// Helper function to check if a file exists
async function pathExists( path: string ): Promise< boolean > {
	try {
		await fs.access( path );
		return true;
	} catch ( err: unknown ) {
		if ( ( err as { code?: string } ).code === 'ENOENT' ) {
			return false;
		}
		throw err;
	}
}

async function rewriteImports( directory: string ) {
	// Get all TypeScript files recursively
	async function getTypeScriptFiles( dir: string ): Promise< string[] > {
		const files: string[] = [];
		const entries = await fs.readdir( dir, { withFileTypes: true } );

		for ( const entry of entries ) {
			const fullPath = path.join( dir, entry.name );
			if ( entry.isDirectory() ) {
				files.push( ...( await getTypeScriptFiles( fullPath ) ) );
			} else if ( entry.isFile() && /\.(ts|tsx)$/.test( entry.name ) ) {
				files.push( fullPath );
			}
		}

		return files;
	}

	// Convert relative path to absolute path (starting with 'src/')
	async function convertToAbsolutePath(
		currentFile: string,
		importPath: string
	): Promise< string | null > {
		if ( ! importPath.startsWith( '.' ) ) {
			return importPath;
		}

		const absolutePath = path.resolve( path.dirname( currentFile ), importPath );
		const relativePath = path.relative( __dirname, absolutePath );

		if ( ! relativePath.startsWith( 'vendor/' ) && ! relativePath.startsWith( 'src/' ) ) {
			return null;
		}

		// Try exact path first
		if ( await pathExists( relativePath ) ) {
			return relativePath;
		}

		// Try with extensions
		for ( const ext of [ '.ts', '.tsx', '/index.ts', '/index.tsx' ] ) {
			const pathWithExt = relativePath + ext;
			if ( await pathExists( pathWithExt ) ) {
				return relativePath; // Return without extension as TypeScript will resolve it
			}
		}

		// If no valid file found, return null
		return null;
	}

	type PathTransformation = {
		node: ts.Node;
		path: string;
		start: number;
		end: number;
	};

	// Helper function to process path transformations
	async function processPathTransformations(
		filePath: string,
		transformations: PathTransformation[]
	): Promise< string | null > {
		let hasChanges = false;
		let newContent = await fs.readFile( filePath, 'utf-8' );

		// Process in reverse order to maintain correct positions
		for ( let i = transformations.length - 1; i >= 0; i-- ) {
			const { path: importPath, start, end } = transformations[ i ];

			// Skip if not a relative import
			if ( ! importPath.startsWith( '.' ) ) {
				continue;
			}

			const absolutePath = await convertToAbsolutePath( filePath, importPath );
			if ( absolutePath === null ) {
				console.warn( `Warning: Could not find file for path ${ importPath } in ${ filePath }` );
				continue;
			}

			newContent = newContent.slice( 0, start ) + `'${ absolutePath }'` + newContent.slice( end );
			hasChanges = true;
		}

		return hasChanges ? newContent : null;
	}

	// Process a single file
	async function processFile( filePath: string ) {
		const sourceFile = ts.createSourceFile(
			filePath,
			await fs.readFile( filePath, 'utf-8' ),
			ts.ScriptTarget.Latest,
			true
		);

		const transformations: PathTransformation[] = [];

		// Collect import transformations
		sourceFile.statements.filter( ts.isImportDeclaration ).forEach( ( importDecl ) => {
			transformations.push( {
				node: importDecl,
				path: importDecl.moduleSpecifier.getText().slice( 1, -1 ),
				start: importDecl.moduleSpecifier.getStart( sourceFile ),
				end: importDecl.moduleSpecifier.getEnd(),
			} );
		} );

		// Collect jest.mock transformations for test files
		if ( filePath.includes( '.test.' ) || filePath.includes( '.spec.' ) ) {
			findJestMockCalls( sourceFile ).forEach( ( mockCall ) => {
				transformations.push( {
					node: mockCall,
					path: mockCall.arguments[ 0 ].getText().slice( 1, -1 ),
					start: mockCall.arguments[ 0 ].getStart( sourceFile ),
					end: mockCall.arguments[ 0 ].getEnd(),
				} );
			} );
		}

		const newContent = await processPathTransformations( filePath, transformations );
		if ( newContent !== null ) {
			await fs.writeFile( filePath, newContent, 'utf-8' );
			console.log( `Updated imports/mocks in ${ filePath }` );
		}
	}

	// Helper function to find jest.mock calls in the AST
	function findJestMockCalls( sourceFile: ts.SourceFile ): ts.CallExpression[] {
		const mockCalls: ts.CallExpression[] = [];

		function visit( node: ts.Node ) {
			if (
				ts.isCallExpression( node ) &&
				ts.isPropertyAccessExpression( node.expression ) &&
				node.expression.expression.getText() === 'jest' &&
				node.expression.name.getText() === 'mock' &&
				node.arguments.length > 0 &&
				ts.isStringLiteral( node.arguments[ 0 ] )
			) {
				mockCalls.push( node );
			}
			ts.forEachChild( node, visit );
		}

		ts.forEachChild( sourceFile, visit );
		return mockCalls;
	}

	// Main execution
	try {
		const files = await getTypeScriptFiles( directory );
		for ( const file of files ) {
			await processFile( file );
		}
		console.log( 'Import rewriting completed successfully!' );
	} catch ( error ) {
		console.error( 'Error processing files:', error );
	}
}

// Usage
rewriteImports( path.join( __dirname, 'src' ) );

Testing Instructions

CI jobs should pass.

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@fredrikekelund fredrikekelund requested a review from a team February 4, 2025 12:11
@fredrikekelund fredrikekelund self-assigned this Feb 4, 2025
@fredrikekelund fredrikekelund force-pushed the f26d/rewrite-relative-imports branch from b4cd234 to f50f396 Compare February 5, 2025 15:14
@fredrikekelund fredrikekelund force-pushed the f26d/rewrite-relative-imports branch from f50f396 to 1a8d9ee Compare February 5, 2025 15:17
Copy link
Contributor

@nightnei nightnei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@fredrikekelund fredrikekelund merged commit 36da255 into trunk Feb 5, 2025
7 checks passed
@fredrikekelund fredrikekelund deleted the f26d/rewrite-relative-imports branch February 5, 2025 15:39
Copy link

sentry-io bot commented Feb 10, 2025

Suspect Issues

This pull request was deployed and Sentry observed the following issues:

  • ‼️ Error: Path contains invalid characters: C:\Users\tsdad\AppData\Local\Temp\studio_backupvDPy1v\cache\page_enhanced\34.79.154.38:80 <anonymous>(main/studio/./src/lib/import-export... View Issue

Did you find this useful? React with a 👍 or 👎

@fredrikekelund
Copy link
Contributor Author

The Sentry issue is unrelated to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants