diff --git a/.editorconfig b/.editorconfig index db9b162..0d9169b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,11 +3,8 @@ root = true [*] -indent_style = tab +indent_style = space tab_width = 2 end_of_line = lf insert_final_newline = true -[*.{yml,yaml}] -indent_style = space -indent_size = 2 diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 26ccf79..0000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -dist/ -build/ -coverage/ -.next/ diff --git a/.github/workflows/check-skills.yml b/.github/workflows/check-skills.yml new file mode 100644 index 0000000..b2f9812 --- /dev/null +++ b/.github/workflows/check-skills.yml @@ -0,0 +1,54 @@ +name: Check Stale Skills + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Set node + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: pnpm + + - name: Setup + run: npm i -g @antfu/ni + + - name: Install + run: nci + + - name: Check for stale skills + id: stale + run: | + cd packages/lib + npx @tanstack/intent@latest stale --json > stale-report.json || true + + - name: Create PR if skills are stale + if: steps.stale.outcome == 'failure' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: update stale skills' + title: 'Update stale skills' + body: | + This PR was created because the following skills may be out of date: + + Please review and update the skills using: + ``` + npx @tanstack/intent@latest scaffold + ``` + + Then run validation: + ``` + npx @tanstack/intent@latest validate + ``` + branch: update-stale-skills diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93e2e3c..5414791 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ on: - "**.mdx" jobs: - lint: + check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -43,60 +43,8 @@ jobs: - name: Build run: nr lib:build - - name: Lint - run: nr lint --cache - - format: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v4 - - - name: Set node - uses: actions/setup-node@v4 - with: - node-version: 24.x - cache: pnpm - - - name: Setup - run: npm i -g @antfu/ni - - - name: Install - run: nci - - - name: Build - run: nr lib:build - - - name: Format - run: nr format:check --cache - - type-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v4 - - - name: Set node - uses: actions/setup-node@v4 - with: - node-version: 24.x - cache: pnpm - - - name: Setup - run: npm i -g @antfu/ni - - - name: Install - run: nci - - - name: Build - run: nr lib:build - - - name: Type check - run: nr type-check + - name: Check + run: nr check e2e-test: runs-on: ubuntu-latest diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml new file mode 100644 index 0000000..09055ab --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -0,0 +1,38 @@ +name: Validate Skills + +on: + pull_request: + paths: + - 'packages/lib/skills/**' + - 'packages/lib/_artifacts/**' + push: + branches: [main] + paths: + - 'packages/lib/skills/**' + - 'packages/lib/_artifacts/**' + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Set node + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: pnpm + + - name: Setup + run: npm i -g @antfu/ni + + - name: Install + run: nci + + - name: Validate skills + run: | + cd packages/lib + npx @tanstack/intent@latest validate diff --git a/.markdownlint.json b/.markdownlint.json deleted file mode 100644 index 9bc533b..0000000 --- a/.markdownlint.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "default": true, - "no-blanks-blockquote": false, - "line-length": false, - "no-inline-html": false, - "first-line-h1": false -} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 66425c6..0000000 --- a/.prettierignore +++ /dev/null @@ -1,8 +0,0 @@ -.vscode -dist -.next -*.yaml -*.yml -*.css -*.md -*.mdx diff --git a/.prettierrc.mjs b/.prettierrc.mjs deleted file mode 100644 index 8f88989..0000000 --- a/.prettierrc.mjs +++ /dev/null @@ -1,11 +0,0 @@ -export default { - semi: false, - useTabs: true, - singleQuote: true, - jsxSingleQuote: false, - trailingComma: 'none', - tabWidth: 2, - arrowParens: 'avoid', - printWidth: 100, - plugins: ['prettier-plugin-tailwindcss'] -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3e032b1..4613b16 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"], + "recommendations": ["oxc.oxc-vscode", "typescriptteam.native-preview"], + "unwantedRecommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 977a1ca..468f65c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,25 +1,12 @@ { - "eslint.workingDirectories": [ - "./packages/lib", - "./demos/nextjs" - ], - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true, - "eslint.options": { - "extensions": [ - ".js", - ".ts", - ".tsx" - ] + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "always" }, - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "typescriptreact" - ], + "oxc.fmt.configPath": "oxfmt.config.ts", "editor.insertSpaces": false, - "editor.detectIndentation": false -} \ No newline at end of file + "editor.detectIndentation": false, + "typescript.experimental.useTsgo": true, + "js/ts.experimental.useTsgo": true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0730157 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,168 @@ +# Contributing to React Turnstile + +Thank you for your interest in contributing! This guide will help you get started with our development workflow. + +## Prerequisites + +- **Node.js 24+** (we recommend using [nvm](https://github.com/nvm-sh/nvm)) +- **pnpm** (package manager - will be auto-installed via corepack) + +## Quick Start + +```bash +# Clone the repository +git clone https://github.com/marsidev/react-turnstile.git +cd react-turnstile + +# Install dependencies +pnpm install + +# Start development server (builds library with HMR + starts Next.js demo) +pnpm dev +``` + +The dev server will start: + +- Library build with hot module replacement on port (via tsup) +- Next.js demo app on port 3333 + +Visit [http://localhost:3333](http://localhost:3333) to see the demo. + +## Development Workflow + +### TypeScript v7 (Go Implementation) + +We use **TypeScript v7** (currently in preview, written in Go) for type-checking and type-aware linting. This provides significant performance improvements over traditional TypeScript. + +```bash +# Build the library first (required for type-aware linting) +pnpm lib:build + +# Run type checking with tsgo +pnpm typecheck +``` + +**Important:** Type-aware linting requires built `.d.ts` files for cross-package type resolution in our monorepo setup. Always build before running type checks in CI. + +### Linting and Formatting + +We use **Oxlint** and **Oxfmt** for linting and formatting. + +```bash +# Fix all linting and formatting issues +pnpm fix + +# Check without modifying files (for CI) +pnpm check + +# Individual commands +pnpm lint # Fix linting issues +pnpm lint:check # Check linting only +pnpm format # Format files +pnpm format:check # Check formatting only +pnpm typecheck # Type check all packages +``` + +### Running Tests + +```bash +# Run all tests +pnpm test + +# Integration tests only +pnpm test:integration + +# E2E tests only +pnpm test:e2e +``` + +### Pre-commit Hooks + +We use `simple-git-hooks` to run checks before each commit. This ensures code quality before pushing. + +```bash +# The following runs automatically on git commit: +pnpm check # lint:check + format:check + typecheck +``` + +## Project Structure + +``` +. +├── packages/lib/ # Library source code (@marsidev/react-turnstile) +│ ├── src/ # Source code +│ ├── dist/ # Build output +│ └── package.json # Library package config +├── demos/nextjs/ # Next.js demo app +├── docs/ # Documentation +└── test/ # E2E tests (Playwright) +``` + +## Making Changes + +1. **Fork** the [repository](https://github.com/marsidev/react-turnstile/fork) and clone it +2. **Create a branch** from `main` for your changes +3. **Make your changes** with clear, focused commits +4. **Run checks** before committing: `pnpm check` +5. **Test your changes** with the demo app: `pnpm dev` +6. **Submit a PR** with a clear description + +## Resources + +- **[📖 Documentation](https://docs.page/marsidev/react-turnstile/)** +- **[🚀 Live Demo](https://react-turnstile.vercel.app/)** +- **[📦 NPM Package](https://npm.im/@marsidev/react-turnstile)** + +## Editor Setup + +### VS Code (or any editor based on VS Code) + +Install the recommended extensions (you'll be prompted): + +- **Oxc** (`oxc.oxc-vscode`) - Linting and formatting +- **TypeScript Native Preview** (`typescriptteam.native-preview`) - TypeScript v7 support + +These extensions will provide: + +- Auto-formatting on save +- Real-time linting +- Type errors +- Import sorting + +## Troubleshooting + +### Type-aware linting is slow + +Enable debug logging to see which files are causing issues: + +```bash +OXC_LOG=debug pnpm lint:check +``` + +Look for programs with unusually high file counts in the output. + +### Build fails with type errors + +Make sure you've built the library first: + +```bash +pnpm lib:build +pnpm typecheck +``` + +### Pre-commit hooks not running + +Reinstall hooks: + +```bash +pnpm simple-git-hooks +``` + +## Questions? + +- Open an [issue](https://github.com/marsidev/react-turnstile/issues/new) +- Check existing [discussions](https://github.com/marsidev/react-turnstile/discussions) + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/README.md b/README.md index 1d1a85e..3fd28c7 100644 --- a/README.md +++ b/README.md @@ -11,26 +11,23 @@ install size - - bundle size + + bundle size powered by bundlejs.com - - CI status - tested with playwright - PRs are welcome ## Features -* 💪 Smart verification with minimal user interaction -* 🕵️‍♀️ Privacy-focused approach -* 💉 Automatic script injection -* ⚡️ SSR ready -* 💻 TypeScript support +* 🕵️‍♀️ **Privacy-first** - No user tracking, no cookies, GDPR-friendly by design +* 📦 **Lightweight** - Only 6.3 KB minified +* 💪 **Smart verification** - Often invisible to users with minimal interaction +* 💉 **Zero config** - Automatic script injection, works out of the box +* 📐 **Multiple widget sizes** - normal, compact, flexible, invisible +* 🎮 **Imperative API** - Control via ref: `reset()`, `execute()`, `getResponse()`, `isExpired()` +* ⚡️ **SSR ready** - Works with Next.js, Remix, and any React SSR framework +* 💻 **Full TypeScript support** -## [Docs](https://docs.page/marsidev/react-turnstile/) | [Demo](https://react-turnstile.vercel.app/) +### **[📖 Read the docs →](https://docs.page/marsidev/react-turnstile/)**    **[🚀 See the demo →](https://react-turnstile.vercel.app/)** ## Getting started @@ -38,11 +35,13 @@ 2. Install `@marsidev/react-turnstile` into your React project. ```bash - npm i @marsidev/react-turnstile + pnpm add @marsidev/react-turnstile ``` ## Usage +### Basic + ```jsx import { Turnstile } from '@marsidev/react-turnstile' @@ -51,9 +50,36 @@ function Widget() { } ``` -> Checkout [the docs](https://docs.page/marsidev/react-turnstile) for more examples and for a detailed info about the `Turnstile` props. +### With form and imperative API -> Checkout [the demo](https://react-turnstile.vercel.app/) for a live example. +```tsx +import { Turnstile } from '@marsidev/react-turnstile' +import type { TurnstileInstance } from '@marsidev/react-turnstile' +import { useRef } from 'react' + +function LoginForm() { + const turnstileRef = useRef(null) + + async function handleSubmit(e: FormEvent) { + e.preventDefault() + const token = turnstileRef.current?.getResponse() + // Send token to your server... + turnstileRef.current?.reset() // Reset after submission + } + + return ( +
+ + + + + + ) +} +``` ## Contributing @@ -63,28 +89,28 @@ Any contributions are greatly appreciated. If you have a suggestion that would m * [Fork](https://github.com/marsidev/react-turnstile/fork) or clone this repository. * Install dependencies with `pnpm install`. -* You can use `pnpm dev` to start the demo page in dev mode, which also rebuild the library when file changes are detected in the `packages/lib` folder. +* Run `pnpm dev` to start both the library (with HMR) and the Next.js demo app concurrently -> The library is written under the `packages/lib` folder, the demo page is under the `demos/nextjs` folder and the docs are under the `docs` folder. +For reference, the project structure is as follows: -## Contributors - - - Contributors - +``` +. +├── packages/lib/ # Library source code +├── demos/nextjs/ # Next.js demo app +└── docs/ # Documentation +``` -## Credits +## Support -Inspired by +If you find this library useful, you can buy me a coffee: -* [nuxt-turnstile](https://github.com/danielroe/nuxt-turnstile) -* [svelte-turnstile](https://github.com/ghostdevv/svelte-turnstile) -* [react-google-recaptcha-v3](https://github.com/t49tran/react-google-recaptcha-v3) -* [reaptcha](https://github.com/sarneeh/reaptcha) +Buy Me a Coffee at ko-fi.com -## Support +## Contributors -If you like this project, please consider supporting it through a [PayPal donation](https://paypal.me/marsigliacr). :blush: + + Contributors + ## License diff --git a/demos/nextjs/.vscode/settings.json b/demos/nextjs/.vscode/settings.json deleted file mode 100644 index 82f4373..0000000 --- a/demos/nextjs/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "typescript.tsdk": "..\\..\\node_modules\\.pnpm\\typescript@4.9.5\\node_modules\\typescript\\lib", - "typescript.enablePromptUseWorkspaceTsdk": true -} \ No newline at end of file diff --git a/demos/nextjs/global.d.ts b/demos/nextjs/global.d.ts new file mode 100644 index 0000000..cbe652d --- /dev/null +++ b/demos/nextjs/global.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/demos/nextjs/next.config.js b/demos/nextjs/next.config.js index c4c0f65..ecb29da 100644 --- a/demos/nextjs/next.config.js +++ b/demos/nextjs/next.config.js @@ -1,19 +1,19 @@ -const path = require('node:path') +const path = require("node:path"); /** @type {import('next').NextConfig} */ const nextConfig = { - turbopack: { - root: path.join(__dirname, '../..') - }, - async redirects() { - return [ - { - source: '/', - destination: '/basic', - permanent: true - } - ] - } -} + turbopack: { + root: path.join(__dirname, "../..") + }, + async redirects() { + return [ + { + source: "/", + destination: "/basic", + permanent: true + } + ]; + } +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/demos/nextjs/package.json b/demos/nextjs/package.json index 6d7ed8b..7667ecb 100644 --- a/demos/nextjs/package.json +++ b/demos/nextjs/package.json @@ -1,31 +1,31 @@ { - "name": "nextjs-demo", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "next dev --port 3333", - "build": "pnpm run --filter=@marsidev/react-turnstile build && next build", - "start": "next start --port 3333", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@marsidev/react-turnstile": "workspace:*", - "@radix-ui/react-select": "2.2.6", - "class-variance-authority": "0.7.1", - "classnames": "2.5.1", - "jotai": "2.17.0", - "lucide-react": "0.563.0", - "next": "16.1.6", - "next-themes": "0.4.6", - "react": "19.2.4", - "react-dom": "19.2.4", - "tailwind-merge": "3.4.0" - }, - "devDependencies": { - "@tailwindcss/postcss": "4.1.18", - "@tailwindcss/typography": "0.5.19", - "autoprefixer": "10.4.24", - "postcss": "8.5.6", - "tailwindcss": "4.1.18" - } + "name": "nextjs-demo", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev --port 3333", + "build": "pnpm run --filter=@marsidev/react-turnstile build && next build", + "start": "next start --port 3333", + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@marsidev/react-turnstile": "workspace:*", + "@radix-ui/react-select": "2.2.6", + "class-variance-authority": "0.7.1", + "classnames": "2.5.1", + "jotai": "2.17.0", + "lucide-react": "0.563.0", + "next": "16.1.6", + "next-themes": "0.4.6", + "react": "19.2.4", + "react-dom": "19.2.4", + "tailwind-merge": "3.4.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "4.1.18", + "@tailwindcss/typography": "0.5.19", + "autoprefixer": "10.4.24", + "postcss": "8.5.6", + "tailwindcss": "4.1.18" + } } diff --git a/demos/nextjs/postcss.config.js b/demos/nextjs/postcss.config.js index 0b295e4..05856bf 100644 --- a/demos/nextjs/postcss.config.js +++ b/demos/nextjs/postcss.config.js @@ -1,5 +1,5 @@ module.exports = { - plugins: { - '@tailwindcss/postcss': {} - } -} + plugins: { + "@tailwindcss/postcss": {} + } +}; diff --git a/demos/nextjs/src/app/api/verify/route.ts b/demos/nextjs/src/app/api/verify/route.ts index 6f67ae6..847b9e7 100644 --- a/demos/nextjs/src/app/api/verify/route.ts +++ b/demos/nextjs/src/app/api/verify/route.ts @@ -1,32 +1,32 @@ -import type { TurnstileServerValidationResponse } from '@marsidev/react-turnstile' +import type { TurnstileServerValidationResponse } from "@marsidev/react-turnstile"; -const verifyEndpoint = 'https://challenges.cloudflare.com/turnstile/v0/siteverify' +const verifyEndpoint = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; const responseHeaders = { - 'content-type': 'application/json' -} + "content-type": "application/json" +}; export async function POST(request: Request) { - const body = (await request.json()) as { token: string; secret: string } - const { token, secret } = body + const body = (await request.json()) as { token: string; secret: string }; + const { token, secret } = body; - const data = (await fetch(verifyEndpoint, { - method: 'POST', - body: `secret=${encodeURIComponent(secret)}&response=${encodeURIComponent(token)}`, - headers: { - 'content-type': 'application/x-www-form-urlencoded' - } - }).then(res => res.json())) as TurnstileServerValidationResponse + const data = (await fetch(verifyEndpoint, { + method: "POST", + body: `secret=${encodeURIComponent(secret)}&response=${encodeURIComponent(token)}`, + headers: { + "content-type": "application/x-www-form-urlencoded" + } + }).then(res => res.json())) as TurnstileServerValidationResponse; - if (!data.success) { - return new Response(JSON.stringify(data), { - status: 400, - headers: responseHeaders - }) - } + if (!data.success) { + return new Response(JSON.stringify(data), { + status: 400, + headers: responseHeaders + }); + } - return new Response(JSON.stringify(data), { - status: 200, - headers: responseHeaders - }) + return new Response(JSON.stringify(data), { + status: 200, + headers: responseHeaders + }); } diff --git a/demos/nextjs/src/app/basic/page.tsx b/demos/nextjs/src/app/basic/page.tsx index 544f89d..728faa7 100644 --- a/demos/nextjs/src/app/basic/page.tsx +++ b/demos/nextjs/src/app/basic/page.tsx @@ -1,13 +1,13 @@ -'use client' +"use client"; -import React from 'react' -import DemoWidget from '~/components/demo-widget' +import React from "react"; +import DemoWidget from "~/components/demo-widget"; export default function Page() { - return ( - -

Basic demo

- -
- ) + return ( + +

Basic demo

+ +
+ ); } diff --git a/demos/nextjs/src/app/layout.tsx b/demos/nextjs/src/app/layout.tsx index 2cdde53..c90dd0c 100644 --- a/demos/nextjs/src/app/layout.tsx +++ b/demos/nextjs/src/app/layout.tsx @@ -1,31 +1,31 @@ -import '~/app/globals.css' -import { Inter } from 'next/font/google' -import Header from '~/components/header' -import Sidebar from '~/components/sidebar' -import Content from '~/components/content' -import ThemeProvider from '~/components/theme-provider' -import { cn } from '~/utils' +import "~/app/globals.css"; +import { Inter } from "next/font/google"; +import Header from "~/components/header"; +import Sidebar from "~/components/sidebar"; +import Content from "~/components/content"; +import ThemeProvider from "~/components/theme-provider"; +import { cn } from "~/utils"; -const inter = Inter({ subsets: ['latin'] }) +const inter = Inter({ subsets: ["latin"] }); export const metadata = { - title: 'React Turnstile Demo', - description: 'React Turnstile Next.js 16 App Router Demo' -} + title: "React Turnstile Demo", + description: "React Turnstile Next.js 16 App Router Demo" +}; export default function RootLayout({ children }: React.PropsWithChildren) { - return ( - - - -
-
- + return ( + + + +
+
+ - {children} -
- - - - ) + {children} +
+ + + + ); } diff --git a/demos/nextjs/src/app/manual-script-injection-with-custom-script-props/page.tsx b/demos/nextjs/src/app/manual-script-injection-with-custom-script-props/page.tsx index af8ae24..95a458e 100644 --- a/demos/nextjs/src/app/manual-script-injection-with-custom-script-props/page.tsx +++ b/demos/nextjs/src/app/manual-script-injection-with-custom-script-props/page.tsx @@ -1,24 +1,24 @@ -'use client' +"use client"; -import Script from 'next/script' -import { SCRIPT_URL } from '@marsidev/react-turnstile' -import React from 'react' -import DemoWidget from '~/components/demo-widget' +import Script from "next/script"; +import { SCRIPT_URL } from "@marsidev/react-turnstile"; +import React from "react"; +import DemoWidget from "~/components/demo-widget"; export default function Page() { - return ( - - {/* We add a custom query param to the script URL to force a re-download of the script, since the manual script injection is also used in other demos. This is not needed if the script ID is the same. */} -