Skip to content

Instantly share code, notes, and snippets.

@royteusink
Last active November 19, 2024 19:15
Show Gist options
  • Save royteusink/e685cc82a0ba5d1bb19f4f245d9a1fc2 to your computer and use it in GitHub Desktop.
Save royteusink/e685cc82a0ba5d1bb19f4f245d9a1fc2 to your computer and use it in GitHub Desktop.
Generate Laravel routes for typescript (SPA)

Laravel Command to generate route endpoints that can be used in typescript fully threeshakable.

GenerateTypescriptRoutes.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;

class GenerateTypescriptRoutes extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:generate-typescript-routes';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Generate a typescript file with all the routes (treeshakable)';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $contents = collect(Route::getRoutes()->getRoutesByName())->map(function ($route, $name) {
            $routeIngoreList = [
                'filament.',
                'debugbar.',
                'livewire.',
                'ignition.',
                'sanctum.csrf-cookie',
            ];

            if (Str::startsWith($name, $routeIngoreList)) {
                return '';
            }

            preg_match_all('/\{(.*?)\}/', $route->uri(), $args, PREG_SET_ORDER);

            $argstr = collect($args)
                ->map(fn ($a) => $this->mapReservedWord($a[1]) . ": string | number")
                ->add('query?: QueryParams')
                ->join(', ');

            $path = preg_replace_callback('/\{(.*?)\}/', function ($match) {
                $isOptional = strpos($match[1], '?') !== false;
                return '${' . str_replace('?', '', $this->mapReservedWord($match[1])) . ($isOptional ? " ?? ''" : '') . '}';
            }, $route->uri());

            if (substr($path, 0, 1) !== '/') {
                $path = '/'. $path;
            }

            $safeName = Str::snake(str_replace(['-', '.'], '_', $name));

            return "/** {$name} */
export function route_to_{$safeName}({$argstr}) { return withQuery(`{$path}`, query); }\n";
        })->filter()->join("\n");

$header = '
// _______  _______  __    _  _______  ______    _______  _______  _______  ______
// |       ||       ||  |  | ||       ||    _ |  |   _   ||       ||       ||      |
// |    ___||    ___||   |_| ||    ___||   | ||  |  |_|  ||_     _||    ___||  _    |
// |   | __ |   |___ |       ||   |___ |   |_||_ |       |  |   |  |   |___ | | |   |
// |   ||  ||    ___||  _    ||    ___||    __  ||       |  |   |  |    ___|| |_|   |
// |   |_| ||   |___ | | |   ||   |___ |   |  | ||   _   |  |   |  |   |___ |       |
// |_______||_______||_|  |__||_______||___|  |_||__| |__|  |___|  |_______||______|
//
// Generated with:
// php artisan app:generate-typescript-routes

export type QueryParams = string | string[][] | Record<string, string> | URLSearchParams | undefined;

function withQuery(path: string, query: QueryParams)  {
  const searchParams = new URLSearchParams(query);
  return `${path}${searchParams.size > 0 ? `?${searchParams.toString()}` : \'\'}`;
}
';
        File::put(resource_path('js/routes.ts'), $header . "\n" . $contents);
        $this->info(resource_path('js/routes.ts') . ' succesfully generated');
    }

    private function mapReservedWord($word) {
        $jsReservedWords = [
            'break',
            'case',
            'catch',
            'class',
            'const',
            'continue',
            'debugger',
            'default',
            'delete',
            'do',
            'else',
            'enum',
            'export',
            'extends',
            'false',
            'finally',
            'for',
            'function',
            'if',
            'import',
            'in',
            'instanceof',
            'let',
            'new',
            'null',
            'return',
            'super',
            'switch',
            'this',
            'throw',
            'true',
            'try',
            'typeof',
            'var',
            'void',
            'while',
            'with',
            'yield',
            'await',
            'implements',
            'interface',
            'package',
            'private',
            'protected',
            'public',
            'static',
        ];

        if (in_array($word, $jsReservedWords)) {
            return '_' . $word;
        }

        return $word;
    }
}

Example generated routes.ts

// _______  _______  __    _  _______  ______    _______  _______  _______  ______
// |       ||       ||  |  | ||       ||    _ |  |   _   ||       ||       ||      |
// |    ___||    ___||   |_| ||    ___||   | ||  |  |_|  ||_     _||    ___||  _    |
// |   | __ |   |___ |       ||   |___ |   |_||_ |       |  |   |  |   |___ | | |   |
// |   ||  ||    ___||  _    ||    ___||    __  ||       |  |   |  |    ___|| |_|   |
// |   |_| ||   |___ | | |   ||   |___ |   |  | ||   _   |  |   |  |   |___ |       |
// |_______||_______||_|  |__||_______||___|  |_||__| |__|  |___|  |_______||______|
//
// Generated with:
// php artisan app:generate-typescript-routes

export type QueryParams = string | string[][] | Record<string, string> | URLSearchParams | undefined;

function withQuery(path: string, query: QueryParams)  {
  const searchParams = new URLSearchParams(query);
  return `${path}${searchParams.size > 0 ? `?${searchParams.toString()}` : ''}`;
}

/** register */
export function route_to_register(query?: QueryParams) { return withQuery(`/register`, query); }

/** login */
export function route_to_login(query?: QueryParams) { return withQuery(`/login`, query); }

Usage

php artisan app:generate-typescript-routes

App.vue

<template>
    <a :href="route_to_login()">Login</a>
    <a :href="route_to_login({ backUrl: '/homepage' })">Login</a>
</template>
<script lang="ts" setup">
import { route_to_login } from './routes';
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment