├── .cargo └── config.toml ├── .gitattributes ├── .github ├── actions │ └── pnpm │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .taplo.toml ├── .vscode └── settings.json ├── .yarnrc.yml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── build.rs ├── index.d.ts ├── index.js ├── package.json ├── pnpm-lock.yaml ├── rustfmt.toml ├── src ├── lib.rs └── transform │ ├── mod.rs │ ├── react_server_action.rs │ └── validate.rs ├── tests └── index.unit.ts └── tsconfig.json /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-musl] 2 | linker = "aarch64-linux-musl-gcc" 3 | rustflags = ["-C", "target-feature=-crt-static"] 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | 5 | *.ts text eol=lf merge=union 6 | *.tsx text eol=lf merge=union 7 | *.rs text eol=lf merge=union 8 | *.js text eol=lf merge=union 9 | *.json text eol=lf merge=union 10 | *.debug text eol=lf merge=union 11 | 12 | # Generated codes 13 | index.js linguist-detectable=false 14 | index.d.ts linguist-detectable=false 15 | -------------------------------------------------------------------------------- /.github/actions/pnpm/action.yml: -------------------------------------------------------------------------------- 1 | name: Pnpm 2 | 3 | description: Install pnpm 4 | 5 | runs: 6 | using: composite 7 | steps: 8 | - uses: pnpm/action-setup@v4 9 | 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version-file: .nvmrc 13 | cache: pnpm 14 | 15 | - run: pnpm install 16 | shell: bash 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | env: 3 | DEBUG: napi:* 4 | APP_NAME: react-server-action 5 | MACOSX_DEPLOYMENT_TARGET: '10.13' 6 | CC: clang 7 | CARGO_INCREMENTAL: '1' 8 | permissions: 9 | contents: write 10 | id-token: write 11 | 'on': 12 | push: 13 | branches: 14 | - main 15 | tags-ignore: 16 | - '**' 17 | paths-ignore: 18 | - '**/*.md' 19 | - LICENSE 20 | - '**/*.gitignore' 21 | - .editorconfig 22 | - docs/** 23 | pull_request: null 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.ref }} 26 | cancel-in-progress: true 27 | jobs: 28 | build: 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | settings: 33 | - host: macos-latest 34 | target: x86_64-apple-darwin 35 | build: pnpm build --target x86_64-apple-darwin 36 | - host: windows-latest 37 | build: pnpm build --target x86_64-pc-windows-msvc 38 | target: x86_64-pc-windows-msvc 39 | - host: windows-latest 40 | build: pnpm build --target i686-pc-windows-msvc 41 | target: i686-pc-windows-msvc 42 | - host: ubuntu-latest 43 | target: x86_64-unknown-linux-gnu 44 | build: pnpm build --target x86_64-unknown-linux-gnu --use-napi-cross 45 | - host: ubuntu-latest 46 | target: x86_64-unknown-linux-musl 47 | build: pnpm build --target x86_64-unknown-linux-musl -x 48 | - host: macos-14 49 | target: aarch64-apple-darwin 50 | build: pnpm build --target aarch64-apple-darwin 51 | - host: ubuntu-latest 52 | target: aarch64-unknown-linux-gnu 53 | build: pnpm build --target aarch64-unknown-linux-gnu --use-napi-cross 54 | - host: ubuntu-latest 55 | target: armv7-unknown-linux-gnueabihf 56 | build: pnpm build --target armv7-unknown-linux-gnueabihf --use-napi-cross 57 | - host: ubuntu-latest 58 | target: aarch64-linux-android 59 | build: pnpm build --target aarch64-linux-android 60 | - host: ubuntu-latest 61 | target: armv7-linux-androideabi 62 | build: pnpm build --target armv7-linux-androideabi 63 | - host: ubuntu-latest 64 | target: aarch64-unknown-linux-musl 65 | build: pnpm build --target aarch64-unknown-linux-musl -x 66 | - host: windows-latest 67 | target: aarch64-pc-windows-msvc 68 | build: pnpm build --target aarch64-pc-windows-msvc 69 | # - host: ubuntu-latest 70 | # target: wasm32-wasip1-threads 71 | # build: pnpm build --target wasm32-wasip1-threads 72 | name: stable - ${{ matrix.settings.target }} - node@20 73 | runs-on: ${{ matrix.settings.host }} 74 | steps: 75 | - uses: actions/checkout@v4 76 | - name: setup pnpm 77 | uses: pnpm/action-setup@v4 78 | - name: Setup node 79 | uses: actions/setup-node@v4 80 | with: 81 | node-version: 20 82 | cache: pnpm 83 | - name: Install 84 | uses: dtolnay/rust-toolchain@stable 85 | with: 86 | toolchain: stable 87 | targets: ${{ matrix.settings.target }} 88 | - name: Cache cargo 89 | uses: actions/cache@v4 90 | with: 91 | path: | 92 | ~/.cargo/registry/index/ 93 | ~/.cargo/registry/cache/ 94 | ~/.cargo/git/db/ 95 | ~/.napi-rs 96 | .cargo-cache 97 | target/ 98 | key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} 99 | - uses: goto-bus-stop/setup-zig@v2 100 | if: ${{ contains(matrix.settings.target, 'musl') }} 101 | with: 102 | version: 0.13.0 103 | - name: Setup toolchain 104 | run: ${{ matrix.settings.setup }} 105 | if: ${{ matrix.settings.setup }} 106 | shell: bash 107 | - name: Install dependencies 108 | run: pnpm install 109 | - name: Setup node x86 110 | uses: actions/setup-node@v4 111 | if: matrix.settings.target == 'i686-pc-windows-msvc' 112 | with: 113 | node-version: 20 114 | cache: pnpm 115 | architecture: x86 116 | - name: Build 117 | run: ${{ matrix.settings.build }} 118 | shell: bash 119 | - name: Upload artifact 120 | uses: actions/upload-artifact@v4 121 | with: 122 | name: bindings-${{ matrix.settings.target }} 123 | path: | 124 | ./*.node 125 | ./*.wasm 126 | if-no-files-found: error 127 | test-macOS-windows-binding: 128 | name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} 129 | needs: 130 | - build 131 | strategy: 132 | fail-fast: false 133 | matrix: 134 | settings: 135 | - host: windows-latest 136 | target: x86_64-pc-windows-msvc 137 | architecture: x64 138 | - host: macos-latest 139 | target: x86_64-apple-darwin 140 | architecture: x64 141 | - host: macos-latest 142 | target: aarch64-apple-darwin 143 | architecture: arm64 144 | node: 145 | - '18' 146 | - '20' 147 | runs-on: ${{ matrix.settings.host }} 148 | steps: 149 | - uses: actions/checkout@v4 150 | - name: setup pnpm 151 | uses: pnpm/action-setup@v4 152 | - name: Setup node 153 | uses: actions/setup-node@v4 154 | with: 155 | node-version: ${{ matrix.node }} 156 | cache: pnpm 157 | architecture: ${{ matrix.settings.architecture }} 158 | - name: Install dependencies 159 | run: pnpm install 160 | - name: Download artifacts 161 | uses: actions/download-artifact@v4 162 | with: 163 | name: bindings-${{ matrix.settings.target }} 164 | path: . 165 | - name: List packages 166 | run: ls -R . 167 | shell: bash 168 | - name: Test bindings 169 | run: pnpm test 170 | test-linux-binding: 171 | name: Test ${{ matrix.target }} - node@${{ matrix.node }} 172 | needs: 173 | - build 174 | strategy: 175 | fail-fast: false 176 | matrix: 177 | target: 178 | - x86_64-unknown-linux-gnu 179 | - x86_64-unknown-linux-musl 180 | - aarch64-unknown-linux-gnu 181 | - aarch64-unknown-linux-musl 182 | - armv7-unknown-linux-gnueabihf 183 | node: 184 | - '18' 185 | - '20' 186 | runs-on: ubuntu-latest 187 | steps: 188 | - uses: actions/checkout@v4 189 | - name: setup pnpm 190 | uses: pnpm/action-setup@v4 191 | - name: Setup node 192 | uses: actions/setup-node@v4 193 | with: 194 | node-version: ${{ matrix.node }} 195 | cache: pnpm 196 | - name: Output docker params 197 | id: docker 198 | run: | 199 | node -e " 200 | if ('${{ matrix.target }}'.startsWith('aarch64')) { 201 | console.log('PLATFORM=linux/arm64') 202 | } else if ('${{ matrix.target }}'.startsWith('armv7')) { 203 | console.log('PLATFORM=linux/arm/v7') 204 | } else { 205 | console.log('PLATFORM=linux/amd64') 206 | } 207 | " >> $GITHUB_OUTPUT 208 | node -e " 209 | if ('${{ matrix.target }}'.endsWith('-musl')) { 210 | console.log('IMAGE=node:${{ matrix.node }}-alpine') 211 | } else { 212 | console.log('IMAGE=node:${{ matrix.node }}-slim') 213 | } 214 | " >> $GITHUB_OUTPUT 215 | echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT 216 | # use --force to download the all platform/arch dependencies 217 | - name: Install dependencies 218 | run: pnpm install --force 219 | - name: Download artifacts 220 | uses: actions/download-artifact@v4 221 | with: 222 | name: bindings-${{ matrix.target }} 223 | path: . 224 | - name: List packages 225 | run: ls -R . 226 | shell: bash 227 | - name: Set up QEMU 228 | uses: docker/setup-qemu-action@v3 229 | with: 230 | platforms: all 231 | - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 232 | - name: Test bindings 233 | uses: addnab/docker-run-action@v3 234 | with: 235 | image: ${{ steps.docker.outputs.IMAGE }} 236 | options: -v ${{ steps.docker.outputs.PNPM_STORE_PATH }}:${{ steps.docker.outputs.PNPM_STORE_PATH }} -v ${{ github.workspace }}:${{ github.workspace }} -w ${{ github.workspace }} --platform ${{ steps.docker.outputs.PLATFORM }} 237 | run: corepack enable && npm run test 238 | # test-wasi: 239 | # name: Test WASI target 240 | # if: false 241 | # needs: 242 | # - build 243 | # runs-on: ubuntu-latest 244 | # steps: 245 | # - uses: actions/checkout@v4 246 | # - name: setup pnpm 247 | # uses: pnpm/action-setup@v4 248 | # - name: Setup node 249 | # uses: actions/setup-node@v4 250 | # with: 251 | # node-version: 20 252 | # cache: pnpm 253 | # - name: Install dependencies 254 | # run: pnpm install 255 | # - name: Download artifacts 256 | # uses: actions/download-artifact@v4 257 | # with: 258 | # name: bindings-wasm32-wasip1-threads 259 | # path: ./packages/core 260 | # - name: List packages 261 | # run: ls -R . 262 | # shell: bash 263 | # - name: Test bindings 264 | # run: pnpm test 265 | # env: 266 | # NAPI_RS_FORCE_WASI: 1 267 | publish: 268 | name: Publish 269 | runs-on: ubuntu-latest 270 | needs: 271 | - test-macOS-windows-binding 272 | - test-linux-binding 273 | # - test-wasi 274 | steps: 275 | - uses: actions/checkout@v4 276 | - name: setup pnpm 277 | uses: pnpm/action-setup@v4 278 | - name: Setup node 279 | uses: actions/setup-node@v4 280 | with: 281 | node-version: 20 282 | cache: pnpm 283 | - name: Install dependencies 284 | run: pnpm install 285 | - name: Download all artifacts 286 | uses: actions/download-artifact@v4 287 | with: 288 | path: artifacts 289 | - name: create npm dirs 290 | run: | 291 | pnpm napi create-npm-dirs 292 | pnpm napi artifacts 293 | - name: List packages 294 | run: ls -R ./npm 295 | shell: bash 296 | - name: Publish 297 | run: | 298 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 299 | npm config set provenance true 300 | if git log -1 --pretty=%B | grep "^v\?[0-9]\+\.[0-9]\+\.[0-9]\+$"; 301 | then 302 | pnpm napi pre-publish -t npm 303 | pnpm publish --access public --no-git-check 304 | elif git log -1 --pretty=%B | grep "^v\?[0-9]\+\.[0-9]\+\.[0-9]\+"; 305 | then 306 | pnpm napi pre-publish -t npm 307 | pnpm publish --tag next --access public --no-git-check 308 | else 309 | echo "Not a release, skipping publish" 310 | fi 311 | env: 312 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 313 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/* 2 | !.yarn/patches 3 | !.yarn/plugins 4 | !.yarn/releases 5 | !.yarn/sdks 6 | !.yarn/versions 7 | 8 | # Swap the comments on the following lines if you wish to use zero-installs 9 | # In that case, don't forget to run `yarn config set enableGlobalCache false`! 10 | # Documentation here: https://yarnpkg.com/features/caching#zero-installs 11 | 12 | #!.yarn/cache 13 | .pnp.* 14 | 15 | 16 | ### Created by https://www.gitignore.io 17 | ### Node ### 18 | # Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | lerna-debug.log* 25 | .pnpm-debug.log* 26 | 27 | # Diagnostic reports (https://nodejs.org/api/report.html) 28 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # Directory for instrumented libs generated by jscoverage/JSCover 37 | lib-cov 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage 41 | *.lcov 42 | 43 | # nyc test coverage 44 | .nyc_output 45 | 46 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 47 | .grunt 48 | 49 | # Bower dependency directory (https://bower.io/) 50 | bower_components 51 | 52 | # node-waf configuration 53 | .lock-wscript 54 | 55 | # Compiled binary addons (https://nodejs.org/api/addons.html) 56 | build/Release 57 | 58 | # Dependency directories 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | web_modules/ 64 | 65 | # TypeScript cache 66 | *.tsbuildinfo 67 | 68 | # Optional npm cache directory 69 | .npm 70 | 71 | # Optional eslint cache 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | .stylelintcache 76 | 77 | # Microbundle cache 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | .node_repl_history 85 | 86 | # Output of 'npm pack' 87 | *.tgz 88 | 89 | # Yarn Integrity file 90 | .yarn-integrity 91 | 92 | # dotenv environment variable files 93 | .env 94 | .env.development.local 95 | .env.test.local 96 | .env.production.local 97 | .env.local 98 | 99 | # parcel-bundler cache (https://parceljs.org/) 100 | .cache 101 | .parcel-cache 102 | 103 | # Next.js build output 104 | .next 105 | out 106 | 107 | # Nuxt.js build / generate output 108 | .nuxt 109 | dist 110 | 111 | # Gatsby files 112 | .cache/ 113 | # Comment in the public line in if your project uses Gatsby and not Next.js 114 | # https://nextjs.org/blog/next-9-1#public-directory-support 115 | # public 116 | 117 | # vuepress build output 118 | .vuepress/dist 119 | 120 | # vuepress v2.x temp and cache directory 121 | .temp 122 | .cache 123 | 124 | # Docusaurus cache and generated files 125 | .docusaurus 126 | 127 | # Serverless directories 128 | .serverless/ 129 | 130 | # FuseBox cache 131 | .fusebox/ 132 | 133 | # DynamoDB Local files 134 | .dynamodb/ 135 | 136 | # TernJS port file 137 | .tern-port 138 | 139 | # Stores VSCode versions used for testing VSCode extensions 140 | .vscode-test 141 | 142 | # yarn v2 143 | .yarn/cache 144 | .yarn/unplugged 145 | .yarn/build-state.yml 146 | .yarn/install-state.gz 147 | .pnp.* 148 | 149 | ### Node Patch ### 150 | # Serverless Webpack directories 151 | .webpack/ 152 | 153 | # Optional stylelint cache 154 | .stylelintcache 155 | 156 | # SvelteKit build / generate output 157 | .svelte-kit 158 | 159 | 160 | 161 | ### Created by https://www.gitignore.io 162 | ### macOS ### 163 | # General 164 | .DS_Store 165 | .AppleDouble 166 | .LSOverride 167 | 168 | # Icon must end with two \r 169 | Icon 170 | 171 | # Thumbnails 172 | ._* 173 | 174 | # Files that might appear in the root of a volume 175 | .DocumentRevisions-V100 176 | .fseventsd 177 | .Spotlight-V100 178 | .TemporaryItems 179 | .Trashes 180 | .VolumeIcon.icns 181 | .com.apple.timemachine.donotpresent 182 | 183 | # Directories potentially created on remote AFP share 184 | .AppleDB 185 | .AppleDesktop 186 | Network Trash Folder 187 | Temporary Items 188 | .apdisk 189 | 190 | ### macOS Patch ### 191 | # iCloud generated files 192 | *.icloud 193 | 194 | 195 | 196 | ### Created by https://www.gitignore.io 197 | ### Windows ### 198 | # Windows thumbnail cache files 199 | Thumbs.db 200 | Thumbs.db:encryptable 201 | ehthumbs.db 202 | ehthumbs_vista.db 203 | 204 | # Dump file 205 | *.stackdump 206 | 207 | # Folder config file 208 | [Dd]esktop.ini 209 | 210 | # Recycle Bin used on file shares 211 | $RECYCLE.BIN/ 212 | 213 | # Windows Installer files 214 | *.cab 215 | *.msi 216 | *.msix 217 | *.msm 218 | *.msp 219 | 220 | # Windows shortcuts 221 | *.lnk 222 | 223 | 224 | 225 | ### Created by https://www.gitignore.io 226 | ### Rust ### 227 | # Generated by Cargo 228 | # will have compiled files and executables 229 | debug/ 230 | target/ 231 | 232 | # These are backup files generated by rustfmt 233 | **/*.rs.bk 234 | 235 | # MSVC Windows builds of rustc generate these, which store debugging information 236 | *.pdb 237 | 238 | *.node 239 | *.wasm 240 | pgo-data 241 | npm 242 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .cargo 4 | .github 5 | npm 6 | .eslintrc 7 | .prettierignore 8 | rustfmt.toml 9 | yarn.lock 10 | *.node 11 | .yarn 12 | __test__ 13 | renovate.json 14 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | exclude = ["node_modules/**/*.toml"] 2 | 3 | # https://taplo.tamasfe.dev/configuration/formatter-options.html 4 | [formatting] 5 | align_entries = true 6 | indent_tables = true 7 | reorder_keys = true 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off" }, 15 | { "rule": "format/*", "severity": "off" }, 16 | { "rule": "*-indent", "severity": "off" }, 17 | { "rule": "*-spacing", "severity": "off" }, 18 | { "rule": "*-spaces", "severity": "off" }, 19 | { "rule": "*-order", "severity": "off" }, 20 | { "rule": "*-dangle", "severity": "off" }, 21 | { "rule": "*-newline", "severity": "off" }, 22 | { "rule": "*quotes", "severity": "off" }, 23 | { "rule": "*semi", "severity": "off" } 24 | ], 25 | 26 | // Enable eslint for all supported languages 27 | "eslint.validate": [ 28 | "javascript", 29 | "javascriptreact", 30 | "typescript", 31 | "typescriptreact", 32 | "vue", 33 | "html", 34 | "markdown", 35 | "json", 36 | "jsonc", 37 | "yaml", 38 | "toml", 39 | "xml", 40 | "gql", 41 | "graphql", 42 | "astro", 43 | "css", 44 | "less", 45 | "scss", 46 | "pcss", 47 | "postcss" 48 | ], 49 | // Disable the default formatter, use eslint instead 50 | "prettier.enable": false, 51 | "editor.formatOnSave": false, 52 | 53 | // Auto fix 54 | "editor.codeActionsOnSave": { 55 | "source.fixAll.eslint": "explicit", 56 | "source.organizeImports": "never" 57 | }, 58 | 59 | // Silent the stylistic rules in you IDE, but still auto fix them 60 | "eslint.rules.customizations": [ 61 | { "rule": "style/*", "severity": "off" }, 62 | { "rule": "format/*", "severity": "off" }, 63 | { "rule": "*-indent", "severity": "off" }, 64 | { "rule": "*-spacing", "severity": "off" }, 65 | { "rule": "*-spaces", "severity": "off" }, 66 | { "rule": "*-order", "severity": "off" }, 67 | { "rule": "*-dangle", "severity": "off" }, 68 | { "rule": "*-newline", "severity": "off" }, 69 | { "rule": "*quotes", "severity": "off" }, 70 | { "rule": "*semi", "severity": "off" } 71 | ], 72 | 73 | // Enable eslint for all supported languages 74 | "eslint.validate": [ 75 | "javascript", 76 | "javascriptreact", 77 | "typescript", 78 | "typescriptreact", 79 | "vue", 80 | "html", 81 | "markdown", 82 | "json", 83 | "jsonc", 84 | "yaml", 85 | "toml", 86 | "xml", 87 | "gql", 88 | "graphql", 89 | "astro", 90 | "css", 91 | "less", 92 | "scss", 93 | "pcss", 94 | "postcss" 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "allocator-api2" 31 | version = "0.2.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 34 | 35 | [[package]] 36 | name = "assert-unchecked" 37 | version = "0.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "7330592adf847ee2e3513587b4db2db410a0d751378654e7e993d9adcbe5c795" 40 | 41 | [[package]] 42 | name = "autocfg" 43 | version = "1.3.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 46 | 47 | [[package]] 48 | name = "backtrace" 49 | version = "0.3.73" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 52 | dependencies = [ 53 | "addr2line", 54 | "cc", 55 | "cfg-if", 56 | "libc", 57 | "miniz_oxide", 58 | "object", 59 | "rustc-demangle", 60 | ] 61 | 62 | [[package]] 63 | name = "base64-simd" 64 | version = "0.8.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" 67 | dependencies = [ 68 | "outref", 69 | "vsimd", 70 | ] 71 | 72 | [[package]] 73 | name = "bitflags" 74 | version = "2.6.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 77 | 78 | [[package]] 79 | name = "block-buffer" 80 | version = "0.10.4" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 83 | dependencies = [ 84 | "generic-array", 85 | ] 86 | 87 | [[package]] 88 | name = "bumpalo" 89 | version = "3.16.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 92 | dependencies = [ 93 | "allocator-api2", 94 | ] 95 | 96 | [[package]] 97 | name = "castaway" 98 | version = "0.2.2" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 101 | dependencies = [ 102 | "rustversion", 103 | ] 104 | 105 | [[package]] 106 | name = "cc" 107 | version = "1.0.101" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" 110 | 111 | [[package]] 112 | name = "cfg-if" 113 | version = "1.0.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 116 | 117 | [[package]] 118 | name = "compact_str" 119 | version = "0.7.1" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 122 | dependencies = [ 123 | "castaway", 124 | "cfg-if", 125 | "itoa", 126 | "ryu", 127 | "static_assertions", 128 | ] 129 | 130 | [[package]] 131 | name = "convert_case" 132 | version = "0.6.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" 135 | dependencies = [ 136 | "unicode-segmentation", 137 | ] 138 | 139 | [[package]] 140 | name = "cpufeatures" 141 | version = "0.2.12" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 144 | dependencies = [ 145 | "libc", 146 | ] 147 | 148 | [[package]] 149 | name = "crypto-common" 150 | version = "0.1.6" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 153 | dependencies = [ 154 | "generic-array", 155 | "typenum", 156 | ] 157 | 158 | [[package]] 159 | name = "ctor" 160 | version = "0.2.8" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" 163 | dependencies = [ 164 | "quote", 165 | "syn", 166 | ] 167 | 168 | [[package]] 169 | name = "daachorse" 170 | version = "1.0.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "63b7ef7a4be509357f4804d0a22e830daddb48f19fd604e4ad32ddce04a94c36" 173 | 174 | [[package]] 175 | name = "dashmap" 176 | version = "5.5.3" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" 179 | dependencies = [ 180 | "cfg-if", 181 | "hashbrown", 182 | "lock_api", 183 | "once_cell", 184 | "parking_lot_core", 185 | ] 186 | 187 | [[package]] 188 | name = "digest" 189 | version = "0.10.7" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 192 | dependencies = [ 193 | "block-buffer", 194 | "crypto-common", 195 | ] 196 | 197 | [[package]] 198 | name = "either" 199 | version = "1.13.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 202 | 203 | [[package]] 204 | name = "equivalent" 205 | version = "1.0.1" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 208 | 209 | [[package]] 210 | name = "fixedbitset" 211 | version = "0.4.2" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 214 | 215 | [[package]] 216 | name = "futures" 217 | version = "0.3.30" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 220 | dependencies = [ 221 | "futures-channel", 222 | "futures-core", 223 | "futures-executor", 224 | "futures-io", 225 | "futures-sink", 226 | "futures-task", 227 | "futures-util", 228 | ] 229 | 230 | [[package]] 231 | name = "futures-channel" 232 | version = "0.3.30" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 235 | dependencies = [ 236 | "futures-core", 237 | "futures-sink", 238 | ] 239 | 240 | [[package]] 241 | name = "futures-core" 242 | version = "0.3.30" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 245 | 246 | [[package]] 247 | name = "futures-executor" 248 | version = "0.3.30" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 251 | dependencies = [ 252 | "futures-core", 253 | "futures-task", 254 | "futures-util", 255 | ] 256 | 257 | [[package]] 258 | name = "futures-io" 259 | version = "0.3.30" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 262 | 263 | [[package]] 264 | name = "futures-macro" 265 | version = "0.3.30" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 268 | dependencies = [ 269 | "proc-macro2", 270 | "quote", 271 | "syn", 272 | ] 273 | 274 | [[package]] 275 | name = "futures-sink" 276 | version = "0.3.30" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 279 | 280 | [[package]] 281 | name = "futures-task" 282 | version = "0.3.30" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 285 | 286 | [[package]] 287 | name = "futures-util" 288 | version = "0.3.30" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 291 | dependencies = [ 292 | "futures-channel", 293 | "futures-core", 294 | "futures-io", 295 | "futures-macro", 296 | "futures-sink", 297 | "futures-task", 298 | "memchr", 299 | "pin-project-lite", 300 | "pin-utils", 301 | "slab", 302 | ] 303 | 304 | [[package]] 305 | name = "generic-array" 306 | version = "0.14.7" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 309 | dependencies = [ 310 | "typenum", 311 | "version_check", 312 | ] 313 | 314 | [[package]] 315 | name = "gimli" 316 | version = "0.29.0" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 319 | 320 | [[package]] 321 | name = "hashbrown" 322 | version = "0.14.5" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 325 | 326 | [[package]] 327 | name = "hermit-abi" 328 | version = "0.3.9" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 331 | 332 | [[package]] 333 | name = "hex" 334 | version = "0.4.3" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 337 | 338 | [[package]] 339 | name = "indexmap" 340 | version = "2.2.6" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 343 | dependencies = [ 344 | "equivalent", 345 | "hashbrown", 346 | ] 347 | 348 | [[package]] 349 | name = "itertools" 350 | version = "0.13.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 353 | dependencies = [ 354 | "either", 355 | ] 356 | 357 | [[package]] 358 | name = "itoa" 359 | version = "1.0.11" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 362 | 363 | [[package]] 364 | name = "libc" 365 | version = "0.2.155" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 368 | 369 | [[package]] 370 | name = "libloading" 371 | version = "0.8.4" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" 374 | dependencies = [ 375 | "cfg-if", 376 | "windows-targets", 377 | ] 378 | 379 | [[package]] 380 | name = "lock_api" 381 | version = "0.4.12" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 384 | dependencies = [ 385 | "autocfg", 386 | "scopeguard", 387 | ] 388 | 389 | [[package]] 390 | name = "memchr" 391 | version = "2.7.4" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 394 | 395 | [[package]] 396 | name = "miette" 397 | version = "7.2.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" 400 | dependencies = [ 401 | "cfg-if", 402 | "miette-derive", 403 | "owo-colors", 404 | "textwrap", 405 | "thiserror", 406 | "unicode-width", 407 | ] 408 | 409 | [[package]] 410 | name = "miette-derive" 411 | version = "7.2.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" 414 | dependencies = [ 415 | "proc-macro2", 416 | "quote", 417 | "syn", 418 | ] 419 | 420 | [[package]] 421 | name = "miniz_oxide" 422 | version = "0.7.4" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 425 | dependencies = [ 426 | "adler", 427 | ] 428 | 429 | [[package]] 430 | name = "napi" 431 | version = "2.16.8" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "a1bd081bbaef43600fd2c5dd4c525b8ecea7dfdacf40ebc674e87851dce6559e" 434 | dependencies = [ 435 | "bitflags", 436 | "ctor", 437 | "napi-derive", 438 | "napi-sys", 439 | "once_cell", 440 | "tokio", 441 | ] 442 | 443 | [[package]] 444 | name = "napi-build" 445 | version = "2.1.3" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" 448 | 449 | [[package]] 450 | name = "napi-derive" 451 | version = "2.16.6" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "70a8a778fd367b13c64232e58632514b795514ece491ce136d96e976d34a3eb8" 454 | dependencies = [ 455 | "cfg-if", 456 | "convert_case", 457 | "napi-derive-backend", 458 | "proc-macro2", 459 | "quote", 460 | "syn", 461 | ] 462 | 463 | [[package]] 464 | name = "napi-derive-backend" 465 | version = "1.0.68" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "35849e64596ecd467e1ac897153364a1ffd09b1d79b32ebad94ef8980ac73311" 468 | dependencies = [ 469 | "convert_case", 470 | "once_cell", 471 | "proc-macro2", 472 | "quote", 473 | "regex", 474 | "semver", 475 | "syn", 476 | ] 477 | 478 | [[package]] 479 | name = "napi-sys" 480 | version = "2.4.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" 483 | dependencies = [ 484 | "libloading", 485 | ] 486 | 487 | [[package]] 488 | name = "num-bigint" 489 | version = "0.4.6" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 492 | dependencies = [ 493 | "num-integer", 494 | "num-traits", 495 | ] 496 | 497 | [[package]] 498 | name = "num-integer" 499 | version = "0.1.46" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 502 | dependencies = [ 503 | "num-traits", 504 | ] 505 | 506 | [[package]] 507 | name = "num-traits" 508 | version = "0.2.19" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 511 | dependencies = [ 512 | "autocfg", 513 | ] 514 | 515 | [[package]] 516 | name = "num_cpus" 517 | version = "1.16.0" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 520 | dependencies = [ 521 | "hermit-abi", 522 | "libc", 523 | ] 524 | 525 | [[package]] 526 | name = "object" 527 | version = "0.36.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" 530 | dependencies = [ 531 | "memchr", 532 | ] 533 | 534 | [[package]] 535 | name = "once_cell" 536 | version = "1.19.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 539 | 540 | [[package]] 541 | name = "outref" 542 | version = "0.5.1" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" 545 | 546 | [[package]] 547 | name = "owo-colors" 548 | version = "4.0.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" 551 | 552 | [[package]] 553 | name = "oxc_allocator" 554 | version = "0.16.2" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "7dddfd4b859f51fe4b91fafd847e136cc21dfab7cec75513f0427ce4d39f614e" 557 | dependencies = [ 558 | "allocator-api2", 559 | "bumpalo", 560 | ] 561 | 562 | [[package]] 563 | name = "oxc_ast" 564 | version = "0.16.2" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "4011133166ab3e57960f26d42ab56dbe9a3fdd20e279bc54c581ad8b9344d585" 567 | dependencies = [ 568 | "bitflags", 569 | "num-bigint", 570 | "oxc_allocator", 571 | "oxc_ast_macros", 572 | "oxc_span", 573 | "oxc_syntax", 574 | ] 575 | 576 | [[package]] 577 | name = "oxc_ast_macros" 578 | version = "0.16.2" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "2cd2e54fe624598fa53806e3d092942f3b11078d9cc29a470e4b5907acf40f2e" 581 | 582 | [[package]] 583 | name = "oxc_cfg" 584 | version = "0.16.2" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "26cb08113a16bbd659bff818b5f8bb9fcc66446df3d48de59263d2cfc09eee69" 587 | dependencies = [ 588 | "bitflags", 589 | "itertools", 590 | "oxc_syntax", 591 | "petgraph", 592 | "rustc-hash", 593 | ] 594 | 595 | [[package]] 596 | name = "oxc_codegen" 597 | version = "0.16.2" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "89ef42afe89122fb8a51febf41d1892602d0600f12ffde5eed1488b1cad060fc" 600 | dependencies = [ 601 | "bitflags", 602 | "daachorse", 603 | "once_cell", 604 | "oxc_allocator", 605 | "oxc_ast", 606 | "oxc_sourcemap", 607 | "oxc_span", 608 | "oxc_syntax", 609 | "rustc-hash", 610 | ] 611 | 612 | [[package]] 613 | name = "oxc_diagnostics" 614 | version = "0.16.2" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "1e27d66ffd8bdc032d7a2d918e25ee91a6aa5ef7d712d624bac1c77e6b8e6870" 617 | dependencies = [ 618 | "miette", 619 | "owo-colors", 620 | "textwrap", 621 | "unicode-width", 622 | ] 623 | 624 | [[package]] 625 | name = "oxc_index" 626 | version = "0.16.2" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "939fd785d887b25e802ae35fbcc6f6ad3ccf648eeefb98c801dab47047c2d281" 629 | 630 | [[package]] 631 | name = "oxc_parser" 632 | version = "0.16.2" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "30a2874be11c32e1dd214bb5ad7a7ae7bca3597e9fc27e633b463f0d7c59ab6f" 635 | dependencies = [ 636 | "assert-unchecked", 637 | "bitflags", 638 | "memchr", 639 | "num-bigint", 640 | "num-traits", 641 | "oxc_allocator", 642 | "oxc_ast", 643 | "oxc_diagnostics", 644 | "oxc_span", 645 | "oxc_syntax", 646 | "rustc-hash", 647 | "seq-macro", 648 | ] 649 | 650 | [[package]] 651 | name = "oxc_semantic" 652 | version = "0.16.2" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "8fc57199f03ee5705ae99dd87366ec9c9a6cd863f0d59774064d88782a0db677" 655 | dependencies = [ 656 | "indexmap", 657 | "itertools", 658 | "oxc_allocator", 659 | "oxc_ast", 660 | "oxc_cfg", 661 | "oxc_diagnostics", 662 | "oxc_index", 663 | "oxc_span", 664 | "oxc_syntax", 665 | "phf", 666 | "rustc-hash", 667 | ] 668 | 669 | [[package]] 670 | name = "oxc_sourcemap" 671 | version = "0.16.2" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "3cf897f8c79442fd1d26da5864456975bf176adc8ca9c1d608e53916069f9a02" 674 | dependencies = [ 675 | "base64-simd", 676 | "cfg-if", 677 | "rustc-hash", 678 | "serde", 679 | "serde_json", 680 | ] 681 | 682 | [[package]] 683 | name = "oxc_span" 684 | version = "0.16.2" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "d55012e4e027cb6b91bfc9047c248d3464300b96355b432583882dd50fa85cbe" 687 | dependencies = [ 688 | "compact_str", 689 | "miette", 690 | ] 691 | 692 | [[package]] 693 | name = "oxc_syntax" 694 | version = "0.16.2" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "cb47ffa16b13d3e08115653aa72c87a982c8d4d248033cc8b3edf5aeec6aa131" 697 | dependencies = [ 698 | "bitflags", 699 | "dashmap", 700 | "oxc_index", 701 | "oxc_span", 702 | "phf", 703 | "rustc-hash", 704 | "unicode-id-start", 705 | ] 706 | 707 | [[package]] 708 | name = "parking_lot_core" 709 | version = "0.9.10" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 712 | dependencies = [ 713 | "cfg-if", 714 | "libc", 715 | "redox_syscall", 716 | "smallvec", 717 | "windows-targets", 718 | ] 719 | 720 | [[package]] 721 | name = "petgraph" 722 | version = "0.6.5" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" 725 | dependencies = [ 726 | "fixedbitset", 727 | "indexmap", 728 | ] 729 | 730 | [[package]] 731 | name = "phf" 732 | version = "0.11.2" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 735 | dependencies = [ 736 | "phf_macros", 737 | "phf_shared", 738 | ] 739 | 740 | [[package]] 741 | name = "phf_generator" 742 | version = "0.11.2" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 745 | dependencies = [ 746 | "phf_shared", 747 | "rand", 748 | ] 749 | 750 | [[package]] 751 | name = "phf_macros" 752 | version = "0.11.2" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" 755 | dependencies = [ 756 | "phf_generator", 757 | "phf_shared", 758 | "proc-macro2", 759 | "quote", 760 | "syn", 761 | ] 762 | 763 | [[package]] 764 | name = "phf_shared" 765 | version = "0.11.2" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 768 | dependencies = [ 769 | "siphasher", 770 | ] 771 | 772 | [[package]] 773 | name = "pin-project-lite" 774 | version = "0.2.14" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 777 | 778 | [[package]] 779 | name = "pin-utils" 780 | version = "0.1.0" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 783 | 784 | [[package]] 785 | name = "proc-macro2" 786 | version = "1.0.86" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 789 | dependencies = [ 790 | "unicode-ident", 791 | ] 792 | 793 | [[package]] 794 | name = "quote" 795 | version = "1.0.36" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 798 | dependencies = [ 799 | "proc-macro2", 800 | ] 801 | 802 | [[package]] 803 | name = "rand" 804 | version = "0.8.5" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 807 | dependencies = [ 808 | "rand_core", 809 | ] 810 | 811 | [[package]] 812 | name = "rand_core" 813 | version = "0.6.4" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 816 | 817 | [[package]] 818 | name = "react-server-action" 819 | version = "0.0.0" 820 | dependencies = [ 821 | "futures", 822 | "hex", 823 | "napi", 824 | "napi-build", 825 | "napi-derive", 826 | "oxc_allocator", 827 | "oxc_ast", 828 | "oxc_codegen", 829 | "oxc_diagnostics", 830 | "oxc_parser", 831 | "oxc_semantic", 832 | "oxc_span", 833 | "oxc_syntax", 834 | "serde_json", 835 | "sha1", 836 | "tokio", 837 | ] 838 | 839 | [[package]] 840 | name = "redox_syscall" 841 | version = "0.5.2" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" 844 | dependencies = [ 845 | "bitflags", 846 | ] 847 | 848 | [[package]] 849 | name = "regex" 850 | version = "1.10.5" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 853 | dependencies = [ 854 | "aho-corasick", 855 | "memchr", 856 | "regex-automata", 857 | "regex-syntax", 858 | ] 859 | 860 | [[package]] 861 | name = "regex-automata" 862 | version = "0.4.7" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 865 | dependencies = [ 866 | "aho-corasick", 867 | "memchr", 868 | "regex-syntax", 869 | ] 870 | 871 | [[package]] 872 | name = "regex-syntax" 873 | version = "0.8.4" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 876 | 877 | [[package]] 878 | name = "rustc-demangle" 879 | version = "0.1.24" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 882 | 883 | [[package]] 884 | name = "rustc-hash" 885 | version = "2.0.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" 888 | 889 | [[package]] 890 | name = "rustversion" 891 | version = "1.0.17" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 894 | 895 | [[package]] 896 | name = "ryu" 897 | version = "1.0.18" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 900 | 901 | [[package]] 902 | name = "scopeguard" 903 | version = "1.2.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 906 | 907 | [[package]] 908 | name = "semver" 909 | version = "1.0.23" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 912 | 913 | [[package]] 914 | name = "seq-macro" 915 | version = "0.3.5" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" 918 | 919 | [[package]] 920 | name = "serde" 921 | version = "1.0.203" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" 924 | dependencies = [ 925 | "serde_derive", 926 | ] 927 | 928 | [[package]] 929 | name = "serde_derive" 930 | version = "1.0.203" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" 933 | dependencies = [ 934 | "proc-macro2", 935 | "quote", 936 | "syn", 937 | ] 938 | 939 | [[package]] 940 | name = "serde_json" 941 | version = "1.0.119" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "e8eddb61f0697cc3989c5d64b452f5488e2b8a60fd7d5076a3045076ffef8cb0" 944 | dependencies = [ 945 | "itoa", 946 | "ryu", 947 | "serde", 948 | ] 949 | 950 | [[package]] 951 | name = "sha1" 952 | version = "0.10.6" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 955 | dependencies = [ 956 | "cfg-if", 957 | "cpufeatures", 958 | "digest", 959 | ] 960 | 961 | [[package]] 962 | name = "siphasher" 963 | version = "0.3.11" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 966 | 967 | [[package]] 968 | name = "slab" 969 | version = "0.4.9" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 972 | dependencies = [ 973 | "autocfg", 974 | ] 975 | 976 | [[package]] 977 | name = "smallvec" 978 | version = "1.13.2" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 981 | 982 | [[package]] 983 | name = "smawk" 984 | version = "0.3.2" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 987 | 988 | [[package]] 989 | name = "static_assertions" 990 | version = "1.1.0" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 993 | 994 | [[package]] 995 | name = "syn" 996 | version = "2.0.68" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" 999 | dependencies = [ 1000 | "proc-macro2", 1001 | "quote", 1002 | "unicode-ident", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "textwrap" 1007 | version = "0.16.1" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 1010 | dependencies = [ 1011 | "smawk", 1012 | "unicode-linebreak", 1013 | "unicode-width", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "thiserror" 1018 | version = "1.0.61" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" 1021 | dependencies = [ 1022 | "thiserror-impl", 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "thiserror-impl" 1027 | version = "1.0.61" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" 1030 | dependencies = [ 1031 | "proc-macro2", 1032 | "quote", 1033 | "syn", 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "tokio" 1038 | version = "1.38.0" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" 1041 | dependencies = [ 1042 | "backtrace", 1043 | "num_cpus", 1044 | "pin-project-lite", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "typenum" 1049 | version = "1.17.0" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1052 | 1053 | [[package]] 1054 | name = "unicode-id-start" 1055 | version = "1.1.2" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "b8f73150333cb58412db36f2aca8f2875b013049705cc77b94ded70a1ab1f5da" 1058 | 1059 | [[package]] 1060 | name = "unicode-ident" 1061 | version = "1.0.12" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1064 | 1065 | [[package]] 1066 | name = "unicode-linebreak" 1067 | version = "0.1.5" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 1070 | 1071 | [[package]] 1072 | name = "unicode-segmentation" 1073 | version = "1.11.0" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1076 | 1077 | [[package]] 1078 | name = "unicode-width" 1079 | version = "0.1.13" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 1082 | 1083 | [[package]] 1084 | name = "version_check" 1085 | version = "0.9.4" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1088 | 1089 | [[package]] 1090 | name = "vsimd" 1091 | version = "0.8.0" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" 1094 | 1095 | [[package]] 1096 | name = "windows-targets" 1097 | version = "0.52.5" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1100 | dependencies = [ 1101 | "windows_aarch64_gnullvm", 1102 | "windows_aarch64_msvc", 1103 | "windows_i686_gnu", 1104 | "windows_i686_gnullvm", 1105 | "windows_i686_msvc", 1106 | "windows_x86_64_gnu", 1107 | "windows_x86_64_gnullvm", 1108 | "windows_x86_64_msvc", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "windows_aarch64_gnullvm" 1113 | version = "0.52.5" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1116 | 1117 | [[package]] 1118 | name = "windows_aarch64_msvc" 1119 | version = "0.52.5" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1122 | 1123 | [[package]] 1124 | name = "windows_i686_gnu" 1125 | version = "0.52.5" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1128 | 1129 | [[package]] 1130 | name = "windows_i686_gnullvm" 1131 | version = "0.52.5" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1134 | 1135 | [[package]] 1136 | name = "windows_i686_msvc" 1137 | version = "0.52.5" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1140 | 1141 | [[package]] 1142 | name = "windows_x86_64_gnu" 1143 | version = "0.52.5" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1146 | 1147 | [[package]] 1148 | name = "windows_x86_64_gnullvm" 1149 | version = "0.52.5" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1152 | 1153 | [[package]] 1154 | name = "windows_x86_64_msvc" 1155 | version = "0.52.5" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1158 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "react-server-action" 4 | version = "0.0.0" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | napi = { version = "2.16.8", default-features = false, features = ["napi9", "async"] } 11 | napi-derive = "2.16.6" 12 | serde_json = "1.0" 13 | oxc_allocator = { version = "0.16.2" } 14 | oxc_codegen = { version = "0.16.2" } 15 | oxc_span = { version = "0.16.2" } 16 | oxc_ast = { version = "0.16.2" } 17 | oxc_semantic = { version = "0.16.2" } 18 | oxc_syntax = { version = "0.16.2" } 19 | oxc_diagnostics = { version = "0.16.2" } 20 | oxc_parser = { version = "0.16.2" } 21 | futures = "0.3.30" 22 | tokio = { version = "1.38.0", features = ["default", "fs"] } 23 | sha1 = "0.10" 24 | hex = "0.4" 25 | 26 | [build-dependencies] 27 | napi-build = "2.1.3" 28 | 29 | [profile.release] 30 | lto = true 31 | strip = "symbols" 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Server 2 | 3 | ## Usage 4 | 5 | ```shell 6 | npm add react-server-action 7 | yarn add react-server-action 8 | pnpm add react-server-action 9 | ``` 10 | 11 | ```tsx 12 | import { validate, reactServerAction } from 'react-server-action' 13 | 14 | const isServerLayer = true 15 | const result = validate('path/to/file', isServerLayer) 16 | if (!result.error) { 17 | const code = await reactServerAction('path/to/file', '__prefix__', isServerLayer) 18 | console.log(code) 19 | } 20 | ``` 21 | 22 | > Collection of React Server tools 23 | 24 | ## RSC rules 25 | 26 | ### Valid RSC 27 | 28 | ```tsx 29 | 'use server' 30 | 'use client' 31 | // ❌ 32 | ``` 33 | 34 | ```tsx 35 | 'use server' 36 | // ✅ 37 | ``` 38 | 39 | ```tsx 40 | 'use client' 41 | // ✅ 42 | ``` 43 | 44 | ```tsx 45 | 'use client' 46 | export const foo = async () => { 47 | 'use server' 48 | return 'rsc' 49 | } 50 | // ❌ 51 | ``` 52 | 53 | ```tsx 54 | export const foo = async () => { 55 | 'use server' 56 | return 'rsc' 57 | } 58 | // ✅ 59 | ``` 60 | 61 | ### Register RSC 62 | 63 | #### Option 1: export `__PREFIX__+name` in the same file 64 | 65 | ```tsx 66 | import { registerServerReference } from 'react-server-dom-webpack/server' 67 | 68 | export const foo = async () => { 69 | 'use server' 70 | return 'rsc' 71 | } 72 | 73 | export const __prefix__foo = registerServerReference(foo, 'file_id', 'foo') 74 | ``` 75 | 76 | #### Option 2: register in local map 77 | 78 | ```tsx 79 | import { registerServerReference } from 'react-server-dom-webpack/server' 80 | 81 | export const rscMap = new Map() 82 | 83 | function register ( 84 | fn: Function, 85 | file: string, 86 | name: string, 87 | ) { 88 | registerServerReference(fn, file, name) 89 | rscMap.set(id, fn) 90 | return fn 91 | } 92 | 93 | export const foo = async () => { 94 | 'use server' 95 | return 'rsc' 96 | } 97 | 98 | register(foo, 'file_id', 'foo') 99 | ``` 100 | 101 | ### Transform RSC 102 | 103 | #### Case 1: server action file 104 | 105 | ```ts 106 | 'use server' 107 | 108 | export async function foo () { 109 | return 'rsc' 110 | } 111 | 112 | export function not_rsc () { 113 | return 'not_rsc' 114 | } 115 | ``` 116 | 117 | ⬇️ 118 | 119 | ```ts 120 | 'use server' 121 | import { registerServerReference } from 'react-server-dom-webpack/server' 122 | 123 | export async function foo () { 124 | return 'rsc' 125 | } 126 | 127 | export function not_rsc () { 128 | return 'not_rsc' 129 | } 130 | 131 | export const __prefix__foo = registerServerReference(foo, 'file_id', 'foo') 132 | ``` 133 | 134 | #### Case 2: non-exported server action 135 | 136 | ```tsx 137 | async function wrapFn (...args: any[]) { 138 | 'use server' 139 | } 140 | 141 | export const App = () => { 142 | wrapFn() 143 | return
App
144 | } 145 | ``` 146 | 147 | ⬇️ 148 | 149 | ```tsx 150 | import { registerServerReference } from 'react-server-dom-webpack/server' 151 | 152 | async function wrapFn (...args: any[]) { 153 | 'use server' 154 | } 155 | 156 | export const App = () => { 157 | wrapFn() 158 | return
App
159 | } 160 | 161 | export const __prefix__wrapFn = registerServerReference(wrapFn, 'file_id', 'wrapFn') 162 | ``` 163 | 164 | Note that all server actions should be exported, otherwise server runtime cannot access them. 165 | 166 | #### Case 3: server action in nested function 167 | 168 | ```tsx 169 | import { Component } from '@/components' 170 | 171 | export const App = (props) => { 172 | const foo = async (input: string) => { 173 | 'use server' 174 | return JSON.stringify(props) + input; 175 | } 176 | return 177 | } 178 | ``` 179 | 180 | ⬇️ 181 | 182 | ```tsx 183 | import { registerServerReference } from 'react-server-dom-webpack/server' 184 | import { Component } from '@/components' 185 | 186 | export const App = (props) => { 187 | const foo = __prefix__1.bind(null, [props]) 188 | return 189 | } 190 | 191 | export const __prefix__1 = registerServerReference(async (bound, input) => { 192 | 'use server' 193 | const [props] = bound; 194 | return JSON.stringify(props) + input; 195 | }, 'file_id', '__prefix__1') 196 | ``` 197 | 198 | You should save the context of the function correctly, otherwise the function will not work as expected. 199 | 200 | #### Case 4: server action in jsx 201 | 202 | ```tsx 203 | export const App = (props) => { 204 | return
{ 205 | 'use server' 206 | console.log(props) 207 | }} /> 208 | } 209 | ``` 210 | 211 | ⬇️ 212 | 213 | ```tsx 214 | import { registerServerReference } from 'react-server-dom-webpack/server' 215 | 216 | export const App = (props) => { 217 | return
218 | } 219 | 220 | export const __prefix__action_1 = registerServerReference(async (bound) => { 221 | 'use server' 222 | const [props] = bound 223 | console.log(props) 224 | }, 'file_id', '__prefix__action_1') 225 | ``` 226 | 227 | ## LICENSE 228 | 229 | MIT 230 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate napi_build; 2 | 3 | fn main() { 4 | napi_build::setup(); 5 | } 6 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /* auto-generated by NAPI-RS */ 2 | /* eslint-disable */ 3 | export interface Comment { 4 | start: number 5 | end: number 6 | text: string 7 | } 8 | 9 | export const enum FileType { 10 | Client = 'Client', 11 | Server = 'Server', 12 | Isomorphic = 'Isomorphic' 13 | } 14 | 15 | export interface ModuleImports { 16 | name: string 17 | } 18 | 19 | export function reactServerAction(filePath: string, actionExportPrefix: string, isServerLayer: boolean): Promise 20 | 21 | export const enum RSCError { 22 | CannotUseBothClientAndServer = 0, 23 | ServerActionMustBeAsync = 1 24 | } 25 | 26 | /** 27 | * 28 | * Validate a file is a valid Server file or Client File 29 | * 30 | * @param code: string - the code to validate 31 | * 32 | * @param file_path: string - the path to the file 33 | * 34 | * @param is_server_layer: boolean - if the file is in the server layer, enable this in server side rendering 35 | */ 36 | export function validate(code: string, filePath: string, isServerLayer: boolean): ValidateResult 37 | 38 | export interface ValidateResult { 39 | fileType: FileType 40 | isServerAction: boolean 41 | error?: RSCError 42 | imports: Array 43 | } 44 | 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | /* eslint-disable */ 3 | /* auto-generated by NAPI-RS */ 4 | 5 | const { readFileSync } = require('fs') 6 | 7 | let nativeBinding = null 8 | const loadErrors = [] 9 | 10 | const isMusl = () => { 11 | let musl = false 12 | if (process.platform === 'linux') { 13 | musl = isMuslFromFilesystem() 14 | if (musl === null) { 15 | musl = isMuslFromReport() 16 | } 17 | if (musl === null) { 18 | musl = isMuslFromChildProcess() 19 | } 20 | } 21 | return musl 22 | } 23 | 24 | const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') 25 | 26 | const isMuslFromFilesystem = () => { 27 | try { 28 | return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') 29 | } catch { 30 | return null 31 | } 32 | } 33 | 34 | const isMuslFromReport = () => { 35 | const report = typeof process.report.getReport === 'function' ? process.report.getReport() : null 36 | if (!report) { 37 | return null 38 | } 39 | if (report.header && report.header.glibcVersionRuntime) { 40 | return false 41 | } 42 | if (Array.isArray(report.sharedObjects)) { 43 | if (report.sharedObjects.some(isFileMusl)) { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | const isMuslFromChildProcess = () => { 51 | try { 52 | return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') 53 | } catch (e) { 54 | // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false 55 | return false 56 | } 57 | } 58 | 59 | function requireNative() { 60 | if (process.platform === 'android') { 61 | if (process.arch === 'arm64') { 62 | try { 63 | return require('./react-server-action.android-arm64.node') 64 | } catch (e) { 65 | loadErrors.push(e) 66 | } 67 | try { 68 | return require('react-server-action-android-arm64') 69 | } catch (e) { 70 | loadErrors.push(e) 71 | } 72 | 73 | } else if (process.arch === 'arm') { 74 | try { 75 | return require('./react-server-action.android-arm-eabi.node') 76 | } catch (e) { 77 | loadErrors.push(e) 78 | } 79 | try { 80 | return require('react-server-action-android-arm-eabi') 81 | } catch (e) { 82 | loadErrors.push(e) 83 | } 84 | 85 | } else { 86 | loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) 87 | } 88 | } else if (process.platform === 'win32') { 89 | if (process.arch === 'x64') { 90 | try { 91 | return require('./react-server-action.win32-x64-msvc.node') 92 | } catch (e) { 93 | loadErrors.push(e) 94 | } 95 | try { 96 | return require('react-server-action-win32-x64-msvc') 97 | } catch (e) { 98 | loadErrors.push(e) 99 | } 100 | 101 | } else if (process.arch === 'ia32') { 102 | try { 103 | return require('./react-server-action.win32-ia32-msvc.node') 104 | } catch (e) { 105 | loadErrors.push(e) 106 | } 107 | try { 108 | return require('react-server-action-win32-ia32-msvc') 109 | } catch (e) { 110 | loadErrors.push(e) 111 | } 112 | 113 | } else if (process.arch === 'arm64') { 114 | try { 115 | return require('./react-server-action.win32-arm64-msvc.node') 116 | } catch (e) { 117 | loadErrors.push(e) 118 | } 119 | try { 120 | return require('react-server-action-win32-arm64-msvc') 121 | } catch (e) { 122 | loadErrors.push(e) 123 | } 124 | 125 | } else { 126 | loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) 127 | } 128 | } else if (process.platform === 'darwin') { 129 | try { 130 | return require('./react-server-action.darwin-universal.node') 131 | } catch (e) { 132 | loadErrors.push(e) 133 | } 134 | try { 135 | return require('react-server-action-darwin-universal') 136 | } catch (e) { 137 | loadErrors.push(e) 138 | } 139 | 140 | if (process.arch === 'x64') { 141 | try { 142 | return require('./react-server-action.darwin-x64.node') 143 | } catch (e) { 144 | loadErrors.push(e) 145 | } 146 | try { 147 | return require('react-server-action-darwin-x64') 148 | } catch (e) { 149 | loadErrors.push(e) 150 | } 151 | 152 | } else if (process.arch === 'arm64') { 153 | try { 154 | return require('./react-server-action.darwin-arm64.node') 155 | } catch (e) { 156 | loadErrors.push(e) 157 | } 158 | try { 159 | return require('react-server-action-darwin-arm64') 160 | } catch (e) { 161 | loadErrors.push(e) 162 | } 163 | 164 | } else { 165 | loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) 166 | } 167 | } else if (process.platform === 'freebsd') { 168 | if (process.arch === 'x64') { 169 | try { 170 | return require('./react-server-action.freebsd-x64.node') 171 | } catch (e) { 172 | loadErrors.push(e) 173 | } 174 | try { 175 | return require('react-server-action-freebsd-x64') 176 | } catch (e) { 177 | loadErrors.push(e) 178 | } 179 | 180 | } else if (process.arch === 'arm64') { 181 | try { 182 | return require('./react-server-action.freebsd-arm64.node') 183 | } catch (e) { 184 | loadErrors.push(e) 185 | } 186 | try { 187 | return require('react-server-action-freebsd-arm64') 188 | } catch (e) { 189 | loadErrors.push(e) 190 | } 191 | 192 | } else { 193 | loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) 194 | } 195 | } else if (process.platform === 'linux') { 196 | if (process.arch === 'x64') { 197 | if (isMusl()) { 198 | try { 199 | return require('./react-server-action.linux-x64-musl.node') 200 | } catch (e) { 201 | loadErrors.push(e) 202 | } 203 | try { 204 | return require('react-server-action-linux-x64-musl') 205 | } catch (e) { 206 | loadErrors.push(e) 207 | } 208 | 209 | } else { 210 | try { 211 | return require('./react-server-action.linux-x64-gnu.node') 212 | } catch (e) { 213 | loadErrors.push(e) 214 | } 215 | try { 216 | return require('react-server-action-linux-x64-gnu') 217 | } catch (e) { 218 | loadErrors.push(e) 219 | } 220 | 221 | } 222 | } else if (process.arch === 'arm64') { 223 | if (isMusl()) { 224 | try { 225 | return require('./react-server-action.linux-arm64-musl.node') 226 | } catch (e) { 227 | loadErrors.push(e) 228 | } 229 | try { 230 | return require('react-server-action-linux-arm64-musl') 231 | } catch (e) { 232 | loadErrors.push(e) 233 | } 234 | 235 | } else { 236 | try { 237 | return require('./react-server-action.linux-arm64-gnu.node') 238 | } catch (e) { 239 | loadErrors.push(e) 240 | } 241 | try { 242 | return require('react-server-action-linux-arm64-gnu') 243 | } catch (e) { 244 | loadErrors.push(e) 245 | } 246 | 247 | } 248 | } else if (process.arch === 'arm') { 249 | if (isMusl()) { 250 | try { 251 | return require('./react-server-action.linux-arm-musleabihf.node') 252 | } catch (e) { 253 | loadErrors.push(e) 254 | } 255 | try { 256 | return require('react-server-action-linux-arm-musleabihf') 257 | } catch (e) { 258 | loadErrors.push(e) 259 | } 260 | 261 | } else { 262 | try { 263 | return require('./react-server-action.linux-arm-gnueabihf.node') 264 | } catch (e) { 265 | loadErrors.push(e) 266 | } 267 | try { 268 | return require('react-server-action-linux-arm-gnueabihf') 269 | } catch (e) { 270 | loadErrors.push(e) 271 | } 272 | 273 | } 274 | } else if (process.arch === 'riscv64') { 275 | if (isMusl()) { 276 | try { 277 | return require('./react-server-action.linux-riscv64-musl.node') 278 | } catch (e) { 279 | loadErrors.push(e) 280 | } 281 | try { 282 | return require('react-server-action-linux-riscv64-musl') 283 | } catch (e) { 284 | loadErrors.push(e) 285 | } 286 | 287 | } else { 288 | try { 289 | return require('./react-server-action.linux-riscv64-gnu.node') 290 | } catch (e) { 291 | loadErrors.push(e) 292 | } 293 | try { 294 | return require('react-server-action-linux-riscv64-gnu') 295 | } catch (e) { 296 | loadErrors.push(e) 297 | } 298 | 299 | } 300 | } else if (process.arch === 'ppc64') { 301 | try { 302 | return require('./react-server-action.linux-ppc64-gnu.node') 303 | } catch (e) { 304 | loadErrors.push(e) 305 | } 306 | try { 307 | return require('react-server-action-linux-ppc64-gnu') 308 | } catch (e) { 309 | loadErrors.push(e) 310 | } 311 | 312 | } else if (process.arch === 's390x') { 313 | try { 314 | return require('./react-server-action.linux-s390x-gnu.node') 315 | } catch (e) { 316 | loadErrors.push(e) 317 | } 318 | try { 319 | return require('react-server-action-linux-s390x-gnu') 320 | } catch (e) { 321 | loadErrors.push(e) 322 | } 323 | 324 | } else { 325 | loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) 326 | } 327 | } else { 328 | loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) 329 | } 330 | } 331 | 332 | nativeBinding = requireNative() 333 | 334 | if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { 335 | try { 336 | nativeBinding = require('./react-server-action.wasi.cjs') 337 | } catch (err) { 338 | if (process.env.NAPI_RS_FORCE_WASI) { 339 | console.error(err) 340 | } 341 | } 342 | if (!nativeBinding) { 343 | try { 344 | nativeBinding = require('react-server-action-wasm32-wasi') 345 | } catch (err) { 346 | if (process.env.NAPI_RS_FORCE_WASI) { 347 | console.error(err) 348 | } 349 | } 350 | } 351 | } 352 | 353 | if (!nativeBinding) { 354 | if (loadErrors.length > 0) { 355 | // TODO Link to documentation with potential fixes 356 | // - The package owner could build/publish bindings for this arch 357 | // - The user may need to bundle the correct files 358 | // - The user may need to re-install node_modules to get new packages 359 | throw new Error('Failed to load native binding', { cause: loadErrors }) 360 | } 361 | throw new Error(`Failed to load native binding`) 362 | } 363 | 364 | module.exports.FileType = nativeBinding.FileType 365 | module.exports.reactServerAction = nativeBinding.reactServerAction 366 | module.exports.RSCError = nativeBinding.RSCError 367 | module.exports.validate = nativeBinding.validate 368 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-server-action", 3 | "version": "0.2.15", 4 | "main": "index.js", 5 | "types": "index.d.ts", 6 | "files": [ 7 | "index.js", 8 | "index.d.ts", 9 | "esm.mjs", 10 | "register.mjs", 11 | "browser.js" 12 | ], 13 | "napi": { 14 | "binaryName": "react-server-action", 15 | "packageName": "react-server-action", 16 | "targets": [ 17 | "x86_64-apple-darwin", 18 | "aarch64-apple-darwin", 19 | "x86_64-pc-windows-msvc", 20 | "x86_64-unknown-linux-gnu", 21 | "aarch64-linux-android", 22 | "aarch64-unknown-linux-gnu", 23 | "aarch64-unknown-linux-musl", 24 | "aarch64-pc-windows-msvc", 25 | "armv7-unknown-linux-gnueabihf", 26 | "x86_64-unknown-linux-musl", 27 | "i686-pc-windows-msvc", 28 | "armv7-linux-androideabi" 29 | ] 30 | }, 31 | "license": "MIT", 32 | "devDependencies": { 33 | "@napi-rs/cli": "^3.0.0-alpha.55", 34 | "@taplo/cli": "^0.7.0", 35 | "ava": "^6.1.3", 36 | "benny": "^3.7.1", 37 | "chalk": "^5.3.0", 38 | "emnapi": "^1.1.1", 39 | "husky": "^9.0.11", 40 | "lint-staged": "^15.2.2", 41 | "oxlint": "^0.5.1", 42 | "prettier": "^3.3.2", 43 | "tsx": "^4.16.2", 44 | "typescript": "^5.5.2" 45 | }, 46 | "lint-staged": { 47 | "*.@(js|ts|tsx)": [ 48 | "eslint --fix" 49 | ], 50 | "*.@(js|ts|tsx|yml|yaml|md|json)": [ 51 | "prettier --write" 52 | ], 53 | "*.toml": [ 54 | "taplo format" 55 | ] 56 | }, 57 | "ava": { 58 | "require": [ 59 | "tsx" 60 | ], 61 | "extensions": [ 62 | "ts" 63 | ], 64 | "timeout": "2m", 65 | "workerThreads": false, 66 | "environmentVariables": { 67 | "TS_NODE_PROJECT": "./tsconfig.json" 68 | } 69 | }, 70 | "prettier": { 71 | "printWidth": 120, 72 | "semi": false, 73 | "trailingComma": "all", 74 | "singleQuote": true, 75 | "arrowParens": "always" 76 | }, 77 | "scripts": { 78 | "artifacts": "napi artifacts", 79 | "build": "napi build --platform --release", 80 | "build:debug": "napi build --platform", 81 | "test": "ava ./tests", 82 | "universal": "napi universal", 83 | "version": "napi version" 84 | }, 85 | "packageManager": "pnpm@9.4.0", 86 | "repository": { 87 | "url": "https://github.com/himself65/react-server" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | #[macro_use] 4 | extern crate napi_derive; 5 | mod transform; 6 | 7 | pub use transform::{ 8 | validate, 9 | react_server_action 10 | }; 11 | -------------------------------------------------------------------------------- /src/transform/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod validate; 2 | pub mod react_server_action; 3 | -------------------------------------------------------------------------------- /src/transform/react_server_action.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use std::path::Path; 3 | use napi::{Error, Status}; 4 | use oxc_allocator::{Allocator, Box, Vec}; 5 | use oxc_ast::ast; 6 | use oxc_ast::visit::{walk_mut}; 7 | use oxc_ast::{AstBuilder, VisitMut}; 8 | use oxc_semantic::{Semantic, SemanticBuilder}; 9 | use oxc_parser::Parser; 10 | use oxc_codegen::CodeGenerator; 11 | use oxc_span::{Atom, SourceType}; 12 | use sha1::{Digest, Sha1}; 13 | use hex::encode; 14 | use oxc_syntax::scope::{ScopeFlags}; 15 | use tokio::fs; 16 | use crate::validate::{validate_string, ValidateResult}; 17 | 18 | #[napi] 19 | pub async fn react_server_action( 20 | file_path: String, 21 | action_export_prefix: String, 22 | is_server_layer: bool, 23 | ) -> napi::Result { 24 | let path = Path::new(&file_path); 25 | let source_text = String::from_utf8( 26 | fs::read(path) 27 | .await 28 | .map_err(|e| Error::new(Status::Unknown, format!("Failed to read file: {}", e)))? 29 | ).unwrap(); 30 | Ok(react_server_action_impl(source_text, file_path.as_str(), action_export_prefix, is_server_layer)) 31 | } 32 | 33 | fn react_server_action_impl( 34 | source_text: String, 35 | action_export_prefix: &str, 36 | file_path: String, 37 | is_server_layer: bool, 38 | ) -> String { 39 | let path = Path::new(&file_path); 40 | let source_type = SourceType::from_path(path).unwrap(); 41 | let valid_ret = validate_string(&source_text, source_type, is_server_layer).unwrap(); 42 | 43 | let allocator = Allocator::default(); 44 | let ret = Parser::new(&allocator, &source_text, source_type).parse(); 45 | let mut program = ret.program; 46 | let ast = AstBuilder::new(&allocator); 47 | let sem_ret = SemanticBuilder::new(&source_text, source_type) 48 | .with_check_syntax_error(true) 49 | .with_trivias(ret.trivias).build( 50 | ast.copy(&&program), 51 | ); 52 | 53 | let mut rsc = ReactServerAction { 54 | file_name: &file_path, 55 | action_export_prefix, 56 | is_server_layer, 57 | rsc_count: 0, 58 | allocator: &allocator, 59 | validate_result: valid_ret, 60 | allow_emit_export: true, 61 | semantic: sem_ret.semantic, 62 | ast, 63 | names: vec![], 64 | comments: vec![], 65 | new_stat: vec![], 66 | }; 67 | 68 | rsc.visit_program(&mut program); 69 | 70 | for stat in rsc.new_stat { 71 | program.body.push(stat); 72 | } 73 | 74 | CodeGenerator::new().build(&program).source_text 75 | } 76 | 77 | #[napi(object)] 78 | #[derive(Debug)] 79 | struct Comment { 80 | pub start: u32, 81 | pub end: u32, 82 | pub text: String, 83 | } 84 | 85 | struct ReactServerAction<'ast> { 86 | file_name: &'ast String, 87 | 88 | action_export_prefix: &'ast str, 89 | 90 | is_server_layer: bool, 91 | 92 | validate_result: ValidateResult, 93 | 94 | allow_emit_export: bool, 95 | 96 | rsc_count: u32, 97 | names: std::vec::Vec, 98 | 99 | new_stat: std::vec::Vec>, 100 | 101 | ast: AstBuilder<'ast>, 102 | semantic: Semantic<'ast>, 103 | 104 | allocator: &'ast Allocator, 105 | comments: std::vec::Vec, 106 | } 107 | 108 | impl<'ast> ReactServerAction<'ast> { 109 | // slightly different logic with next.js 110 | // next.js uses sha1(file_name#export_name) as id, and store function into a global map, 111 | // but we use id = sha1('file_name')#sha1('start') 112 | // then we can use jump table Map> in the JS side. 113 | fn generate_action_id( 114 | &self, 115 | file_name: &str, 116 | start: u32, 117 | ) -> (&'ast str, &'ast str) { 118 | let mut hasher = Sha1::new(); 119 | hasher.update(file_name.as_bytes()); 120 | let file_name_result = hasher.finalize(); 121 | let mut hasher = Sha1::new(); 122 | hasher.update(start.to_string().as_bytes()); 123 | let fn_id_result = hasher.finalize(); 124 | 125 | ( 126 | oxc_allocator::String::from_str_in( 127 | encode(file_name_result).as_str(), 128 | self.allocator, 129 | ).into_bump_str(), 130 | oxc_allocator::String::from_str_in( 131 | encode(fn_id_result).as_str(), 132 | self.allocator, 133 | ).into_bump_str() 134 | ) 135 | } 136 | } 137 | 138 | impl<'ast> ReactServerAction<'ast> { 139 | fn generate_register_call( 140 | &self, 141 | func_name: &'ast str, 142 | arguments: Vec<'ast, ast::Argument<'ast>>, 143 | ) -> ast::Expression<'ast> { 144 | ast::Expression::CallExpression( 145 | Box::new_in( 146 | ast::CallExpression { 147 | span: Default::default(), 148 | callee: ast::Expression::Identifier( 149 | Box::new_in( 150 | ast::IdentifierReference { 151 | span: Default::default(), 152 | name: Atom::from(func_name), 153 | reference_id: Cell::new(None), 154 | reference_flag: Default::default(), 155 | }, 156 | self.allocator, 157 | ) 158 | ), 159 | arguments, 160 | optional: false, 161 | type_parameters: None, 162 | }, 163 | self.allocator, 164 | ) 165 | ) 166 | } 167 | 168 | fn get_export_id( 169 | &self, 170 | export_id: &'ast str, 171 | ) -> &'ast str { 172 | oxc_allocator::String::from_str_in( 173 | format!("{}{}", self.action_export_prefix, export_id).as_str(), 174 | self.allocator, 175 | ).into_bump_str() 176 | } 177 | 178 | /// const fn_name = createServerReference(id, callServerRSC) 179 | /// callServerRSC is a global variable 180 | fn generate_rsc_reference_call( 181 | &mut self, 182 | fn_name: &'ast str, 183 | id: &'ast str, 184 | ) -> ast::Declaration<'ast> { 185 | let mut arguments = Vec::new_in(self.allocator); 186 | arguments.push( 187 | ast::Argument::StringLiteral( 188 | Box::new_in( 189 | ast::StringLiteral { 190 | span: Default::default(), 191 | value: Atom::from(id), 192 | }, 193 | self.allocator, 194 | ) 195 | )); 196 | arguments.push( 197 | ast::Argument::Identifier( 198 | Box::new_in( 199 | ast::IdentifierReference { 200 | span: Default::default(), 201 | name: Atom::from("callServerRSC"), 202 | reference_id: Cell::new(None), 203 | reference_flag: Default::default(), 204 | }, 205 | self.allocator, 206 | ) 207 | ), 208 | ); 209 | let mut declarations = Vec::new_in(self.allocator); 210 | 211 | declarations.push( 212 | ast::VariableDeclarator { 213 | span: Default::default(), 214 | kind: ast::VariableDeclarationKind::Const, 215 | id: ast::BindingPattern { 216 | kind: ast::BindingPatternKind::BindingIdentifier( 217 | Box::new_in( 218 | ast::BindingIdentifier { 219 | span: Default::default(), 220 | name: Atom::from(fn_name), 221 | symbol_id: Cell::new(None), 222 | }, 223 | self.allocator, 224 | ) 225 | ), 226 | type_annotation: None, 227 | optional: false, 228 | }, 229 | init: Some( 230 | ast::Expression::CallExpression( 231 | Box::new_in( 232 | ast::CallExpression { 233 | span: Default::default(), 234 | callee: ast::Expression::Identifier( 235 | Box::new_in( 236 | ast::IdentifierReference { 237 | span: Default::default(), 238 | name: Atom::from("registerServerReference"), 239 | reference_id: Cell::new(None), 240 | reference_flag: Default::default(), 241 | }, 242 | self.allocator, 243 | ) 244 | ), 245 | arguments, 246 | optional: false, 247 | type_parameters: None, 248 | }, 249 | self.allocator, 250 | ) 251 | ) 252 | ), 253 | definite: false, 254 | } 255 | ); 256 | 257 | ast::Declaration::VariableDeclaration( 258 | Box::new_in( 259 | ast::VariableDeclaration { 260 | span: Default::default(), 261 | kind: ast::VariableDeclarationKind::Const, 262 | declarations, 263 | declare: false, 264 | }, 265 | self.allocator, 266 | ) 267 | ) 268 | } 269 | 270 | /// For server side 271 | /// emit `export const $PREFIX$id = registerServerReference(original_func_id, fileId, actionId)` 272 | /// For client side 273 | /// emit `export const name = registerClientReference(error, fileId, actionId)` 274 | fn emit_rsc_export( 275 | &mut self, 276 | fn_name: &'ast str, 277 | file_id: &'ast str, 278 | export_id: &'ast str, 279 | ) { 280 | let mut arguments: Vec = Vec::new_in(self.allocator); 281 | 282 | if self.validate_result.is_client() && self.is_server_layer { 283 | // func_name = invalid_rsc_call 284 | arguments.push( 285 | ast::Argument::Identifier( 286 | Box::new_in( 287 | ast::IdentifierReference { 288 | span: Default::default(), 289 | name: Atom::from("invalid_rsc_call"), 290 | reference_id: Cell::new(None), 291 | reference_flag: Default::default(), 292 | }, 293 | self.allocator, 294 | ) 295 | ) 296 | ); 297 | } else { 298 | // func_name 299 | arguments.push( 300 | ast::Argument::Identifier( 301 | Box::new_in( 302 | ast::IdentifierReference { 303 | span: Default::default(), 304 | name: Atom::from(fn_name), 305 | reference_id: Cell::new(None), 306 | reference_flag: Default::default(), 307 | }, 308 | self.allocator, 309 | ) 310 | ) 311 | ); 312 | } 313 | // file_id 314 | arguments.push( 315 | ast::Argument::StringLiteral( 316 | Box::new_in( 317 | ast::StringLiteral { 318 | span: Default::default(), 319 | value: Atom::from(file_id), 320 | }, 321 | self.allocator, 322 | ) 323 | ) 324 | ); 325 | // export_id 326 | arguments.push( 327 | ast::Argument::StringLiteral( 328 | Box::new_in( 329 | ast::StringLiteral { 330 | span: Default::default(), 331 | value: Atom::from(export_id), 332 | }, 333 | self.allocator, 334 | ) 335 | ) 336 | ); 337 | 338 | let mut var = Vec::new_in(self.allocator); 339 | let export_id: &str = match self.validate_result.is_client() { 340 | true => fn_name, 341 | false => self.get_export_id(export_id) 342 | }; 343 | self.names.push(String::from(export_id)); 344 | 345 | var.push( 346 | ast::VariableDeclarator { 347 | span: Default::default(), 348 | kind: ast::VariableDeclarationKind::Const, 349 | id: ast::BindingPattern { 350 | kind: ast::BindingPatternKind::BindingIdentifier( 351 | Box::new_in( 352 | ast::BindingIdentifier { 353 | span: Default::default(), 354 | name: Atom::from(export_id), 355 | symbol_id: Cell::new(None), 356 | }, 357 | self.allocator, 358 | ) 359 | ), 360 | type_annotation: None, 361 | optional: false, 362 | }, 363 | init: Some( 364 | self.generate_register_call( 365 | match self.validate_result.is_client() { 366 | true => "registerClientReference", 367 | false => "registerServerReference", 368 | }, 369 | arguments, 370 | ) 371 | ), 372 | definite: false, 373 | } 374 | ); 375 | 376 | // export const ... 377 | self.new_stat.push( 378 | ast::Statement::ExportNamedDeclaration( 379 | Box::new_in( 380 | ast::ExportNamedDeclaration { 381 | span: Default::default(), 382 | declaration: Some( 383 | ast::Declaration::VariableDeclaration( 384 | Box::new_in( 385 | ast::VariableDeclaration { 386 | span: Default::default(), 387 | kind: ast::VariableDeclarationKind::Const, 388 | declarations: var, 389 | declare: false, 390 | }, self.allocator, 391 | ) 392 | ) 393 | ), 394 | specifiers: Vec::new_in(self.allocator), 395 | source: None, 396 | export_kind: ast::ImportOrExportKind::Value, 397 | with_clause: None, 398 | }, self.allocator) 399 | ) 400 | ); 401 | } 402 | 403 | /// emit `export const SERVER_ACTION_ID = registerServerReference(original_func with impl, fileId, actionId)` 404 | /// 405 | /// Return the SERVER_ACTION_ID string 406 | fn emit_anonymous_rsc_export( 407 | &mut self, 408 | original_func: &ast::Function<'ast>, 409 | ) -> &'ast str { 410 | let mut var = Vec::new_in(self.allocator); 411 | let (file_id, fn_id) = self.generate_action_id(self.file_name.as_str(), original_func.span.start); 412 | self.rsc_count += 1; 413 | let mut arguments = Vec::new_in(self.allocator); 414 | 415 | let func: Box> = Box::new_in( 416 | ast::Function::new( 417 | original_func.r#type, 418 | original_func.span, 419 | self.ast.copy(&original_func.id), 420 | original_func.generator, 421 | original_func.r#async, 422 | original_func.declare, 423 | self.ast.copy(&original_func.this_param), 424 | self.ast.copy(&original_func.params), 425 | self.ast.copy(&original_func.body), 426 | self.ast.copy(&original_func.type_parameters), 427 | self.ast.copy(&original_func.return_type), 428 | ), 429 | self.allocator, 430 | ); 431 | // original function 432 | arguments.push( 433 | ast::Argument::FunctionExpression( 434 | func 435 | ) 436 | ); 437 | // file_id 438 | arguments.push( 439 | ast::Argument::StringLiteral( 440 | Box::new_in( 441 | ast::StringLiteral { 442 | span: Default::default(), 443 | value: Atom::from(file_id), 444 | }, 445 | self.allocator, 446 | ) 447 | ) 448 | ); 449 | // export_id 450 | arguments.push( 451 | ast::Argument::StringLiteral( 452 | Box::new_in( 453 | ast::StringLiteral { 454 | span: Default::default(), 455 | value: Atom::from(fn_id), 456 | }, 457 | self.allocator, 458 | ) 459 | ) 460 | ); 461 | 462 | let export_id = self.get_export_id(fn_id); 463 | 464 | var.push( 465 | ast::VariableDeclarator { 466 | span: Default::default(), 467 | kind: ast::VariableDeclarationKind::Const, 468 | id: ast::BindingPattern { 469 | kind: ast::BindingPatternKind::BindingIdentifier( 470 | Box::new_in( 471 | ast::BindingIdentifier { 472 | span: Default::default(), 473 | name: Atom::from(export_id), 474 | symbol_id: Cell::new(None), 475 | }, 476 | self.allocator, 477 | ) 478 | ), 479 | type_annotation: None, 480 | optional: false, 481 | }, 482 | init: Some( 483 | self.generate_register_call("registerServerReference", arguments) 484 | ), 485 | definite: false, 486 | } 487 | ); 488 | // export const SERVER_ACTION_ID = registerServerReference(func, fileId, actionId); 489 | self.new_stat.push( 490 | ast::Statement::ExportNamedDeclaration( 491 | Box::new_in( 492 | ast::ExportNamedDeclaration { 493 | span: Default::default(), 494 | declaration: Some( 495 | ast::Declaration::VariableDeclaration( 496 | Box::new_in( 497 | ast::VariableDeclaration { 498 | span: Default::default(), 499 | kind: ast::VariableDeclarationKind::Const, 500 | declarations: var, 501 | declare: false, 502 | }, self.allocator, 503 | ) 504 | ) 505 | ), 506 | specifiers: Vec::new_in(self.allocator), 507 | source: None, 508 | export_kind: ast::ImportOrExportKind::Value, 509 | with_clause: None, 510 | }, self.allocator, 511 | ) 512 | ) 513 | ); 514 | return export_id; 515 | } 516 | 517 | fn is_server_action( 518 | &mut self, 519 | func: &ast::Function<'ast>, 520 | ) -> bool { 521 | let mut has_use_server = false; 522 | for stmt in &func.body { 523 | for d in &stmt.directives { 524 | if &d.expression.value == "use server" { 525 | has_use_server = true; 526 | } 527 | } 528 | } 529 | 530 | 531 | if !self.is_server_layer { 532 | // client side 533 | func.r#async && has_use_server 534 | } else { 535 | // server side 536 | func.r#async && (self.is_server_layer || has_use_server) 537 | } 538 | } 539 | 540 | fn try_add_function_as_rsc( 541 | &mut self, 542 | func: &mut ast::Function<'ast>, 543 | file_id: &'ast str, 544 | export_id: &'ast str, 545 | ) { 546 | let fn_name = match &func.id { 547 | Some(id) => Some(id.name.as_str()), 548 | None => None, 549 | }; 550 | let is_server_action = self.is_server_action(func); 551 | if self.is_server_layer { 552 | if self.validate_result.is_client() { 553 | // do nothing here 554 | } else if is_server_action { 555 | // convert to `registerServerReference(fn, file_id, export_id);` 556 | if !func.r#async { 557 | return self.comments.push(Comment { 558 | start: func.span.start, 559 | end: func.span.end, 560 | text: "Server actions must be async.".to_string(), 561 | }); 562 | // throw error; 563 | } 564 | match fn_name { 565 | Some(fn_name) => { 566 | if self.allow_emit_export { 567 | self.emit_rsc_export(fn_name, file_id, export_id); 568 | } 569 | } 570 | None => { 571 | // if this function doesn't have a function name, we should not convert it 572 | } 573 | } 574 | } else { 575 | // convert to `registerClientReference(() => { 576 | // throw new Error(); 577 | // }, file_id, export_id);` 578 | // handle it in visit_declaration 579 | } 580 | } else { 581 | // convert to `createServerReference(id, callServerRSC);` 582 | // could not handle here, because Function cannot be converted to CallExpression at this point 583 | // handle it in visit_declaration 584 | } 585 | } 586 | } 587 | 588 | /// Case 1: server import client -> registerClientReference 589 | /// Case 2: server import server -> registerServerReference 590 | /// Case 3: client import server -> createServerReference 591 | /// Case 4: client import client -> as-is 592 | impl<'ast> VisitMut<'ast> for ReactServerAction<'ast> { 593 | fn visit_variable_declarator(&mut self, declarator: &mut ast::VariableDeclarator<'ast>) { 594 | if let Some(expr) = declarator.init.as_mut() { 595 | match expr { 596 | ast::Expression::ArrowFunctionExpression(arrow_func) => { 597 | let arrow_func: Box> = Box::new_in( 598 | ast::ArrowFunctionExpression::new( 599 | arrow_func.span, 600 | arrow_func.expression, 601 | arrow_func.r#async, 602 | self.ast.copy(&arrow_func.params), 603 | self.ast.copy(&arrow_func.body), 604 | self.ast.copy(&arrow_func.type_parameters), 605 | self.ast.copy(&arrow_func.return_type), 606 | ), 607 | self.allocator, 608 | ); 609 | let mut arguments: Vec> = Vec::new_in(self.allocator); 610 | arguments.push( 611 | ast::Argument::ArrowFunctionExpression( 612 | arrow_func 613 | ) 614 | ); 615 | declarator.init = Some(self.generate_register_call("registerServerReference", arguments)); 616 | walk_mut::walk_variable_declarator_mut(self, declarator); 617 | return; 618 | } 619 | ast::Expression::FunctionExpression(func) => { 620 | match func.scope_id.get() { 621 | Some(scope_id) => { 622 | let bindings = self.semantic.scopes().get_bindings( 623 | scope_id 624 | ); 625 | for (name, symbol_id) in bindings.iter() { 626 | println!("name: {}, symbol_id: {:?}", name, symbol_id); 627 | // todo 628 | } 629 | } 630 | None => { 631 | // no scope, no need to bind vars 632 | } 633 | } 634 | 635 | let export_id = self.emit_anonymous_rsc_export(func); 636 | declarator.init = Some(ast::Expression::Identifier( 637 | Box::new_in( 638 | ast::IdentifierReference { 639 | span: Default::default(), 640 | name: Atom::from(export_id), 641 | reference_id: Cell::new(None), 642 | reference_flag: Default::default(), 643 | }, 644 | self.allocator, 645 | ) 646 | )); 647 | } 648 | ast::Expression::ObjectExpression(expr) => { 649 | for kind in expr.properties.iter_mut() { 650 | match kind { 651 | ast::ObjectPropertyKind::ObjectProperty(item) => { 652 | match &item.value { 653 | ast::Expression::FunctionExpression(func) => { 654 | let export_id = self.emit_anonymous_rsc_export(func); 655 | item.value = ast::Expression::Identifier( 656 | Box::new_in( 657 | ast::IdentifierReference { 658 | span: Default::default(), 659 | name: Atom::from(export_id), 660 | reference_id: Cell::new(None), 661 | reference_flag: Default::default(), 662 | }, 663 | self.allocator, 664 | ) 665 | ); 666 | } 667 | ast::Expression::ArrowFunctionExpression(_) => {} 668 | _ => {} 669 | } 670 | } 671 | _ => {} 672 | } 673 | } 674 | walk_mut::walk_variable_declarator_mut(self, declarator); 675 | } 676 | _ => {} 677 | } 678 | } 679 | self.allow_emit_export = false; 680 | walk_mut::walk_variable_declarator_mut(self, declarator); 681 | self.allow_emit_export = true; 682 | } 683 | 684 | fn visit_function(&mut self, func: &mut ast::Function<'ast>, flags: Option) { 685 | // if this function is a top-level function, we should add it to rsc here 686 | let is_top_level_function: bool = match func.scope_id.get() { 687 | None => false, 688 | Some(scope_id) => { 689 | let parent_id = self.semantic.scopes().get_parent_id(scope_id); 690 | match parent_id { 691 | Some(parent_id) => { 692 | self.semantic.scopes().root_scope_id() == parent_id 693 | } 694 | None => false 695 | } 696 | } 697 | }; 698 | if is_top_level_function { 699 | let (file_id, fn_id) = self.generate_action_id(self.file_name.as_str(), func.span.start); 700 | 701 | self.try_add_function_as_rsc( 702 | func, 703 | file_id, 704 | fn_id, 705 | ); 706 | } 707 | walk_mut::walk_function_mut(self, func, flags); 708 | } 709 | 710 | fn visit_expression(&mut self, expr: &mut ast::Expression<'ast>) { 711 | match expr { 712 | ast::Expression::ObjectExpression(obj) => { 713 | for kind in obj.properties.iter_mut() { 714 | match kind { 715 | ast::ObjectPropertyKind::ObjectProperty(obj) => { 716 | match &obj.value { 717 | ast::Expression::FunctionExpression(func) => { 718 | let is_server_action = self.is_server_action(func); 719 | if is_server_action && self.is_server_layer { 720 | let export_id = self.emit_anonymous_rsc_export(func); 721 | obj.value = ast::Expression::Identifier( 722 | Box::new_in( 723 | ast::IdentifierReference { 724 | span: Default::default(), 725 | name: Atom::from(export_id), 726 | reference_id: Cell::new(None), 727 | reference_flag: Default::default(), 728 | }, 729 | self.allocator, 730 | ) 731 | ); 732 | } 733 | } 734 | _ => {} 735 | } 736 | } 737 | _ => {} 738 | } 739 | } 740 | } 741 | _ => {} 742 | } 743 | walk_mut::walk_expression_mut(self, expr); 744 | } 745 | 746 | fn visit_export_default_declaration(&mut self, decl: &mut ast::ExportDefaultDeclaration<'ast>) { 747 | if self.validate_result.is_client() && self.is_server_layer { 748 | // todo: unfinished 749 | // convert to `registerClientReference(fn, file_id, export_id);` 750 | *decl = ast::ExportDefaultDeclaration { 751 | span: Default::default(), 752 | declaration: ast::ExportDefaultDeclarationKind::FunctionDeclaration( 753 | Box::new_in( 754 | ast::Function { 755 | r#type: ast::FunctionType::FunctionDeclaration, 756 | span: Default::default(), 757 | id: Some(ast::BindingIdentifier { 758 | span: Default::default(), 759 | name: "invalid_rsc_call_default".into(), 760 | symbol_id: Cell::new(None), 761 | }), 762 | generator: false, 763 | r#async: false, 764 | declare: false, 765 | this_param: None, 766 | params: Box::new_in( 767 | ast::FormalParameters { 768 | span: Default::default(), 769 | kind: ast::FormalParameterKind::FormalParameter, 770 | items: Vec::new_in(self.allocator), 771 | rest: None, 772 | }, 773 | self.allocator, 774 | ), 775 | type_parameters: None, 776 | return_type: None, 777 | scope_id: Cell::new(None), 778 | body: None, 779 | }, 780 | self.allocator, 781 | ) 782 | ), 783 | exported: ast::ModuleExportName::IdentifierName(ast::IdentifierName { 784 | span: Default::default(), 785 | name: "default".into(), 786 | }), 787 | } 788 | } 789 | } 790 | 791 | fn visit_export_named_declaration(&mut self, export_decl: &mut ast::ExportNamedDeclaration<'ast>) { 792 | if self.validate_result.is_client() && self.is_server_layer { 793 | // convert to `registerClientReference(fn, file_id, export_id);` 794 | let (file_id, fn_id) = self.generate_action_id(self.file_name.as_str(), export_decl.span.start); 795 | if let Some(decl) = &export_decl.declaration { 796 | if let Some(id) = decl.id() { 797 | self.emit_rsc_export(id.name.as_str(), file_id, fn_id); 798 | *export_decl = ast::ExportNamedDeclaration { 799 | span: Default::default(), 800 | declaration: None, 801 | specifiers: Vec::new_in(self.allocator), 802 | source: None, 803 | export_kind: ast::ImportOrExportKind::Value, 804 | with_clause: None, 805 | } 806 | } 807 | } 808 | } 809 | walk_mut::walk_export_named_declaration_mut(self, export_decl); 810 | } 811 | 812 | fn visit_declaration(&mut self, decl: &mut ast::Declaration<'ast>) { 813 | match decl { 814 | ast::Declaration::FunctionDeclaration(func) => { 815 | let is_server_action = self.is_server_action(func); 816 | if !self.is_server_layer & is_server_action { 817 | // convert to `createServerReference(id, callServerRSC);` 818 | match &func.id { 819 | Some(id) => { 820 | let (_, fn_id) = self.generate_action_id(self.file_name.as_str(), func.span.start); 821 | *decl = self.generate_rsc_reference_call( 822 | id.name.as_str(), 823 | self.get_export_id(fn_id), 824 | ); 825 | } 826 | None => {} 827 | } 828 | } else if self.is_server_layer && self.validate_result.is_client() { 829 | // convert to `registerClientReference(fn, file_id, export_id);` 830 | match &func.id { 831 | Some(_) => { 832 | let (_, _) = self.generate_action_id(self.file_name.as_str(), func.span.start); 833 | // self.emit_rsc_export( 834 | // id.name.as_str(), 835 | // self.get_export_id(fn_id), 836 | // fn_id, 837 | // ); 838 | } 839 | None => {} 840 | } 841 | } 842 | } 843 | _ => {} 844 | } 845 | walk_mut::walk_declaration_mut(self, decl); 846 | } 847 | } 848 | 849 | #[cfg(test)] 850 | mod tests { 851 | use super::*; 852 | 853 | fn test_tsx_server_input( 854 | input: &str, 855 | expected_output: &str, 856 | ) { 857 | let parsed_code = react_server_action_impl( 858 | input.to_string(), 859 | "__waku__server__", 860 | TSX_FILE_PATH.into(), 861 | IS_SERVER_LAYER, 862 | ); 863 | assert_eq!(parsed_code, expected_output); 864 | } 865 | 866 | fn test_tsx_client_input( 867 | input: &str, 868 | expected_output: &str, 869 | ) { 870 | let parsed_code = react_server_action_impl( 871 | input.to_string(), 872 | "__waku__server__", 873 | TSX_FILE_PATH.into(), 874 | NOT_SERVER_LAYER, 875 | ); 876 | assert_eq!(parsed_code, expected_output); 877 | } 878 | 879 | fn test_ts_server_input( 880 | input: &str, 881 | expected_output: &str, 882 | ) { 883 | let parsed_code = react_server_action_impl( 884 | input.to_string(), 885 | "__waku__server__", 886 | NORMAL_FILE_PATH.into(), 887 | IS_SERVER_LAYER, 888 | ); 889 | assert_eq!(parsed_code, expected_output); 890 | } 891 | 892 | const NORMAL_FILE_PATH: &str = "./file.ts"; 893 | const TSX_FILE_PATH: &str = "./file.tsx"; 894 | const IS_SERVER_LAYER: bool = true; 895 | const NOT_SERVER_LAYER: bool = false; 896 | 897 | #[test] 898 | fn test_vercel_ai_rsc() { 899 | let input = r#" 900 | const AI = createAI({ 901 | actions: { 902 | foo: async function() { 903 | "use server"; 904 | return 0; 905 | } 906 | } 907 | }); 908 | export { AI }; 909 | "#; 910 | test_ts_server_input(input, r#"const AI = createAI({actions: {foo: __waku__server__92cfceb39d57d914ed8b14d0e37643de0797ae56}}); 911 | export { AI }; 912 | export const __waku__server__92cfceb39d57d914ed8b14d0e37643de0797ae56 = registerServerReference(async function() { 913 | 'use server'; 914 | return 0; 915 | }, '9a024afde04fb48946fa537e9d0b5e8a4bfde606', '92cfceb39d57d914ed8b14d0e37643de0797ae56'); 916 | "#) 917 | } 918 | 919 | #[test] 920 | fn test_client_import_rsc() { 921 | let input = r#" 922 | export async function a() { 923 | "use server"; 924 | return 0; 925 | } 926 | "#; 927 | test_tsx_client_input( 928 | input, r#"export const a = registerServerReference('__waku__server__fe5dbbcea5ce7e2988b8c69bcfdfde8904aabc1f', callServerRSC); 929 | "#, 930 | ); 931 | 932 | test_tsx_server_input(input, r#"export async function a() { 933 | 'use server'; 934 | return 0; 935 | } 936 | export const __waku__server__fe5dbbcea5ce7e2988b8c69bcfdfde8904aabc1f = registerServerReference(a, 'aa08e78087e8703bec46e0df0ebc4c78800fdbaa', 'fe5dbbcea5ce7e2988b8c69bcfdfde8904aabc1f'); 937 | "#); 938 | 939 | // test_tsx_server_input(r#" 940 | // "use client" 941 | // export default async function () { 942 | // return 0; 943 | // }"#, r#""#); 944 | } 945 | 946 | #[test] 947 | fn test_client_import_general_function() { 948 | test_ts_server_input(r#" 949 | "use client" 950 | export async function foo() {}"#, 951 | r#"'use client'; 952 | export {}; 953 | export const foo = registerClientReference(invalid_rsc_call, '9a024afde04fb48946fa537e9d0b5e8a4bfde606', 'fa35e192121eabf3dabf9f5ea6abdbcbc107ac3b'); 954 | "#); 955 | } 956 | 957 | #[test] 958 | fn test_rsc_inside_component() { 959 | // fixme: this is wrong, nested function should be converted to rsc correctly 960 | test_tsx_server_input( 961 | r#" 962 | export default function Home () { 963 | const ctx = getContext() 964 | 965 | async function a () { 966 | "use server"; 967 | return ctx.a 968 | } 969 | 970 | return ( 971 | 972 | ) 973 | } 974 | "#, r#"export default function Home() { 975 | const ctx = getContext(); 976 | async function a() { 977 | 'use server'; 978 | return ctx.a; 979 | } 980 | return ; 981 | }"#, 982 | ); 983 | } 984 | 985 | #[test] 986 | fn test_react_server_action() { 987 | // a "use server" function should be converted correctly 988 | test_ts_server_input( 989 | r#" 990 | async function test() { 991 | "use server" 992 | return 0; 993 | } 994 | "#, r#"async function test() { 995 | 'use server'; 996 | return 0; 997 | } 998 | export const __waku__server__356a192b7913b04c54574d18c28d46e6395428ab = registerServerReference(test, '9a024afde04fb48946fa537e9d0b5e8a4bfde606', '356a192b7913b04c54574d18c28d46e6395428ab'); 999 | "#); 1000 | // if it is not declared as "use server", it should not be converted 1001 | 1002 | test_ts_server_input( 1003 | r#" 1004 | async function test() { 1005 | return 0; 1006 | } 1007 | "#, r#"async function test() { 1008 | return 0; 1009 | } 1010 | export const __waku__server__356a192b7913b04c54574d18c28d46e6395428ab = registerServerReference(test, '9a024afde04fb48946fa537e9d0b5e8a4bfde606', '356a192b7913b04c54574d18c28d46e6395428ab'); 1011 | "#); 1012 | 1013 | // if it's an anonymous function, it should be converted correctly 1014 | test_ts_server_input( 1015 | r#" 1016 | const a = function () { 1017 | "use server" 1018 | return 0; 1019 | } 1020 | "#, r#"const a = __waku__server__17ba0791499db908433b80f37c5fbc89b870084b; 1021 | export const __waku__server__17ba0791499db908433b80f37c5fbc89b870084b = registerServerReference(function() { 1022 | 'use server'; 1023 | return 0; 1024 | }, '9a024afde04fb48946fa537e9d0b5e8a4bfde606', '17ba0791499db908433b80f37c5fbc89b870084b'); 1025 | "#); 1026 | 1027 | // server action in object 1028 | test_ts_server_input( 1029 | r#" 1030 | const a = { 1031 | b: async function() { 1032 | "use server" 1033 | return 0; 1034 | } 1035 | } 1036 | "#, r#"const a = {b: __waku__server__0716d9708d321ffb6a00818614779e779925365c}; 1037 | export const __waku__server__0716d9708d321ffb6a00818614779e779925365c = registerServerReference(async function() { 1038 | 'use server'; 1039 | return 0; 1040 | }, '9a024afde04fb48946fa537e9d0b5e8a4bfde606', '0716d9708d321ffb6a00818614779e779925365c'); 1041 | "#, 1042 | ); 1043 | } 1044 | } -------------------------------------------------------------------------------- /src/transform/validate.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use napi::{Error, Status}; 3 | use oxc_allocator::Allocator; 4 | use oxc_ast::ast; 5 | use oxc_ast::{Visit}; 6 | use oxc_ast::visit::walk; 7 | use oxc_parser::Parser; 8 | use oxc_span::SourceType; 9 | use oxc_syntax::scope::ScopeFlags; 10 | 11 | #[napi(object)] 12 | pub struct ValidateResult { 13 | pub file_type: FileType, 14 | pub is_server_action: bool, 15 | pub error: Option, 16 | pub imports: Vec, 17 | } 18 | 19 | #[napi] 20 | #[derive(Debug, PartialEq)] 21 | pub enum RSCError { 22 | CannotUseBothClientAndServer, 23 | ServerActionMustBeAsync, 24 | } 25 | 26 | #[napi(object)] 27 | #[derive(Debug)] 28 | pub struct ModuleImports { 29 | pub name: String, 30 | } 31 | 32 | impl ValidateResult { 33 | pub(crate) fn is_client(&self) -> bool { 34 | self.file_type == FileType::Client 35 | } 36 | } 37 | 38 | /// 39 | /// Validate a file is a valid Server file or Client File 40 | /// 41 | /// @param code: string - the code to validate 42 | /// 43 | /// @param file_path: string - the path to the file 44 | /// 45 | /// @param is_server_layer: boolean - if the file is in the server layer, enable this in server side rendering 46 | #[napi] 47 | pub fn validate( 48 | code: String, 49 | file_path: String, 50 | is_server_layer: bool, 51 | ) -> napi::Result { 52 | let path = Path::new(&file_path); 53 | let source_type = SourceType::from_path(path).unwrap(); 54 | validate_string(&code, source_type, is_server_layer) 55 | } 56 | 57 | pub fn validate_string( 58 | source_text: &String, 59 | source_type: SourceType, 60 | is_server_layer: bool, 61 | ) -> napi::Result { 62 | let allocator = Allocator::default(); 63 | let ret = Parser::new(&allocator, source_text, source_type).parse(); 64 | 65 | let size = ret.errors.len(); 66 | for error in ret.errors { 67 | let error = error.with_source_code(source_text.clone()); 68 | println!("{error:?}"); 69 | } 70 | 71 | if size > 0 { 72 | return Err(Error::new(Status::Unknown, "Failed to parse file")); 73 | } 74 | 75 | let program = ret.program; 76 | 77 | let mut rsc = ReactServerComponent { 78 | is_server_layer, 79 | 80 | is_server_action: false, 81 | is_client_entry: false, 82 | error: None, 83 | has_use_client: false, 84 | has_use_server: false, 85 | imports: vec![], 86 | has_async_function: false, 87 | }; 88 | 89 | rsc.visit_program(&program); 90 | 91 | if rsc.error.is_none() { 92 | if rsc.has_use_server { 93 | assert_eq!(rsc.has_use_client, false); 94 | } else if rsc.has_use_client { 95 | assert_eq!(rsc.has_use_server, false); 96 | } else { 97 | assert_eq!(rsc.has_use_server, false); 98 | assert_eq!(rsc.has_use_client, false); 99 | } 100 | } 101 | 102 | let result = ValidateResult { 103 | file_type: if rsc.has_use_client { 104 | FileType::Client 105 | } else if rsc.has_use_server { 106 | FileType::Server 107 | } else { 108 | FileType::Isomorphic 109 | }, 110 | is_server_action: rsc.is_server_action, 111 | error: rsc.error, 112 | imports: rsc.imports, 113 | }; 114 | 115 | Ok(result) 116 | } 117 | 118 | #[derive(Debug, PartialEq)] 119 | #[napi(string_enum)] 120 | pub enum FileType { 121 | Client, 122 | Server, 123 | Isomorphic, 124 | } 125 | 126 | #[derive(Debug)] 127 | pub struct ReactServerComponent { 128 | pub is_server_layer: bool, 129 | 130 | // could this file be treated as server action files 131 | pub is_server_action: bool, 132 | // is this file a client entry file 133 | pub is_client_entry: bool, 134 | 135 | pub error: Option, 136 | pub imports: Vec, 137 | 138 | // has "use client" directive on top of the file 139 | pub has_use_client: bool, 140 | // has "use server" directive on top of the file 141 | pub has_use_server: bool, 142 | pub has_async_function: bool, 143 | } 144 | 145 | impl ReactServerComponent { 146 | fn has_server_directive(&self, function_body: &ast::FunctionBody) -> bool { 147 | for d in &function_body.directives { 148 | if &d.expression.value == "use server" { 149 | return true; 150 | } 151 | } 152 | false 153 | } 154 | } 155 | 156 | 157 | impl<'a> Visit<'a> for ReactServerComponent { 158 | fn visit_program(&mut self, program: &ast::Program<'a>) { 159 | program.directives.iter().for_each(|d| { 160 | if &d.expression.value == "use server" { 161 | self.has_use_server = true; 162 | } else if &d.expression.value == "use client" { 163 | self.has_use_client = true; 164 | } 165 | }); 166 | if self.has_use_server && self.has_use_client { 167 | self.error = Some(RSCError::CannotUseBothClientAndServer); 168 | } else { 169 | walk::walk_program(self, program); 170 | if self.has_use_server && self.has_use_client { 171 | self.error = Some(RSCError::CannotUseBothClientAndServer); 172 | } 173 | // final check if this file is a server action file 174 | if !self.has_use_client { 175 | if !self.is_server_action { 176 | if self.has_use_server && self.has_async_function { 177 | self.is_server_action = true; 178 | } else if !self.has_use_server & !self.has_use_client { 179 | self.is_server_action = self.has_async_function && self.is_server_layer; 180 | } 181 | } 182 | } 183 | } 184 | } 185 | 186 | fn visit_function(&mut self, func: &ast::Function<'a>, flags: Option) { 187 | let mut is_server_action = false; 188 | if let Some(body) = &func.body { 189 | is_server_action = self.has_server_directive(body); 190 | } 191 | // check if server action is valid 192 | if !func.r#async && is_server_action { 193 | self.error = Some(RSCError::ServerActionMustBeAsync); 194 | return; 195 | } 196 | 197 | self.has_async_function |= func.r#async; 198 | 199 | walk::walk_function(self, func, flags); 200 | } 201 | 202 | fn visit_arrow_expression(&mut self, func: &ast::ArrowFunctionExpression<'a>) { 203 | let is_server_action = self.has_server_directive(&func.body); 204 | 205 | // check if server action is valid 206 | if !func.r#async && is_server_action { 207 | self.error = Some(RSCError::ServerActionMustBeAsync); 208 | return; 209 | } 210 | 211 | self.has_async_function |= func.r#async; 212 | 213 | walk::walk_arrow_expression(self, func); 214 | } 215 | 216 | fn visit_import_declaration(&mut self, decl: &ast::ImportDeclaration<'a>) { 217 | if decl.source.value == "client-only" { 218 | self.has_use_client = true; 219 | } else if decl.source.value == "server-only" { 220 | self.has_use_server = true; 221 | } 222 | self.imports.push( 223 | ModuleImports { 224 | name: decl.source.value.to_string() 225 | } 226 | ); 227 | walk::walk_import_declaration(self, decl); 228 | } 229 | } 230 | 231 | #[cfg(test)] 232 | mod tests { 233 | use super::*; 234 | 235 | #[test] 236 | fn test_use_server() { 237 | let source_text = r#" 238 | "use server" 239 | "#; 240 | let rsc = validate_string(&source_text.to_string(), SourceType::from_path("test.js").unwrap(), true).unwrap(); 241 | assert_eq!(rsc.is_server_action, false); 242 | assert_eq!(rsc.file_type, FileType::Server); 243 | assert_eq!(rsc.error, None); 244 | } 245 | 246 | #[test] 247 | fn test_use_client() { 248 | let source_text = r#" 249 | "use client" 250 | "#; 251 | let rsc = validate_string(&source_text.to_string(), SourceType::from_path("test.js").unwrap(), true).unwrap(); 252 | assert_eq!(rsc.is_server_action, false); 253 | assert_eq!(rsc.file_type, FileType::Client); 254 | assert_eq!(rsc.error, None); 255 | } 256 | 257 | #[test] 258 | fn test_use_server_and_client() { 259 | let source_text = r#" 260 | "use server" 261 | "use client" 262 | "#; 263 | let rsc = validate_string(&source_text.to_string(), SourceType::from_path("test.js").unwrap(), true).unwrap(); 264 | assert_eq!(rsc.error, Some(RSCError::CannotUseBothClientAndServer)); 265 | } 266 | 267 | #[test] 268 | fn test_use_client_and_server_action() { 269 | let source_text = r#" 270 | "use client" 271 | 272 | async function foo() {} 273 | "#; 274 | let rsc = validate_string(&source_text.to_string(), SourceType::from_path("test.js").unwrap(), true).unwrap(); 275 | assert_eq!(rsc.is_server_action, false); 276 | assert_eq!(rsc.file_type, FileType::Client); 277 | assert_eq!(rsc.error, None); 278 | } 279 | 280 | #[test] 281 | fn test_use_server_and_server_action_without_directive() { 282 | let source_text = r#" 283 | "use server" 284 | 285 | export async function foo() { 286 | // no directive 287 | return 0 288 | } 289 | 290 | export default function () { 291 | return 1 292 | } 293 | "#; 294 | let rsc = validate_string(&source_text.to_string(), SourceType::from_path("test.js").unwrap(), true).unwrap(); 295 | // cannot tell if 'foo' is a server action, depends on if there's a Server component that uses it 296 | assert_eq!(rsc.is_server_action, true); 297 | assert_eq!(rsc.file_type, FileType::Server); 298 | assert_eq!(rsc.error, None); 299 | } 300 | 301 | #[test] 302 | fn test_arrow_function() { 303 | let source_text = r#" 304 | "use server" 305 | 306 | const foo = async () => { 307 | "use server" 308 | } 309 | "#; 310 | let rsc = validate_string(&source_text.to_string(), SourceType::from_path("test.js").unwrap(), true).unwrap(); 311 | assert_eq!(rsc.is_server_action, true); 312 | assert_eq!(rsc.file_type, FileType::Server); 313 | assert_eq!(rsc.error, None); 314 | } 315 | 316 | #[test] 317 | fn client_side_isomorphic() { 318 | let source_text = r#" 319 | export async function foo() { 320 | // unknown if this is a server action 321 | } 322 | "#; 323 | let rsc = validate_string(&source_text.to_string(), SourceType::from_path("test.js").unwrap(), false).unwrap(); 324 | assert_eq!(rsc.is_server_action, false); 325 | assert_eq!(rsc.file_type, FileType::Isomorphic); 326 | assert_eq!(rsc.error, None); 327 | } 328 | 329 | #[test] 330 | fn server_side_isomorphic() { 331 | let source_text = r#" 332 | export async function foo() { 333 | // unknown if this is a server action 334 | } 335 | "#; 336 | let rsc = validate_string(&source_text.to_string(), SourceType::from_path("test.js").unwrap(), true).unwrap(); 337 | assert_eq!(rsc.is_server_action, true); 338 | assert_eq!(rsc.file_type, FileType::Isomorphic); 339 | assert_eq!(rsc.error, None); 340 | } 341 | 342 | #[test] 343 | fn server_side_nothing() { 344 | let source_text = r#" 345 | // do nothing 346 | "#; 347 | let rsc = validate_string(&source_text.to_string(), SourceType::from_path("test.ts").unwrap(), true).unwrap(); 348 | assert_eq!(rsc.is_server_action, false); 349 | assert_eq!(rsc.file_type, FileType::Isomorphic); 350 | assert_eq!(rsc.error, None); 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /tests/index.unit.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { validate, reactServerAction } from '..' 3 | 4 | test('function exist', async (t) => { 5 | t.is(typeof validate, 'function') 6 | t.is(typeof reactServerAction, 'function') 7 | }) 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "moduleResolution": "Bundler" 5 | } 6 | } 7 | --------------------------------------------------------------------------------