├── .changeset ├── README.md └── config.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── publish-commit.yaml │ ├── publish.yaml │ └── pull-request.yaml ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.MD ├── biome.json ├── lefthook.yml ├── package.json ├── packages └── react-directives-plugin │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── babel.ts │ ├── index.ts │ ├── transformer.test.ts │ └── transformer.ts │ ├── tests │ └── setup.ts │ ├── tsconfig.json │ ├── tsdown.config.ts │ └── vitest.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── index.test.ts └── index.ts └── test-apps └── react-router-esm ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── app ├── entry.client.tsx ├── entry.server.tsx ├── root.tsx ├── routes.ts ├── routes │ └── _index.tsx ├── transform.client.ts ├── transform.server.ts └── transform.strict.ts ├── package.json ├── public └── favicon.ico ├── tsconfig.json └── vite.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": true, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Create a bug report to help us improve 3 | title: "bug: " 4 | labels: ["🐞 unconfirmed bug"] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Provide environment information 9 | description: | 10 | Run this command in your project root and paste the results: 11 | ```bash 12 | npx envinfo --system --binaries 13 | ``` 14 | 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Describe the bug 20 | description: A clear and concise description of the bug, as well as what you expected to happen when encountering it. 21 | validations: 22 | required: true 23 | - type: input 24 | attributes: 25 | label: Reproduction repo 26 | description: If applicable, please provide a link to a reproduction repo or a Stackblitz / CodeSandbox project. Your issue may be closed if this is not provided and we are unable to reproduce the issue. 27 | - type: textarea 28 | attributes: 29 | label: Expected behavior 30 | description: A clear and concise description of what you expected to happen. 31 | validations: 32 | required: true 33 | - type: textarea 34 | attributes: 35 | label: Actual behavior 36 | description: A clear and concise description of what actually happened. 37 | validations: 38 | required: true 39 | - type: textarea 40 | attributes: 41 | label: Steps to reproduce 42 | description: Steps to reproduce the behavior. 43 | placeholder: | 44 | 1. Go to '...' 45 | 2. Click on '....' 46 | 3. Scroll down to '....' 47 | 4. See error 48 | validations: 49 | required: true 50 | - type: textarea 51 | attributes: 52 | label: Screenshots 53 | description: If applicable, add screenshots to help explain your problem. 54 | - type: textarea 55 | attributes: 56 | label: Additional context 57 | description: Add any other context about the problem here. 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a Question 4 | url: https://discord.gg/uZ39dxhj 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | title: "feat: " 4 | labels: ["🌟 enhancement"] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Is your feature request related to a problem? Please describe. 9 | description: A clear and concise description of what the problem is. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Describe the solution you'd like to see 15 | description: A clear and concise description of what you want to happen. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Describe alternate solutions 21 | description: A clear and concise description of any alternative solutions or features you've considered. 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Additional information 27 | description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here. 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | # Description 4 | 5 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 6 | List any dependencies that are required for this change. 7 | 8 | ## Type of change 9 | 10 | Please mark relevant options with an `x` in the brackets. 11 | 12 | - [ ] Bug fix (non-breaking change which fixes an issue) 13 | - [ ] New feature (non-breaking change which adds functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] This change requires a documentation update 16 | - [ ] Algorithm update - updates algorithm documentation/questions/answers etc. 17 | - [ ] Other (please describe): 18 | 19 | # How Has This Been Tested? 20 | 21 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also 22 | list any relevant details for your test configuration 23 | 24 | - [ ] Integration tests 25 | - [ ] Unit tests 26 | - [ ] Manual tests 27 | - [ ] No tests required 28 | 29 | # Reviewer checklist 30 | 31 | Mark everything that needs to be checked before merging the PR. 32 | 33 | - [ ] Check if the UI is working as expected and is satisfactory 34 | - [ ] Check if the code is well documented 35 | - [ ] Check if the behavior is what is expected 36 | - [ ] Check if the code is well tested 37 | - [ ] Check if the code is readable and well formatted 38 | - [ ] Additional checks (document below if any) 39 | 40 | # Screenshots (if appropriate): 41 | 42 | # Questions (if appropriate): 43 | -------------------------------------------------------------------------------- /.github/workflows/publish-commit.yaml: -------------------------------------------------------------------------------- 1 | name: 🚀 pkg-pr-new 2 | on: [push, pull_request] 3 | 4 | concurrency: 5 | group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Install pnpm 16 | uses: pnpm/action-setup@v4 17 | 18 | - run: corepack enable 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version-file: "package.json" 22 | 23 | - name: Install dependencies 24 | run: pnpm install 25 | 26 | - name: Build 27 | run: pnpm run build 28 | 29 | - run: npx pkg-pr-new publish ./packages/* 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | actions: write 16 | id-token: write 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@v3 20 | 21 | - name: Install pnpm 22 | uses: pnpm/action-setup@v4 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version-file: "package.json" 28 | 29 | - name: Install Dependencies 30 | run: pnpm install 31 | 32 | # - name: 🔐 Setup npm auth 33 | # run: | 34 | # echo "registry=https://registry.npmjs.org" >> ~/.npmrc 35 | # echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc 36 | 37 | - name: Create Release Pull Request or Publish to npm 38 | id: changesets 39 | uses: changesets/action@v1 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | with: 44 | title: "🚀 Release PR" 45 | commit: "chore: release" 46 | version: pnpm run version 47 | publish: pnpm run release 48 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: 🚀 PR 2 | 3 | concurrency: 4 | group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: [pull_request] # Run only on pull_request, to also get status updates in PRs. We omit push because this would run the steps two times (for push and pull_request). 8 | 9 | permissions: 10 | actions: write 11 | contents: read 12 | # Required to put a comment into the pull-request 13 | pull-requests: write 14 | 15 | jobs: 16 | lint: 17 | name: ⬣ Linting 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: biomejs/setup-biome@v2 22 | - run: biome ci . --reporter=github 23 | 24 | typecheck: 25 | name: 🔎 Type check 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: 🛑 Cancel Previous Runs 29 | uses: styfle/cancel-workflow-action@0.12.1 30 | 31 | - name: ⬇️ Checkout repo 32 | uses: actions/checkout@v4 33 | - name: Install pnpm 34 | uses: pnpm/action-setup@v4 35 | - name: ⎔ Setup node 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version-file: "package.json" 39 | 40 | - name: 📥 Download deps 41 | run: pnpm install 42 | 43 | - name: 🔎 Type check 44 | run: pnpm run typecheck 45 | 46 | vitest: 47 | name: ⚡ Unit Tests 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: 🛑 Cancel Previous Runs 51 | uses: styfle/cancel-workflow-action@0.12.1 52 | 53 | - name: ⬇️ Checkout repo 54 | uses: actions/checkout@v4 55 | 56 | - name: Install pnpm 57 | uses: pnpm/action-setup@v4 58 | 59 | - name: ⎔ Setup node 60 | uses: actions/setup-node@v4 61 | with: 62 | node-version-file: "package.json" 63 | 64 | - name: 📥 Download deps 65 | run: pnpm install 66 | 67 | - name: ⚡ Run vitest 68 | run: pnpm run test:cov 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | /dist 132 | .history 133 | .react-router -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "biome.enabled": true, 3 | "editor.defaultFormatter": "biomejs.biome", 4 | "editor.formatOnSave": true, 5 | "javascript.format.enable": false, 6 | "javascript.suggest.autoImports": true, 7 | "javascript.suggest.paths": true, 8 | "typescript.format.enable": false, 9 | "typescript.suggest.paths": true, 10 | "typescript.suggest.autoImports": true, 11 | "editor.renderWhitespace": "all", 12 | "editor.rulers": [120, 160], 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll": "always", 15 | "source.organizeImports": "never", 16 | "source.organizeImports.biome": "always", 17 | "quickfix.biome": "always" 18 | }, 19 | "editor.insertSpaces": false, 20 | "editor.detectIndentation": true, 21 | "editor.trimAutoWhitespace": true, 22 | "files.trimTrailingWhitespace": true, 23 | "files.trimTrailingWhitespaceInRegexAndStrings": true, 24 | "files.trimFinalNewlines": true, 25 | "explorer.fileNesting.patterns": { 26 | "*.ts": "${basename}.*.${extname}", 27 | ".env": ".env.*", 28 | "*.tsx": "${basename}.*.${extname},${basename}.*.ts", 29 | "package.json": "*.json, *.yml, *.config.js, *.config.ts, *.yaml" 30 | }, 31 | "eslint.enable": false, 32 | "eslint.format.enable": false, 33 | "prettier.enable": false, 34 | "[typescript]": { 35 | "editor.defaultFormatter": "biomejs.biome" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Forge 42 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-directives-plugin 2 | 3 | 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/forge-42/react-directives-plugin?style=social) 5 | ![npm](https://img.shields.io/npm/v/react-directives-plugin?style=plastic) 6 | ![GitHub](https://img.shields.io/github/license/forge-42/react-directives-plugin?style=plastic) 7 | ![npm](https://img.shields.io/npm/dy/react-directives-plugin?style=plastic) 8 | ![npm](https://img.shields.io/npm/dw/react-directives-plugin?style=plastic) 9 | ![GitHub top language](https://img.shields.io/github/languages/top/forge-42/react-directives-plugin?style=plastic) 10 | 11 | react-directives-plugin is a vite plugin that allows you to add directives on top of files matched by patterns you 12 | provide. Do not forget to add a `"use server"`, `"use client"` or `"use strict"` directive to the top of your file ever again! 13 | 14 | ## Installation 15 | 16 | ```bash 17 | npm install -D react-directives-plugin 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```ts 23 | import { reactDirectives } from "react-directives-plugin"; 24 | 25 | export default defineConfig({ 26 | plugins: [ 27 | reactDirectives({ 28 | // Adds a "use server" directive to all files that end with .action.ts 29 | "use server": ["**/*.action.ts"], 30 | // Adds a "use client" directive to all files that end with .client.ts 31 | "use client": ["**/*.client.ts"], 32 | // Adds a "use strict" directive to all files that end with .strict.ts 33 | "use strict": ["**/*.strict.ts"], 34 | }) 35 | ], 36 | }); 37 | ``` 38 | 39 | ### Config options 40 | 41 | | Option | Type | Description | 42 | |--------|------|-------------| 43 | | `use server` | `string[]` | An array of glob patterns to match files to add the `"use server"` directive. | 44 | | `use client` | `string[]` | An array of glob patterns to match files to add the `"use client"` directive. | 45 | | `use strict` | `string[]` | An array of glob patterns to match files to add the `"use strict"` directive. | 46 | -------------------------------------------------------------------------------- /SECURITY.MD: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | In case of a vulnerability please reach out to active maintainers of the project and report it to them. -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "defaultBranch": "main", 7 | "useIgnoreFile": true 8 | }, 9 | "formatter": { 10 | "ignore": ["test-apps"], 11 | "enabled": true, 12 | "formatWithErrors": false, 13 | "indentStyle": "tab", 14 | "lineEnding": "lf", 15 | "lineWidth": 120 16 | }, 17 | "organizeImports": { 18 | "ignore": ["test-apps"], 19 | "enabled": true 20 | }, 21 | "linter": { 22 | "ignore": ["test-apps"], 23 | "enabled": true, 24 | "rules": { 25 | "recommended": true, 26 | "suspicious": { 27 | "recommended": true, 28 | "noConsole": "error" 29 | }, 30 | "style": { 31 | "recommended": true 32 | }, 33 | "complexity": { 34 | "recommended": true 35 | }, 36 | "security": { 37 | "recommended": true 38 | }, 39 | "performance": { 40 | "recommended": true 41 | }, 42 | "correctness": { 43 | "recommended": true, 44 | "noUnusedImports": "error", 45 | "noUnusedVariables": "error", 46 | "noUnusedLabels": "error", 47 | "noUnusedFunctionParameters": "error" 48 | }, 49 | "a11y": { 50 | "recommended": true 51 | }, 52 | "nursery": { 53 | "recommended": true 54 | } 55 | } 56 | }, 57 | "javascript": { 58 | "formatter": { 59 | "semicolons": "asNeeded", 60 | "trailingCommas": "es5" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | check: 5 | run: pnpm run check --staged --fix --no-errors-on-unmatched 6 | stage_fixed: true 7 | typecheck: 8 | run: pnpm run typecheck 9 | test: 10 | run: pnpm run test 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-directives-plugin", 3 | "version": "1.0.0", 4 | "description": "Vite plugin that allows you to add react directives to files behind the scenes", 5 | "scripts": { 6 | "build": "pnpm run --filter=\"./packages/**/*\" build", 7 | "build:watch": "pnpm build && pnpm run --filter=\"./packages/**/*\" --parallel build --watch", 8 | "clean": "git clean -fdX .", 9 | "clean:build": "git clean -fdx -e node_modules .", 10 | "typecheck": "pnpm run --filter=\"./packages/**/*\" --parallel typecheck", 11 | "test": "pnpm run --filter=\"./packages/**/*\" --parallel test", 12 | "test:cov": "pnpm run --filter=\"./packages/**/*\" --parallel test:cov", 13 | "dev": "pnpm build && pnpm run --parallel dev", 14 | "check": "biome check .", 15 | "check:fix": "biome check --fix .", 16 | "changeset": "changeset", 17 | "release": "changeset publish", 18 | "local-release": "changeset version && changeset publish", 19 | "version": "changeset version" 20 | }, 21 | "author": "forge-42", 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/forge-42/react-directives-plugin.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/forge-42/react-directives-plugin/issues" 29 | }, 30 | "homepage": "https://github.com/forge-42/react-directives-plugin#readme", 31 | "devDependencies": { 32 | "@biomejs/biome": "^1.9.4", 33 | "@changesets/cli": "^2.29.0", 34 | "@types/node": "^20.17.30", 35 | "lefthook": "^1.11.10" 36 | }, 37 | "packageManager": "pnpm@10.6.5", 38 | "engines": { 39 | "pnpm": ">=10.6.5", 40 | "node": ">=20.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-directives-plugin 2 | 3 | ## 1.1.1 4 | 5 | ### Patch Changes 6 | 7 | - dfba6a2: update docs 8 | 9 | ## 1.1.0 10 | 11 | ### Minor Changes 12 | 13 | - 56bf289: Added support for custom directives like "use something" 14 | 15 | ## 1.0.0 16 | 17 | ### Major Changes 18 | 19 | - 365b89d: Initial release of react-directives-plugin 20 | 21 | ## 1.0.0 22 | 23 | ### Major Changes 24 | 25 | - 5c9ec3c: The inital release of react-directives-plugin 26 | 27 | ## 1.0.0 28 | 29 | ### Major Changes 30 | 31 | - 7776354: Initial release of react-directives package 32 | 33 | ## 1.1.2 34 | 35 | ### Patch Changes 36 | 37 | - 45ae372: Updated dependencies, fixed a bug with staging script 38 | - ecd88f8: Migrated to tsdown from tsup 39 | 40 | ## 1.1.1 41 | 42 | ### Patch Changes 43 | 44 | - 34f867e: Added provencance to the package release 45 | 46 | ## 1.1.0 47 | 48 | ### Minor Changes 49 | 50 | - f2fbd38: Added changesets to the project 51 | - e051f2f: We have migrated the react-directives-plugin to use pnpm workspaces with changesets instead of the old npm approach with npm workspaces. 52 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/README.md: -------------------------------------------------------------------------------- 1 | # react-directives-plugin 2 | 3 | 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/forge-42/react-directives-plugin?style=social) 5 | ![npm](https://img.shields.io/npm/v/react-directives-plugin?style=plastic) 6 | ![GitHub](https://img.shields.io/github/license/forge-42/react-directives-plugin?style=plastic) 7 | ![npm](https://img.shields.io/npm/dy/react-directives-plugin?style=plastic) 8 | ![npm](https://img.shields.io/npm/dw/react-directives-plugin?style=plastic) 9 | ![GitHub top language](https://img.shields.io/github/languages/top/forge-42/react-directives-plugin?style=plastic) 10 | 11 | react-directives-plugin is a vite plugin that allows you to add directives on top of files matched by patterns you 12 | provide. Do not forget to add a `"use server"`, `"use client"` or `"use strict"` directive to the top of your file ever again! 13 | 14 | ## Installation 15 | 16 | ```bash 17 | npm install -D react-directives-plugin 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```ts 23 | import { reactDirectives } from "react-directives-plugin"; 24 | 25 | export default defineConfig({ 26 | plugins: [ 27 | reactDirectives({ 28 | // Adds a "use server" directive to all files that end with .server.ts 29 | "use server": ["**/*.server.ts"], 30 | // Adds a "use client" directive to all files that end with .client.ts 31 | "use client": ["**/*.client.ts"], 32 | // Adds a "use strict" directive to all files that end with .strict.ts 33 | "use strict": ["**/*.strict.ts"], 34 | }) 35 | ], 36 | }); 37 | ``` 38 | 39 | ### Config options 40 | 41 | | Option | Type | Description | 42 | |--------|------|-------------| 43 | | `use server` | `string[]` | An array of glob patterns to match files to add the `"use server"` directive. | 44 | | `use client` | `string[]` | An array of glob patterns to match files to add the `"use client"` directive. | 45 | | `use strict` | `string[]` | An array of glob patterns to match files to add the `"use strict"` directive. | 46 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-directives-plugin", 3 | "version": "1.1.1", 4 | "description": "Vite plugin that allows you to add react directives to files behind the scenes", 5 | "module": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "type": "module", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "import": { 12 | "types": "./dist/index.d.ts", 13 | "import": "./dist/index.js", 14 | "default": "./dist/index.js" 15 | } 16 | } 17 | }, 18 | "scripts": { 19 | "test": "vitest run", 20 | "test:cov": "vitest run --coverage", 21 | "build": "tsdown src/index.ts --config tsdown.config.ts --clean", 22 | "dev": "tsdown src/index.ts --config tsdown.config.ts --watch", 23 | "prepublishOnly": "pnpm run build", 24 | "typecheck": "tsc", 25 | "validate": "pnpm run check && pnpm run typecheck && pnpm run test", 26 | "check:exports": "attw --pack ." 27 | }, 28 | "author": "", 29 | "license": "MIT", 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/forge-42/react-directives-plugin.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/forge-42/react-directives-plugin/issues" 36 | }, 37 | "files": [ 38 | "dist" 39 | ], 40 | "homepage": "https://github.com/forge-42/react-directives-plugin#readme", 41 | "publishConfig": { 42 | "provenance": true 43 | }, 44 | "peerDependencies": { 45 | "vite": ">=5.0.0" 46 | }, 47 | "devDependencies": { 48 | "@arethetypeswrong/cli": "^0.17.4", 49 | "@changesets/cli": "^2.29.0", 50 | "@types/babel__core": "^7.20.5", 51 | "@types/babel__generator": "^7.27.0", 52 | "@types/babel__traverse": "^7.20.7", 53 | "@types/node": "^20.17.30", 54 | "@types/picomatch": "^4.0.0", 55 | "@vitest/coverage-v8": "^3.1.1", 56 | "happy-dom": "^17.4.4", 57 | "tsdown": "^0.9.1", 58 | "typescript": "^5.8.3", 59 | "vitest": "^3.1.1" 60 | }, 61 | "dependencies": { 62 | "@babel/core": "^7.26.10", 63 | "@babel/generator": "^7.26.10", 64 | "@babel/parser": "^7.26.10", 65 | "@babel/traverse": "^7.26.10", 66 | "@babel/types": "^7.26.10", 67 | "picomatch": "^4.0.2" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/src/babel.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "@babel/parser" 2 | 3 | import * as t from "@babel/types" 4 | 5 | export { parse, t } 6 | import generate from "@babel/generator" 7 | import traverse from "@babel/traverse" 8 | export const trav = 9 | // biome-ignore lint/suspicious/noExplicitAny: 10 | typeof (traverse as any).default !== "undefined" 11 | ? // biome-ignore lint/suspicious/noExplicitAny: 12 | ((traverse as any).default as typeof import("@babel/traverse").default) 13 | : traverse 14 | 15 | export const gen = 16 | // biome-ignore lint/suspicious/noExplicitAny: 17 | typeof (generate as any).default !== "undefined" 18 | ? // biome-ignore lint/suspicious/noExplicitAny: 19 | ((generate as any).default as typeof import("@babel/generator").default) 20 | : generate 21 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "vite" 2 | import { transformCode } from "./transformer" 3 | 4 | export interface ReactDirectivesPluginOptions { 5 | "use strict"?: string[] 6 | "use server"?: string[] 7 | "use client"?: string[] 8 | [key: `use ${string}`]: string[] | undefined 9 | } 10 | 11 | // This is your packages entry point, everything exported from here will be accessible to the end-user. 12 | export const reactDirectives = ( 13 | props?: ReactDirectivesPluginOptions 14 | // biome-ignore lint/suspicious/noExplicitAny: Avoid issues with vite-plugin types 15 | ): any => { 16 | return { 17 | name: "react-directives-plugin", 18 | enforce: "pre", 19 | transform(code, id) { 20 | return transformCode(code, id, props) 21 | }, 22 | } satisfies Plugin 23 | } 24 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/src/transformer.test.ts: -------------------------------------------------------------------------------- 1 | import { addReactDirectives, transformCode } from "./transformer" 2 | const removeWhitespace = (str: string) => str.replace(/\s/g, "") 3 | describe("addReactDirectives", () => { 4 | it("should add directives to the top of the file", () => { 5 | const code = "const a = 1;" 6 | const directivesToAdd = ["use client", "use server"] 7 | const id = "test.js" 8 | const result = addReactDirectives(code, directivesToAdd, id) 9 | expect(removeWhitespace(result.code)).toMatch( 10 | removeWhitespace(` 11 | "use server"; 12 | "use client"; 13 | const a = 1; 14 | `) 15 | ) 16 | }) 17 | 18 | it("should not add directives to the top of the file if they exist", () => { 19 | const code = ` 20 | "use server"; 21 | "use strict"; 22 | const a = 1; 23 | ` 24 | const directivesToAdd = ["use client", "use server"] 25 | const id = "test.js" 26 | const result = addReactDirectives(code, directivesToAdd, id) 27 | 28 | expect(removeWhitespace(result.code)).toEqual( 29 | removeWhitespace(` 30 | "use server"; 31 | "use strict"; 32 | "use client"; 33 | const a = 1; 34 | `) 35 | ) 36 | }) 37 | 38 | it("should not add directives if none are provided", () => { 39 | const code = "const a = 1;" 40 | const directivesToAdd: string[] = [] 41 | const id = "test.js" 42 | const result = addReactDirectives(code, directivesToAdd, id) 43 | expect(result.code).toBe(code) 44 | }) 45 | }) 46 | 47 | describe("transformCode", () => { 48 | it("should add client directive with single matcher", () => { 49 | const code = "const a = 1;" 50 | const id = "C://path/to/file/test.js" 51 | const result = transformCode(code, id, { 52 | "use client": ["**/*.js"], 53 | "use server": [], 54 | "use strict": [], 55 | }) 56 | expect(removeWhitespace(result.code)).toMatch( 57 | removeWhitespace(` 58 | "use client"; 59 | const a = 1; 60 | `) 61 | ) 62 | }) 63 | it("should add client directive with multiple matchers", () => { 64 | const code = "const a = 1;" 65 | const id = "C://path/to/file/test.js" 66 | const result = transformCode(code, id, { 67 | "use client": ["*/.ts", "**/*.js"], 68 | "use server": [], 69 | "use strict": [], 70 | }) 71 | expect(removeWhitespace(result.code)).toMatch( 72 | removeWhitespace(` 73 | "use client"; 74 | const a = 1; 75 | `) 76 | ) 77 | }) 78 | it("should not add server directive with multiple matchers if they don't match", () => { 79 | const code = "const a = 1;" 80 | const id = "C://path/to/file/test.js" 81 | const result = transformCode(code, id, { 82 | "use client": [], 83 | "use server": ["*/.ts", "**/*.ts"], 84 | "use strict": [], 85 | }) 86 | expect(removeWhitespace(result.code)).toMatch( 87 | removeWhitespace(` 88 | const a = 1; 89 | `) 90 | ) 91 | }) 92 | 93 | it("should add server directive with single matcher", () => { 94 | const code = "const a = 1;" 95 | const id = "C://path/to/file/test.js" 96 | const result = transformCode(code, id, { 97 | "use client": [], 98 | "use server": ["**/*.js"], 99 | "use strict": [], 100 | }) 101 | expect(removeWhitespace(result.code)).toMatch( 102 | removeWhitespace(` 103 | "use server"; 104 | const a = 1; 105 | `) 106 | ) 107 | }) 108 | 109 | it("should add server directive with single matcher with suffix", () => { 110 | const code = "const a = 1;" 111 | const id = "C://path/to/file/test.server.js" 112 | const result = transformCode(code, id, { 113 | "use client": [], 114 | "use server": ["**/*.server.js"], 115 | "use strict": [], 116 | }) 117 | expect(removeWhitespace(result.code)).toMatch( 118 | removeWhitespace(` 119 | "use server"; 120 | const a = 1; 121 | `) 122 | ) 123 | }) 124 | it("should add server directive with multiple matchers", () => { 125 | const code = "const a = 1;" 126 | const id = "C://path/to/file/test.js" 127 | const result = transformCode(code, id, { 128 | "use client": [], 129 | "use server": ["*/.ts", "**/*.js"], 130 | "use strict": [], 131 | }) 132 | expect(removeWhitespace(result.code)).toMatch( 133 | removeWhitespace(` 134 | "use server"; 135 | const a = 1; 136 | `) 137 | ) 138 | }) 139 | it("should not add server directive with multiple matchers if they don't match", () => { 140 | const code = "const a = 1;" 141 | const id = "C://path/to/file/test.js" 142 | const result = transformCode(code, id, { 143 | "use client": [], 144 | "use server": ["*/.ts", "**/*.ts"], 145 | "use strict": [], 146 | }) 147 | expect(removeWhitespace(result.code)).toMatch( 148 | removeWhitespace(` 149 | 150 | const a = 1; 151 | `) 152 | ) 153 | }) 154 | 155 | it("should not add strict directive with multiple matchers if they don't match", () => { 156 | const code = "const a = 1;" 157 | const id = "C://path/to/file/test.js" 158 | const result = transformCode(code, id, { 159 | "use client": [], 160 | "use server": [], 161 | "use strict": ["*/.ts", "**/*.ts"], 162 | }) 163 | expect(removeWhitespace(result.code)).toMatch( 164 | removeWhitespace(` 165 | const a = 1; 166 | `) 167 | ) 168 | }) 169 | 170 | it("should add strict directive with single matcher", () => { 171 | const code = "const a = 1;" 172 | const id = "C://path/to/file/test.js" 173 | const result = transformCode(code, id, { 174 | "use client": [], 175 | "use server": [], 176 | "use strict": ["**/*.js"], 177 | }) 178 | expect(removeWhitespace(result.code)).toMatch( 179 | removeWhitespace(` 180 | "use strict"; 181 | const a = 1; 182 | `) 183 | ) 184 | }) 185 | it("should add strict directive with multiple matchers", () => { 186 | const code = "const a = 1;" 187 | const id = "C://path/to/file/test.js" 188 | const result = transformCode(code, id, { 189 | "use client": [], 190 | "use server": [], 191 | "use strict": ["*/.ts", "**/*.js"], 192 | }) 193 | expect(removeWhitespace(result.code)).toMatch( 194 | removeWhitespace(` 195 | "use strict"; 196 | const a = 1; 197 | `) 198 | ) 199 | }) 200 | 201 | it("should add random directive with multiple matchers", () => { 202 | const code = "const a = 1;" 203 | const id = "C://path/to/file/test.js" 204 | const result = transformCode(code, id, { 205 | "use client": [], 206 | "use server": [], 207 | "use strict": ["*/.ts", "**/*.js"], 208 | "use potato": ["*/.ts", "**/*.js"], 209 | }) 210 | expect(removeWhitespace(result.code)).toMatch( 211 | removeWhitespace(` 212 | "use potato"; 213 | "use strict"; 214 | const a = 1; 215 | `) 216 | ) 217 | }) 218 | it("should not add strict directive with multiple matchers if they don't match", () => { 219 | const code = "const a = 1;" 220 | const id = "C://path/to/file/test.js" 221 | const result = transformCode(code, id, { 222 | "use client": [], 223 | "use server": [], 224 | "use strict": ["*/.ts", "**/*.ts"], 225 | }) 226 | expect(removeWhitespace(result.code)).toMatch( 227 | removeWhitespace(` 228 | 229 | const a = 1; 230 | `) 231 | ) 232 | }) 233 | }) 234 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/src/transformer.ts: -------------------------------------------------------------------------------- 1 | import type { types as Babel } from "@babel/core" 2 | import type { ParseResult } from "@babel/parser" 3 | import picomatch from "picomatch" 4 | import type { ReactDirectivesPluginOptions } from "." 5 | import { gen, parse, t } from "./babel" 6 | 7 | const transform = (ast: ParseResult, directivesToAdd: string[]) => { 8 | if (!directivesToAdd.length) { 9 | return false 10 | } 11 | 12 | for (const directive of directivesToAdd) { 13 | ast.program.body.unshift(t.expressionStatement(t.stringLiteral(directive))) 14 | } 15 | 16 | return true 17 | } 18 | 19 | export function addReactDirectives(code: string, directivesToAdd: string[], id: string) { 20 | const [filePath] = id.split("?") 21 | try { 22 | const ast = parse(code, { sourceType: "module" }) 23 | const nonExistingDirectives = directivesToAdd.filter((directive) => { 24 | const directiveString = `"${directive}"` 25 | if (code.includes(directiveString)) { 26 | return false 27 | } 28 | return true 29 | }) 30 | 31 | const didTransform = transform(ast, nonExistingDirectives) 32 | if (!didTransform) { 33 | return { code } 34 | } 35 | return gen(ast, { sourceMaps: true, filename: id, sourceFileName: filePath }) 36 | } catch (_e) { 37 | return { code } 38 | } 39 | } 40 | 41 | export const transformCode = (code: string, id: string, directives?: ReactDirectivesPluginOptions) => { 42 | if (!directives) { 43 | return { code } 44 | } 45 | const directivesToAdd = [] 46 | for (const [key, value] of Object.entries(directives)) { 47 | const matcher = picomatch(value) 48 | if (matcher(id)) { 49 | directivesToAdd.push(key) 50 | } 51 | } 52 | 53 | if (!directivesToAdd.length) { 54 | return { code } 55 | } 56 | 57 | return addReactDirectives(code, directivesToAdd, id) 58 | } 59 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/tests/setup.ts: -------------------------------------------------------------------------------- 1 | // Setup your test environment here 2 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "moduleResolution": "Bundler", 5 | "module": "ESNext" /* Specify what module code is generated. */, 6 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 7 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 8 | "strict": true /* Enable all strict type-checking options. */, 9 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 10 | "types": ["vitest/globals"], 11 | "rootDir": ".", 12 | "outDir": "./dist", 13 | "noEmit": true 14 | }, 15 | "include": ["src/**/*", "tests/**/*"], 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsdown" 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | sourcemap: true, 6 | dts: true, 7 | minify: false, 8 | format: ["esm"], 9 | outDir: "dist", 10 | }) 11 | -------------------------------------------------------------------------------- /packages/react-directives-plugin/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vitest/config" 3 | 4 | export default defineConfig({ 5 | test: { 6 | setupFiles: ["./tests/setup.ts"], 7 | environment: "node", 8 | globals: true, 9 | 10 | coverage: { 11 | all: false, 12 | provider: "v8", 13 | reporter: ["json-summary", "html"], 14 | thresholds: { 15 | statements: 80, 16 | branches: 80, 17 | functions: 80, 18 | lines: 80, 19 | }, 20 | }, 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - test-apps/* 4 | onlyBuiltDependencies: 5 | - '@biomejs/biome' 6 | - esbuild 7 | - lefthook 8 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "." 2 | 3 | describe("test", () => { 4 | it("should work", () => { 5 | expect(test()).toBeUndefined() 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // This is your packages entry point, everything exported from here will be accessible to the end-user. 2 | export const test = (): void => {} 3 | -------------------------------------------------------------------------------- /test-apps/react-router-esm/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is intended to be a basic starting point for linting in your app. 3 | * It relies on recommended configs out of the box for simplicity, but you can 4 | * and should modify this configuration to best suit your team's needs. 5 | */ 6 | 7 | /** @type {import('eslint').Linter.Config} */ 8 | module.exports = { 9 | root: true, 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | sourceType: "module", 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | env: { 18 | browser: true, 19 | commonjs: true, 20 | es6: true, 21 | }, 22 | ignorePatterns: ["!**/.server", "!**/.client"], 23 | 24 | // Base config 25 | extends: ["eslint:recommended"], 26 | 27 | overrides: [ 28 | // React 29 | { 30 | files: ["**/*.{js,jsx,ts,tsx}"], 31 | plugins: ["react", "jsx-a11y"], 32 | extends: [ 33 | "plugin:react/recommended", 34 | "plugin:react/jsx-runtime", 35 | "plugin:react-hooks/recommended", 36 | "plugin:jsx-a11y/recommended", 37 | ], 38 | settings: { 39 | react: { 40 | version: "detect", 41 | }, 42 | formComponents: ["Form"], 43 | linkComponents: [ 44 | { name: "Link", linkAttribute: "to" }, 45 | { name: "NavLink", linkAttribute: "to" }, 46 | ], 47 | "import/resolver": { 48 | typescript: {}, 49 | }, 50 | }, 51 | }, 52 | 53 | // Typescript 54 | { 55 | files: ["**/*.{ts,tsx}"], 56 | plugins: ["@typescript-eslint", "import"], 57 | parser: "@typescript-eslint/parser", 58 | settings: { 59 | "import/internal-regex": "^~/", 60 | "import/resolver": { 61 | node: { 62 | extensions: [".ts", ".tsx"], 63 | }, 64 | typescript: { 65 | alwaysTryTypes: true, 66 | }, 67 | }, 68 | }, 69 | extends: [ 70 | "plugin:@typescript-eslint/recommended", 71 | "plugin:import/recommended", 72 | "plugin:import/typescript", 73 | ], 74 | }, 75 | 76 | // Node 77 | { 78 | files: [".eslintrc.cjs"], 79 | env: { 80 | node: true, 81 | }, 82 | }, 83 | ], 84 | }; 85 | -------------------------------------------------------------------------------- /test-apps/react-router-esm/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | -------------------------------------------------------------------------------- /test-apps/react-router-esm/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix + Vite! 2 | 3 | 📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/guides/vite) for details on supported features. 4 | 5 | ## Development 6 | 7 | Run the Vite dev server: 8 | 9 | ```shellscript 10 | npm run dev 11 | ``` 12 | 13 | ## Deployment 14 | 15 | First, build your app for production: 16 | 17 | ```sh 18 | npm run build 19 | ``` 20 | 21 | Then run the app in production mode: 22 | 23 | ```sh 24 | npm start 25 | ``` 26 | 27 | Now you'll need to pick a host to deploy it to. 28 | 29 | ### DIY 30 | 31 | If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. 32 | 33 | Make sure to deploy the output of `npm run build` 34 | 35 | - `build/server` 36 | - `build/client` 37 | -------------------------------------------------------------------------------- /test-apps/react-router-esm/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { HydratedRouter } from "react-router/dom" 8 | import { startTransition, StrictMode } from "react"; 9 | import { hydrateRoot } from "react-dom/client"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /test-apps/react-router-esm/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle generating the HTTP Response for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.server 5 | */ 6 | 7 | import { PassThrough } from "node:stream"; 8 | import { createReadableStreamFromReadable } from "@react-router/node"; 9 | import { type AppLoadContext, type EntryContext, ServerRouter } from "react-router" 10 | import { isbot } from "isbot"; 11 | import { renderToPipeableStream } from "react-dom/server"; 12 | 13 | const ABORT_DELAY = 5_000; 14 | 15 | export default function handleRequest( 16 | request: Request, 17 | responseStatusCode: number, 18 | responseHeaders: Headers, 19 | reactRouterContext: EntryContext, 20 | // This is ignored so we can keep it in the template for visibility. Feel 21 | // free to delete this parameter in your app if you're not using it! 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | loadContext: AppLoadContext 24 | ) { 25 | return isbot(request.headers.get("user-agent") || "") 26 | ? handleBotRequest( 27 | request, 28 | responseStatusCode, 29 | responseHeaders, 30 | reactRouterContext 31 | ) 32 | : handleBrowserRequest( 33 | request, 34 | responseStatusCode, 35 | responseHeaders, 36 | reactRouterContext 37 | ); 38 | } 39 | 40 | function handleBotRequest( 41 | request: Request, 42 | responseStatusCode: number, 43 | responseHeaders: Headers, 44 | reactRouterContext: EntryContext 45 | ) { 46 | return new Promise((resolve, reject) => { 47 | let shellRendered = false; 48 | const { pipe, abort } = renderToPipeableStream( 49 | , 50 | { 51 | onAllReady() { 52 | shellRendered = true; 53 | const body = new PassThrough(); 54 | const stream = createReadableStreamFromReadable(body); 55 | 56 | responseHeaders.set("Content-Type", "text/html"); 57 | 58 | resolve( 59 | new Response(stream, { 60 | headers: responseHeaders, 61 | status: responseStatusCode, 62 | }) 63 | ); 64 | 65 | pipe(body); 66 | }, 67 | onShellError(error: unknown) { 68 | reject(error); 69 | }, 70 | onError(error: unknown) { 71 | responseStatusCode = 500; 72 | // Log streaming rendering errors from inside the shell. Don't log 73 | // errors encountered during initial shell rendering since they'll 74 | // reject and get logged in handleDocumentRequest. 75 | if (shellRendered) { 76 | console.error(error); 77 | } 78 | }, 79 | } 80 | ); 81 | 82 | setTimeout(abort, ABORT_DELAY); 83 | }); 84 | } 85 | 86 | function handleBrowserRequest( 87 | request: Request, 88 | responseStatusCode: number, 89 | responseHeaders: Headers, 90 | reactRouterContext: EntryContext 91 | ) { 92 | return new Promise((resolve, reject) => { 93 | let shellRendered = false; 94 | const { pipe, abort } = renderToPipeableStream( 95 | , 96 | { 97 | onShellReady() { 98 | shellRendered = true; 99 | const body = new PassThrough(); 100 | const stream = createReadableStreamFromReadable(body); 101 | 102 | responseHeaders.set("Content-Type", "text/html"); 103 | 104 | resolve( 105 | new Response(stream, { 106 | headers: responseHeaders, 107 | status: responseStatusCode, 108 | }) 109 | ); 110 | 111 | pipe(body); 112 | }, 113 | onShellError(error: unknown) { 114 | reject(error); 115 | }, 116 | onError(error: unknown) { 117 | responseStatusCode = 500; 118 | // Log streaming rendering errors from inside the shell. Don't log 119 | // errors encountered during initial shell rendering since they'll 120 | // reject and get logged in handleDocumentRequest. 121 | if (shellRendered) { 122 | console.error(error); 123 | } 124 | }, 125 | } 126 | ); 127 | 128 | setTimeout(abort, ABORT_DELAY); 129 | }); 130 | } 131 | -------------------------------------------------------------------------------- /test-apps/react-router-esm/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Links, 3 | Meta, 4 | Outlet, 5 | Scripts, 6 | ScrollRestoration, 7 | } from "react-router"; 8 | import { something } from "./transform.server"; 9 | import { somethingElse } from "./transform.client"; 10 | import { somethingElseThird } from "./transform.strict"; 11 | 12 | export const loader = async () => { 13 | somethingElse(); 14 | somethingElseThird (); 15 | return something() 16 | } 17 | 18 | export function Layout({ children }: { children: React.ReactNode }) { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {children} 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default function App() { 37 | return ; 38 | } 39 | -------------------------------------------------------------------------------- /test-apps/react-router-esm/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { route } from "@react-router/dev/routes"; 2 | 3 | export default [ 4 | route("/", "./routes/_index.tsx") 5 | ] -------------------------------------------------------------------------------- /test-apps/react-router-esm/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction } from "react-router"; 2 | 3 | export const meta: MetaFunction = () => { 4 | return [{ title: "New Remix App" }, { name: "description", content: "Welcome to Remix!" }]; 5 | }; 6 | 7 | export default function Index() { 8 | return ( 9 |
10 |

