diff --git a/src/lib/Translation/Extractor/JavaScriptFileVisitor.php b/src/lib/Translation/Extractor/JavaScriptFileVisitor.php index f253caab45..c98233eb10 100644 --- a/src/lib/Translation/Extractor/JavaScriptFileVisitor.php +++ b/src/lib/Translation/Extractor/JavaScriptFileVisitor.php @@ -20,7 +20,10 @@ use Peast\Syntax\Node; use Psr\Log\LoggerAwareTrait; use Psr\Log\NullLogger; +use RuntimeException; use SplFileInfo; +use Symfony\Component\Process\ExecutableFinder; +use Symfony\Component\Process\Process; use Twig\Node\Node as TwigNode; final class JavaScriptFileVisitor implements FileVisitorInterface, LoggerAwareInterface @@ -57,6 +60,10 @@ public function visitFile(SplFileInfo $file, MessageCatalogue $catalogue): void try { $source = file_get_contents($file->getRealPath()); + if (str_ends_with($file->getRealPath(), '.ts')) { + $source = $this->transpileTypeScript($source); + } + $parser = Peast::latest($source, [ 'comments' => true, 'jsx' => true, @@ -73,6 +80,14 @@ public function visitFile(SplFileInfo $file, MessageCatalogue $catalogue): void $e->getPosition()->getColumn() )); + return; + } catch (RuntimeException $e) { + $this->logger?->error(sprintf( + 'Unable to parse file %s: %s', + $file->getRealPath(), + $e->getMessage() + )); + return; } @@ -204,8 +219,51 @@ private function extractDesc(array $arguments): ?string return null; } + /** + * Strips TypeScript syntax (type annotations, interfaces, generics, etc.) so the + * resulting source can be parsed by Peast, which only understands plain ECMAScript. + */ + private function transpileTypeScript(string $source): string + { + $process = new Process([ + $this->findEsbuildBinary(), + '--loader=ts', + '--format=esm', + '--target=esnext', + ]); + $process->setInput($source); + $process->run(); + + if (!$process->isSuccessful()) { + throw new RuntimeException(sprintf( + 'Unable to transpile TypeScript source: %s', + $process->getErrorOutput() + )); + } + + return $process->getOutput(); + } + + private function findEsbuildBinary(): string + { + $candidate = getcwd() . '/node_modules/.bin/esbuild'; + if (is_executable($candidate)) { + return $candidate; + } + + $binary = (new ExecutableFinder())->find('esbuild'); + if ($binary !== null) { + return $binary; + } + + throw new RuntimeException( + 'Unable to locate the "esbuild" executable required to parse TypeScript files. ' . + 'Run this command from the project root or install esbuild (yarn add -D esbuild).' + ); + } + private function supports(SplFileInfo $file): bool { - return str_ends_with($file->getRealPath(), '.js') && !str_ends_with($file->getRealPath(), '.min.js'); + return (str_ends_with($file->getRealPath(), '.js') || str_ends_with($file->getRealPath(), '.ts')) && !str_ends_with($file->getRealPath(), '.min.js'); } }