diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 000000000..1cb5de72d --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,68 @@ +name: fix code styling + +on: [push] + +jobs: + pint: + uses: laravel/.github/.github/workflows/coding-standards.yml@main + with: + message: "Pint: fix code styling" + + eslint: + name: Lint stubs for Inertia stacks + runs-on: ubuntu-latest + needs: pint + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # We need to pull the latest changes because the `pint` job might have pushed changes + - name: Pull Remote Changes + run: git pull + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install NPM packages + run: | + # Common packages + npm install \ + eslint@^8.57.0 \ + prettier@^3.3.0 \ + prettier-plugin-organize-imports@^4.0.0 \ + prettier-plugin-tailwindcss@^0.6.5 + + # React + npm install \ + react@^18.2.0 \ + eslint-plugin-react@^7.34.4 \ + eslint-plugin-react-hooks@^4.6.2 \ + eslint-plugin-prettier@^5.1.3 \ + eslint-config-prettier@^9.1.0 \ + @typescript-eslint/eslint-plugin@^7.16.0 \ + @typescript-eslint/parser@^7.16.0 + + # Vue + npm install \ + eslint-plugin-vue@^9.23.0 \ + @rushstack/eslint-patch@^1.8.0 \ + @vue/eslint-config-prettier@^9.0.0 \ + @vue/eslint-config-typescript@^13.0.0 + + - name: Run ESLint + run: | + cp stubs/inertia-common/.prettierrc . + npx eslint --config stubs/inertia-react/.eslintrc.json stubs/inertia-react/resources/js --ext .js,.jsx --fix + npx eslint --config stubs/inertia-react-ts/.eslintrc.json stubs/inertia-react-ts/resources/js --ext .js,.jsx,.ts,.tsx --fix + npx eslint --config stubs/inertia-vue/.eslintrc.cjs stubs/inertia-vue/resources/js --ext .js,.vue --fix + npx eslint --config stubs/inertia-vue-ts/.eslintrc.cjs stubs/inertia-vue-ts/resources/js --ext .js,.ts,.vue --fix + + - name: Clean up + run: rm -rf node_modules package.json package-lock.json .prettierrc + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "ESLint: fix code styling" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0cb5994ff..b5a62a4bf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ on: jobs: stub-tests: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: fail-fast: true diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index e71ec8969..000000000 --- a/.styleci.yml +++ /dev/null @@ -1,8 +0,0 @@ -php: - preset: laravel -js: - finder: - not-name: - - vite.config.js -css: true -vue: true diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 98712258a..babeb1604 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -34,6 +34,7 @@ class InstallCommand extends Command implements PromptsForMissingInput {--pest : Indicate that Pest should be installed} {--ssr : Indicates if Inertia SSR support should be installed} {--typescript : Indicates if TypeScript is preferred for the Inertia stack} + {--eslint : Indicates if ESLint with Prettier should be installed} {--composer=global : Absolute path to the Composer binary which should be used to install packages}'; /** @@ -181,7 +182,6 @@ protected function hasComposerPackage($package) /** * Installs the given Composer Packages into the application. * - * @param array $packages * @param bool $asDev * @return bool */ @@ -209,7 +209,6 @@ protected function requireComposerPackages(array $packages, $asDev = false) /** * Removes the given Composer Packages from the application. * - * @param array $packages * @param bool $asDev * @return bool */ @@ -235,9 +234,8 @@ protected function removeComposerPackages(array $packages, $asDev = false) } /** - * Update the "package.json" file. + * Update the dependencies in the "package.json" file. * - * @param callable $callback * @param bool $dev * @return void */ @@ -264,6 +262,29 @@ protected static function updateNodePackages(callable $callback, $dev = true) ); } + /** + * Update the scripts in the "package.json" file. + * + * @return void + */ + protected static function updateNodeScripts(callable $callback) + { + if (! file_exists(base_path('package.json'))) { + return; + } + + $content = json_decode(file_get_contents(base_path('package.json')), true); + + $content['scripts'] = $callback( + array_key_exists('scripts', $content) ? $content['scripts'] : [] + ); + + file_put_contents( + base_path('package.json'), + json_encode($content, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT).PHP_EOL + ); + } + /** * Delete the "node_modules" directory and remove the associated lock files. * @@ -301,7 +322,7 @@ protected function replaceInFile($search, $replace, $path) */ protected function phpBinary() { - return (new PhpExecutableFinder())->find(false) ?: 'php'; + return (new PhpExecutableFinder)->find(false) ?: 'php'; } /** @@ -330,7 +351,6 @@ protected function runCommands($commands) /** * Remove Tailwind dark classes from the given files. * - * @param \Symfony\Component\Finder\Finder $finder * @return void */ protected function removeDarkClasses(Finder $finder) @@ -366,8 +386,6 @@ protected function promptForMissingArgumentsUsing() /** * Interact further with the user if they were prompted for missing arguments. * - * @param \Symfony\Component\Console\Input\InputInterface $input - * @param \Symfony\Component\Console\Output\OutputInterface $output * @return void */ protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) @@ -381,6 +399,7 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp 'dark' => 'Dark mode', 'ssr' => 'Inertia SSR', 'typescript' => 'TypeScript', + 'eslint' => 'ESLint with Prettier', ] ))->each(fn ($option) => $input->setOption($option, true)); } elseif (in_array($stack, ['blade', 'livewire', 'livewire-functional'])) { diff --git a/src/Console/InstallsInertiaStacks.php b/src/Console/InstallsInertiaStacks.php index 9e2291e60..3ba5268c1 100644 --- a/src/Console/InstallsInertiaStacks.php +++ b/src/Console/InstallsInertiaStacks.php @@ -41,6 +41,46 @@ protected function installInertiaVueStack() }); } + if ($this->option('eslint')) { + $this->updateNodePackages(function ($packages) { + return [ + 'eslint' => '^8.57.0', + 'eslint-plugin-vue' => '^9.23.0', + '@rushstack/eslint-patch' => '^1.8.0', + '@vue/eslint-config-prettier' => '^9.0.0', + 'prettier' => '^3.3.0', + 'prettier-plugin-organize-imports' => '^4.0.0', + 'prettier-plugin-tailwindcss' => '^0.6.5', + ] + $packages; + }); + + if ($this->option('typescript')) { + $this->updateNodePackages(function ($packages) { + return [ + '@vue/eslint-config-typescript' => '^13.0.0', + ] + $packages; + }); + + $this->updateNodeScripts(function ($scripts) { + return $scripts + [ + 'lint' => 'eslint resources/js --ext .js,.ts,.vue --ignore-path .gitignore --fix', + ]; + }); + + copy(__DIR__.'/../../stubs/inertia-vue-ts/.eslintrc.cjs', base_path('.eslintrc.cjs')); + } else { + $this->updateNodeScripts(function ($scripts) { + return $scripts + [ + 'lint' => 'eslint resources/js --ext .js,.vue --ignore-path .gitignore --fix', + ]; + }); + + copy(__DIR__.'/../../stubs/inertia-vue/.eslintrc.cjs', base_path('.eslintrc.cjs')); + } + + copy(__DIR__.'/../../stubs/inertia-common/.prettierrc', base_path('.prettierrc')); + } + // Providers... (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-common/app/Providers', app_path('Providers')); @@ -216,6 +256,48 @@ protected function installInertiaReactStack() }); } + if ($this->option('eslint')) { + $this->updateNodePackages(function ($packages) { + return [ + 'eslint' => '^8.57.0', + 'eslint-plugin-react' => '^7.34.4', + 'eslint-plugin-react-hooks' => '^4.6.2', + 'eslint-plugin-prettier' => '^5.1.3', + 'eslint-config-prettier' => '^9.1.0', + 'prettier' => '^3.3.0', + 'prettier-plugin-organize-imports' => '^4.0.0', + 'prettier-plugin-tailwindcss' => '^0.6.5', + ] + $packages; + }); + + if ($this->option('typescript')) { + $this->updateNodePackages(function ($packages) { + return [ + '@typescript-eslint/eslint-plugin' => '^7.16.0', + '@typescript-eslint/parser' => '^7.16.0', + ] + $packages; + }); + + $this->updateNodeScripts(function ($scripts) { + return $scripts + [ + 'lint' => 'eslint resources/js --ext .js,.jsx,.ts,.tsx --ignore-path .gitignore --fix', + ]; + }); + + copy(__DIR__.'/../../stubs/inertia-react-ts/.eslintrc.json', base_path('.eslintrc.json')); + } else { + $this->updateNodeScripts(function ($scripts) { + return $scripts + [ + 'lint' => 'eslint resources/js --ext .js,.jsx --ignore-path .gitignore --fix', + ]; + }); + + copy(__DIR__.'/../../stubs/inertia-react/.eslintrc.json', base_path('.eslintrc.json')); + } + + copy(__DIR__.'/../../stubs/inertia-common/.prettierrc', base_path('.prettierrc')); + } + // Providers... (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia-common/app/Providers', app_path('Providers')); diff --git a/stubs/api/routes/auth.php b/stubs/api/routes/auth.php index ae6cc20dc..9cf5ffd70 100644 --- a/stubs/api/routes/auth.php +++ b/stubs/api/routes/auth.php @@ -9,29 +9,29 @@ use Illuminate\Support\Facades\Route; Route::post('/register', [RegisteredUserController::class, 'store']) - ->middleware('guest') - ->name('register'); + ->middleware('guest') + ->name('register'); Route::post('/login', [AuthenticatedSessionController::class, 'store']) - ->middleware('guest') - ->name('login'); + ->middleware('guest') + ->name('login'); Route::post('/forgot-password', [PasswordResetLinkController::class, 'store']) - ->middleware('guest') - ->name('password.email'); + ->middleware('guest') + ->name('password.email'); Route::post('/reset-password', [NewPasswordController::class, 'store']) - ->middleware('guest') - ->name('password.store'); + ->middleware('guest') + ->name('password.store'); Route::get('/verify-email/{id}/{hash}', VerifyEmailController::class) - ->middleware(['auth', 'signed', 'throttle:6,1']) - ->name('verification.verify'); + ->middleware(['auth', 'signed', 'throttle:6,1']) + ->name('verification.verify'); Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store']) - ->middleware(['auth', 'throttle:6,1']) - ->name('verification.send'); + ->middleware(['auth', 'throttle:6,1']) + ->name('verification.send'); Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']) - ->middleware('auth') - ->name('logout'); + ->middleware('auth') + ->name('logout'); diff --git a/stubs/default/app/Http/Controllers/Auth/NewPasswordController.php b/stubs/default/app/Http/Controllers/Auth/NewPasswordController.php index f1e2814fa..79ac3cd65 100644 --- a/stubs/default/app/Http/Controllers/Auth/NewPasswordController.php +++ b/stubs/default/app/Http/Controllers/Auth/NewPasswordController.php @@ -56,6 +56,6 @@ function ($user) use ($request) { return $status == Password::PASSWORD_RESET ? redirect()->route('login')->with('status', __($status)) : back()->withInput($request->only('email')) - ->withErrors(['email' => __($status)]); + ->withErrors(['email' => __($status)]); } } diff --git a/stubs/default/app/Http/Controllers/Auth/PasswordResetLinkController.php b/stubs/default/app/Http/Controllers/Auth/PasswordResetLinkController.php index ce813a62e..bf1ebfa78 100644 --- a/stubs/default/app/Http/Controllers/Auth/PasswordResetLinkController.php +++ b/stubs/default/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -39,6 +39,6 @@ public function store(Request $request): RedirectResponse return $status == Password::RESET_LINK_SENT ? back()->with('status', __($status)) : back()->withInput($request->only('email')) - ->withErrors(['email' => __($status)]); + ->withErrors(['email' => __($status)]); } } diff --git a/stubs/default/routes/auth.php b/stubs/default/routes/auth.php index 1040b5174..3926ecf72 100644 --- a/stubs/default/routes/auth.php +++ b/stubs/default/routes/auth.php @@ -13,47 +13,47 @@ Route::middleware('guest')->group(function () { Route::get('register', [RegisteredUserController::class, 'create']) - ->name('register'); + ->name('register'); Route::post('register', [RegisteredUserController::class, 'store']); Route::get('login', [AuthenticatedSessionController::class, 'create']) - ->name('login'); + ->name('login'); Route::post('login', [AuthenticatedSessionController::class, 'store']); Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) - ->name('password.request'); + ->name('password.request'); Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) - ->name('password.email'); + ->name('password.email'); Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) - ->name('password.reset'); + ->name('password.reset'); Route::post('reset-password', [NewPasswordController::class, 'store']) - ->name('password.store'); + ->name('password.store'); }); Route::middleware('auth')->group(function () { Route::get('verify-email', EmailVerificationPromptController::class) - ->name('verification.notice'); + ->name('verification.notice'); Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) - ->middleware(['signed', 'throttle:6,1']) - ->name('verification.verify'); + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) - ->middleware('throttle:6,1') - ->name('verification.send'); + ->middleware('throttle:6,1') + ->name('verification.send'); Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) - ->name('password.confirm'); + ->name('password.confirm'); Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); Route::put('password', [PasswordController::class, 'update'])->name('password.update'); Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) - ->name('logout'); + ->name('logout'); }); diff --git a/stubs/inertia-common/.prettierrc b/stubs/inertia-common/.prettierrc new file mode 100644 index 000000000..f03e24a5b --- /dev/null +++ b/stubs/inertia-common/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "plugins": [ + "prettier-plugin-organize-imports", + "prettier-plugin-tailwindcss" + ] +} diff --git a/stubs/inertia-common/app/Http/Middleware/HandleInertiaRequests.php b/stubs/inertia-common/app/Http/Middleware/HandleInertiaRequests.php index c73606599..3867f225c 100644 --- a/stubs/inertia-common/app/Http/Middleware/HandleInertiaRequests.php +++ b/stubs/inertia-common/app/Http/Middleware/HandleInertiaRequests.php @@ -17,7 +17,7 @@ class HandleInertiaRequests extends Middleware /** * Determine the current asset version. */ - public function version(Request $request): string|null + public function version(Request $request): ?string { return parent::version($request); } diff --git a/stubs/inertia-common/routes/auth.php b/stubs/inertia-common/routes/auth.php index 1040b5174..3926ecf72 100644 --- a/stubs/inertia-common/routes/auth.php +++ b/stubs/inertia-common/routes/auth.php @@ -13,47 +13,47 @@ Route::middleware('guest')->group(function () { Route::get('register', [RegisteredUserController::class, 'create']) - ->name('register'); + ->name('register'); Route::post('register', [RegisteredUserController::class, 'store']); Route::get('login', [AuthenticatedSessionController::class, 'create']) - ->name('login'); + ->name('login'); Route::post('login', [AuthenticatedSessionController::class, 'store']); Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) - ->name('password.request'); + ->name('password.request'); Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) - ->name('password.email'); + ->name('password.email'); Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) - ->name('password.reset'); + ->name('password.reset'); Route::post('reset-password', [NewPasswordController::class, 'store']) - ->name('password.store'); + ->name('password.store'); }); Route::middleware('auth')->group(function () { Route::get('verify-email', EmailVerificationPromptController::class) - ->name('verification.notice'); + ->name('verification.notice'); Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) - ->middleware(['signed', 'throttle:6,1']) - ->name('verification.verify'); + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) - ->middleware('throttle:6,1') - ->name('verification.send'); + ->middleware('throttle:6,1') + ->name('verification.send'); Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) - ->name('password.confirm'); + ->name('password.confirm'); Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); Route::put('password', [PasswordController::class, 'update'])->name('password.update'); Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) - ->name('logout'); + ->name('logout'); }); diff --git a/stubs/inertia-react-ts/.eslintrc.json b/stubs/inertia-react-ts/.eslintrc.json new file mode 100644 index 000000000..fbeee9276 --- /dev/null +++ b/stubs/inertia-react-ts/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "env": { + "browser": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "react/no-unescaped-entities": "off" + }, + "settings": { + "react": { + "version": "detect" + } + } +} diff --git a/stubs/inertia-react-ts/resources/js/Components/ApplicationLogo.tsx b/stubs/inertia-react-ts/resources/js/Components/ApplicationLogo.tsx index 9180a3e2e..ccd928517 100644 --- a/stubs/inertia-react-ts/resources/js/Components/ApplicationLogo.tsx +++ b/stubs/inertia-react-ts/resources/js/Components/ApplicationLogo.tsx @@ -2,7 +2,11 @@ import { SVGAttributes } from 'react'; export default function ApplicationLogo(props: SVGAttributes) { return ( - + ); diff --git a/stubs/inertia-react-ts/resources/js/Components/Checkbox.tsx b/stubs/inertia-react-ts/resources/js/Components/Checkbox.tsx index c494a2bf6..ed2284d9d 100644 --- a/stubs/inertia-react-ts/resources/js/Components/Checkbox.tsx +++ b/stubs/inertia-react-ts/resources/js/Components/Checkbox.tsx @@ -1,12 +1,15 @@ import { InputHTMLAttributes } from 'react'; -export default function Checkbox({ className = '', ...props }: InputHTMLAttributes) { +export default function Checkbox({ + className = '', + ...props +}: InputHTMLAttributes) { return ( diff --git a/stubs/inertia-react-ts/resources/js/Components/DangerButton.tsx b/stubs/inertia-react-ts/resources/js/Components/DangerButton.tsx index 8fd81d187..a76480e2d 100644 --- a/stubs/inertia-react-ts/resources/js/Components/DangerButton.tsx +++ b/stubs/inertia-react-ts/resources/js/Components/DangerButton.tsx @@ -1,11 +1,16 @@ import { ButtonHTMLAttributes } from 'react'; -export default function DangerButton({ className = '', disabled, children, ...props }: ButtonHTMLAttributes) { +export default function DangerButton({ + className = '', + disabled, + children, + ...props +}: ButtonHTMLAttributes) { return (
{children}
- {open &&
setOpen(false)}>
} + {open && ( +
setOpen(false)} + >
+ )} ); }; -const Content = ({ align = 'right', width = '48', contentClasses = 'py-1 bg-white dark:bg-gray-700', children }: PropsWithChildren<{ align?: 'left'|'right', width?: '48', contentClasses?: string }>) => { +const Content = ({ + align = 'right', + width = '48', + contentClasses = 'py-1 bg-white dark:bg-gray-700', + children, +}: PropsWithChildren<{ + align?: 'left' | 'right'; + width?: '48'; + contentClasses?: string; +}>) => { const { open, setOpen } = useContext(DropDownContext); let alignmentClasses = 'origin-top'; @@ -70,19 +91,30 @@ const Content = ({ align = 'right', width = '48', contentClasses = 'py-1 bg-whit className={`absolute z-50 mt-2 rounded-md shadow-lg ${alignmentClasses} ${widthClasses}`} onClick={() => setOpen(false)} > -
{children}
+
+ {children} +
); }; -const DropdownLink = ({ className = '', children, ...props }: InertiaLinkProps) => { +const DropdownLink = ({ + className = '', + children, + ...props +}: InertiaLinkProps) => { return ( diff --git a/stubs/inertia-react-ts/resources/js/Components/InputError.tsx b/stubs/inertia-react-ts/resources/js/Components/InputError.tsx index 68c210056..842dab88f 100644 --- a/stubs/inertia-react-ts/resources/js/Components/InputError.tsx +++ b/stubs/inertia-react-ts/resources/js/Components/InputError.tsx @@ -1,8 +1,15 @@ import { HTMLAttributes } from 'react'; -export default function InputError({ message, className = '', ...props }: HTMLAttributes & { message?: string }) { +export default function InputError({ + message, + className = '', + ...props +}: HTMLAttributes & { message?: string }) { return message ? ( -

+

{message}

) : null; diff --git a/stubs/inertia-react-ts/resources/js/Components/InputLabel.tsx b/stubs/inertia-react-ts/resources/js/Components/InputLabel.tsx index b0ea407de..c51c0161b 100644 --- a/stubs/inertia-react-ts/resources/js/Components/InputLabel.tsx +++ b/stubs/inertia-react-ts/resources/js/Components/InputLabel.tsx @@ -1,8 +1,19 @@ import { LabelHTMLAttributes } from 'react'; -export default function InputLabel({ value, className = '', children, ...props }: LabelHTMLAttributes & { value?: string }) { +export default function InputLabel({ + value, + className = '', + children, + ...props +}: LabelHTMLAttributes & { value?: string }) { return ( -