Welcome to Remix

11 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /test-apps/react-router-esm/app/transform.client.ts: -------------------------------------------------------------------------------- 1 | export async function somethingElse (){ 2 | console.log("hello") 3 | return "hello" 4 | } -------------------------------------------------------------------------------- /test-apps/react-router-esm/app/transform.server.ts: -------------------------------------------------------------------------------- 1 | export async function something (){ 2 | console.log("hello") 3 | return "hello" 4 | } -------------------------------------------------------------------------------- /test-apps/react-router-esm/app/transform.strict.ts: -------------------------------------------------------------------------------- 1 | export async function somethingElseThird(){ 2 | console.log("hello") 3 | return "hello" 4 | } -------------------------------------------------------------------------------- /test-apps/react-router-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-esm", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "scripts": { 7 | "build": "react-router build", 8 | "dev": "react-router dev", 9 | "start": "react-router-serve ./build/server/index.js", 10 | "typecheck": "tsc" 11 | }, 12 | "dependencies": { 13 | "@react-router/node": "^7.5.0", 14 | "@react-router/serve": "^7.5.0", 15 | "isbot": "^5.1.26", 16 | "react": "^19.1.0", 17 | "react-directives-plugin": "workspace:*", 18 | "react-dom": "^19.1.0", 19 | "react-router": "^7.5.0", 20 | "vite-plugin-inspect": "^11.1.0" 21 | }, 22 | "devDependencies": { 23 | "@react-router/dev": "^7.5.0", 24 | "@types/node": "22.14.1", 25 | "@types/react": "^19.1.2", 26 | "@types/react-dom": "^19.1.2", 27 | "react-router-devtools": "1.1.10", 28 | "typescript": "^5.8.3", 29 | "vite": "^6.2.6", 30 | "vite-tsconfig-paths": "^5.1.4" 31 | }, 32 | "engines": { 33 | "node": ">=22.0.0" 34 | } 35 | } -------------------------------------------------------------------------------- /test-apps/react-router-esm/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forge-42/react-directives-plugin/55d1fa63507a9f446bbb5111ee282a13eef9eeaf/test-apps/react-router-esm/public/favicon.ico -------------------------------------------------------------------------------- /test-apps/react-router-esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*.ts", 4 | "**/*.tsx", 5 | "**/.server/**/*.ts", 6 | "**/.server/**/*.tsx", 7 | "**/.client/**/*.ts", 8 | "**/.client/**/*.tsx" 9 | ], 10 | "compilerOptions": { 11 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 12 | "types": ["@react-router/node", "vite/client"], 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "jsx": "react-jsx", 16 | "module": "ESNext", 17 | "moduleResolution": "Bundler", 18 | "resolveJsonModule": true, 19 | "target": "ES2022", 20 | "strict": true, 21 | "allowJs": true, 22 | "skipLibCheck": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "~/*": ["./app/*"] 27 | }, 28 | 29 | // Vite takes care of building everything, not tsc. 30 | "noEmit": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test-apps/react-router-esm/vite.config.ts: -------------------------------------------------------------------------------- 1 | 2 | import { defineConfig } from "vite"; 3 | import { reactDirectives } from "react-directives-plugin"; 4 | import { reactRouterDevTools } from "react-router-devtools" 5 | import { reactRouter } from "@react-router/dev/vite"; 6 | import tsconfigPaths from "vite-tsconfig-paths"; 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | reactDirectives({ 11 | "use server": ["**/*.server.ts"], 12 | "use client": ["**/*.client.ts"], 13 | "use strict": ["**/*.strict.ts"], 14 | }), 15 | reactRouterDevTools(),reactRouter(), tsconfigPaths() 16 | ], 17 | }); 18 | --------------------------------------------------------------------------------