├── .changeset ├── README.md ├── config.json └── wicked-dogs-dream.md ├── .commitlintrc.ts ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codecov.yml │ ├── codeql.yml │ ├── continous-release.yml │ ├── release.yml │ ├── snyk-security.yml │ └── sonarqube.yml ├── .gitignore ├── .husky ├── commit-msg └── post-commit ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .run └── All Tests in nemo _ jest.config.ts.run.xml ├── .vscode └── nemo.code-workspace ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── apps └── docs │ ├── .gitignore │ ├── app │ ├── (docs) │ │ └── docs │ │ │ ├── [[...slug]] │ │ │ └── page.tsx │ │ │ └── layout.tsx │ ├── (home) │ │ └── page.tsx │ ├── api │ │ └── search │ │ │ └── route.ts │ ├── docs-og │ │ └── [...slug] │ │ │ └── route.tsx │ ├── global.css │ ├── layout.tsx │ ├── llms.txt │ │ └── route.ts │ ├── opengraph-image.png │ ├── robots.ts │ ├── sitemap.ts │ ├── source.ts │ └── twitter-image.png │ ├── components.json │ ├── components │ ├── code-block.tsx │ ├── copy-button.tsx │ ├── homepage │ │ ├── comparizon.tsx │ │ ├── hero.tsx │ │ └── vercel-oss-program.tsx │ ├── icon.tsx │ ├── overlay.tsx │ └── ui │ │ └── button.tsx │ ├── content │ ├── 1.4 │ │ ├── api-reference │ │ │ └── supabase.mdx │ │ ├── configuration.mdx │ │ ├── context.mdx │ │ ├── conventions │ │ │ ├── functions-naming.mdx │ │ │ ├── meta.json │ │ │ └── project-structure.mdx │ │ ├── forward-functions.mdx │ │ ├── functions.mdx │ │ ├── index.mdx │ │ ├── matcher.mdx │ │ └── meta.json │ └── 2.0 │ │ ├── 3rd-parties │ │ ├── meta.json │ │ ├── next-auth.mdx │ │ └── supabase.mdx │ │ ├── advanced-matching.mdx │ │ ├── best-practices.mdx │ │ ├── configuration.mdx │ │ ├── context.mdx │ │ ├── conventions │ │ ├── functions-naming.mdx │ │ ├── meta.json │ │ └── project-structure.mdx │ │ ├── functions.mdx │ │ ├── index.mdx │ │ ├── matcher.mdx │ │ ├── meta.json │ │ └── nesting.mdx │ ├── eslint.config.mjs │ ├── lib │ ├── metadata.ts │ └── utils.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── source.config.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── turbo.json │ └── types │ └── index.d.ts ├── bun.lock ├── bunfig.toml ├── examples ├── basic │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── src │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ ├── page1 │ │ │ │ └── page.tsx │ │ │ └── page2 │ │ │ │ └── page.tsx │ │ └── middleware.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── cookies │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── src │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── middleware.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── headers │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── src │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── middleware.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json └── next-auth │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── next.svg │ └── vercel.svg │ ├── src │ ├── app │ │ ├── api │ │ │ └── auth │ │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── page1 │ │ │ └── page.tsx │ │ └── page2 │ │ │ └── page.tsx │ ├── auth.ts │ └── middleware.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── package.json ├── packages └── nemo │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ ├── edge-cases.test.ts │ ├── errors.test.ts │ ├── event.test.ts │ ├── index.test.ts │ ├── logger.test.ts │ ├── middleware-chain.test.ts │ ├── middleware-execution-order.test.ts │ ├── nesting.test.ts │ ├── path-matching.test.ts │ ├── storage.test.ts │ ├── usage.test.ts │ └── utils.test.ts │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ ├── errors.ts │ ├── event.ts │ ├── index.ts │ ├── logger.ts │ ├── storage │ │ ├── adapter.ts │ │ ├── adapters │ │ │ └── memory.ts │ │ └── index.ts │ ├── types.ts │ └── utils.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── turbo.json ├── sonar-project.properties └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "access": "public", 4 | "baseBranch": "main", 5 | "changelog": "@changesets/cli/changelog", 6 | "fixed": [], 7 | "ignore": [ 8 | "docs", 9 | "basic-example", 10 | "cookies-example", 11 | "headers-example", 12 | "next-auth-example" 13 | ], 14 | "linked": [], 15 | "updateInternalDependencies": "patch" 16 | } -------------------------------------------------------------------------------- /.changeset/wicked-dogs-dream.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@rescale/nemo": patch 3 | --- 4 | 5 | Updated readme, simplified peer deps versioning 6 | -------------------------------------------------------------------------------- /.commitlintrc.ts: -------------------------------------------------------------------------------- 1 | const Configuration = { 2 | extends: ["@commitlint/config-conventional"], 3 | ignores: [(commit) => commit.includes("Version Packages")], 4 | defaultIgnores: true, 5 | }; 6 | 7 | export default Configuration; 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | analyze: 11 | name: Codecov 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Prepare 20 | uses: oven-sh/setup-bun@v2 21 | 22 | - name: Install Dependencies 23 | run: bun install 24 | 25 | - name: Run Tests 26 | run: bun test 27 | 28 | - name: Upload coverage reports to Codecov 29 | uses: codecov/codecov-action@v4.6.0 30 | with: 31 | fail_ci_if_error: true 32 | directory: ./packages/nemo/coverage 33 | token: ${{ secrets.CODECOV_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | schedule: 9 | - cron: '41 11 * * 6' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze package 14 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 15 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 16 | defaults: 17 | run: 18 | working-directory: packages/nemo 19 | permissions: 20 | security-events: write 21 | 22 | actions: read 23 | contents: read 24 | 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | language: ['javascript-typescript'] 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: ${{ matrix.language }} 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | with: 42 | category: '/language:${{matrix.language}}' 43 | -------------------------------------------------------------------------------- /.github/workflows/continous-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v2 11 | 12 | - name: Prepare 13 | uses: oven-sh/setup-bun@v2 14 | 15 | - name: Install dependencies 16 | run: bun install 17 | 18 | - name: Build 19 | run: bun run build --filter @rescale/nemo 20 | 21 | - run: bunx pkg-pr-new publish "./packages/nemo" 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: write-all 9 | 10 | concurrency: ${{ github.workflow }}-${{ github.ref }} 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@v4 19 | 20 | - name: Prepare 21 | uses: oven-sh/setup-bun@v2 22 | 23 | - name: Install Dependencies 24 | run: bun install 25 | 26 | - name: Create Release Pull Request or Publish to npm 27 | id: changesets 28 | uses: changesets/action@v1 29 | with: 30 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 31 | publish: bun run release 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/snyk-security.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # A sample workflow which sets up Snyk to analyze the full Snyk platform (Snyk Open Source, Snyk Code, 7 | # Snyk Container and Snyk Infrastructure as Code) 8 | # The setup installs the Snyk CLI - for more details on the possible commands 9 | # check https://docs.snyk.io/snyk-cli/cli-reference 10 | # The results of Snyk Code are then uploaded to GitHub Security Code Scanning 11 | # 12 | # In order to use the Snyk Action you will need to have a Snyk API token. 13 | # More details in https://github.com/snyk/actions#getting-your-snyk-token 14 | # or you can signup for free at https://snyk.io/login 15 | # 16 | # For more examples, including how to limit scans to only high-severity issues 17 | # and fail PR checks, see https://github.com/snyk/actions/ 18 | 19 | name: Snyk Security 20 | 21 | on: 22 | push: 23 | branches: ["main" ] 24 | pull_request: 25 | branches: ["main"] 26 | 27 | permissions: 28 | actions: read 29 | contents: read 30 | statuses: read 31 | security-events: write 32 | 33 | jobs: 34 | snyk: 35 | permissions: 36 | contents: read # for actions/checkout to fetch code 37 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 38 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 39 | runs-on: ubuntu-latest 40 | env: 41 | # This is where you will need to introduce the Snyk API token created with your Snyk account 42 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 43 | steps: 44 | - uses: actions/checkout@v3 45 | - uses: actions/setup-node@v3 46 | with: 47 | node-version: 20 48 | - name: Set up Snyk CLI to check for security issues 49 | # Snyk can be used to break the build when it detects security issues. 50 | # In this case we want to upload the SAST issues to GitHub Code Scanning 51 | uses: snyk/actions/setup@master 52 | 53 | # Runs Snyk Code (SAST) analysis and uploads result into GitHub. 54 | # Use || true to not fail the pipeline 55 | - name: Snyk Code test 56 | run: snyk code test -d --sarif > snyk-code.sarif # || true 57 | 58 | # Push the Snyk Code results into GitHub Code Scanning tab 59 | - name: Upload result to GitHub Code Scanning 60 | uses: github/codeql-action/upload-sarif@v2 61 | with: 62 | sarif_file: snyk-code.sarif 63 | -------------------------------------------------------------------------------- /.github/workflows/sonarqube.yml: -------------------------------------------------------------------------------- 1 | name: SonarQube 2 | 3 | on: 4 | push: 5 | branches: 6 | - canary 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | workflow_dispatch: 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | sonarqube: 15 | name: SonarQube 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Prepare 23 | uses: oven-sh/setup-bun@v2 24 | 25 | - name: Install Dependencies 26 | run: bun install 27 | 28 | - name: Run Tests 29 | run: bun test 30 | 31 | - name: SonarQube Scan 32 | uses: SonarSource/sonarqube-scan-action@v5 33 | env: 34 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .turbo 2 | node_modules 3 | dist 4 | .idea 5 | .vscode/* 6 | !.vscode/*.code-workspace 7 | .scannerwork -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | 2 | bunx -- commitlint --edit "$1" 3 | -------------------------------------------------------------------------------- /.husky/post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git update-index -g -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | provenance=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | public/ 4 | *.md 5 | *.mdx 6 | -------------------------------------------------------------------------------- /.run/All Tests in nemo _ jest.config.ts.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.vscode/nemo.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "NEMO", 5 | "path": "../packages/nemo", 6 | }, 7 | { 8 | "name": "examples", 9 | "path": "../examples", 10 | }, 11 | { 12 | "name": "docs", 13 | "path": "../apps/docs", 14 | }, 15 | { 16 | "name": "ROOT", 17 | "path": "../", 18 | }, 19 | ], 20 | "settings": { 21 | "files.exclude": { 22 | "**/.git": true, 23 | "**/.svn": true, 24 | "**/.hg": true, 25 | "**/CVS": true, 26 | "**/.DS_Store": true, 27 | "**/Thumbs.db": true, 28 | "node_modules/": true, 29 | "**/node_modules/": true, 30 | "apps/": true, 31 | "services/": true, 32 | "infrastructure/": true, 33 | "pnpm-lock.yaml": true, 34 | "dist/": true, 35 | ".turbo/": true, 36 | ".next/": true, 37 | "bun.lock": true, 38 | "**/.retool_types/**": true, 39 | "**/*tsconfig.json": true, 40 | ".cache": true, 41 | "retool.config.json": true 42 | }, 43 | "files.autoSave": "afterDelay", 44 | "editor.tabSize": 2, 45 | "editor.formatOnSave": true, 46 | "editor.formatOnPaste": true, 47 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 48 | "editor.codeActionsOnSave": { 49 | "source.fixAll": "explicit", 50 | "source.fixAll.sortJSON": "explicit", 51 | "source.organizeImports": "explicit", 52 | "source.sortImports": "explicit", 53 | "source.removeUnusedImports": "explicit" 54 | }, 55 | "eslint.run": "onSave", 56 | "eslint.format.enable": true, 57 | "eslint.ignoreUntitled": true, 58 | "eslint.useFlatConfig": true, 59 | "eslint.probe": [ 60 | "astro", 61 | "javascript", 62 | "javascriptreact", 63 | "typescript", 64 | "typescriptreact", 65 | "html", 66 | "vue", 67 | "json", 68 | "jsonc", 69 | "mdx", 70 | "md", 71 | "markdown" 72 | ], 73 | "eslint.validate": [ 74 | "javascript", 75 | "javascriptreact", 76 | "typescript", 77 | "typescriptreact", 78 | "html", 79 | "vue", 80 | "json", 81 | "jsonc", 82 | "mdx", 83 | "md", 84 | "markdown" 85 | ], 86 | "[jsonc]": { 87 | "editor.defaultFormatter": "vscode.json-language-features" 88 | }, 89 | "[json]": { 90 | "editor.defaultFormatter": "vscode.json-language-features" 91 | }, 92 | "jest.enable": false, 93 | "typescript.preferences.preferTypeOnlyAutoImports": true, 94 | "[mdx]": { 95 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 96 | "files.trimTrailingWhitespace": false, 97 | "editor.rulers": [ 98 | 100 99 | ], 100 | "editor.wordWrap": "wordWrapColumn", 101 | "editor.wordWrapColumn": 100, 102 | "editor.codeActionsOnSave": { 103 | "source.sortImports": "never", 104 | "source.organizeImports": "never" 105 | } 106 | }, 107 | }, 108 | "extensions": { 109 | "recommendations": [ 110 | "dbaeumer.vscode-eslint", 111 | "SonarSource.sonarlint-vscode", 112 | "bradlc.vscode-tailwindcss", 113 | "Vercel.turbo-vsc", 114 | "joshbolduc.commitlint", 115 | "oven.bun-vscode", 116 | "github.vscode-github-actions", 117 | "GitHub.vscode-pull-request-github", 118 | "GitHub.codespaces", 119 | "folke.vscode-monorepo-workspace", 120 | "formulahendry.auto-close-tag", 121 | "formulahendry.auto-rename-tag", 122 | "mikestead.dotenv", 123 | "GitHub.copilot", 124 | "GitHub.copilot-chat" 125 | ] 126 | }, 127 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mateusz `Z4NR34L` Janota 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/nemo/README.md -------------------------------------------------------------------------------- /apps/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | /node_modules 3 | 4 | # generated content 5 | _map.ts 6 | .contentlayer 7 | 8 | # test & build 9 | /coverage 10 | /.next/ 11 | /out/ 12 | /build 13 | *.tsbuildinfo 14 | 15 | # misc 16 | .DS_Store 17 | *.pem 18 | /.pnp 19 | .pnp.js 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # others 25 | .env*.local 26 | .vercel 27 | next-env.d.ts 28 | .source 29 | -------------------------------------------------------------------------------- /apps/docs/app/(docs)/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { source } from "@/app/source"; 2 | import { metadataImage } from "@/lib/metadata"; 3 | import { Popup, PopupContent, PopupTrigger } from "fumadocs-twoslash/ui"; 4 | import { createTypeTable } from "fumadocs-typescript/ui"; 5 | import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; 6 | import { Callout } from "fumadocs-ui/components/callout"; 7 | import { File, Files, Folder } from "fumadocs-ui/components/files"; 8 | import { Step, Steps } from "fumadocs-ui/components/steps"; 9 | import { Tab, Tabs } from "fumadocs-ui/components/tabs"; 10 | import { TypeTable } from "fumadocs-ui/components/type-table"; 11 | import defaultMdxComponents from "fumadocs-ui/mdx"; 12 | import { 13 | DocsBody, 14 | DocsDescription, 15 | DocsPage, 16 | DocsTitle, 17 | } from "fumadocs-ui/page"; 18 | import { notFound } from "next/navigation"; 19 | import type { ReactElement } from "react"; 20 | 21 | export default async function Page(props: { 22 | params: Promise<{ slug?: string[] }>; 23 | }) { 24 | const params = await props.params; 25 | const page = source.getPage(params.slug); 26 | if (!page) notFound(); 27 | 28 | const { AutoTypeTable } = createTypeTable(); 29 | 30 | const MDX = page.data.body; 31 | 32 | return ( 33 | 41 | {page.data.title} 42 | {page.data.description} 43 | 44 | ( 66 | 67 | {children} 68 | 69 | ), 70 | Popup, 71 | PopupContent, 72 | PopupTrigger, 73 | }} 74 | /> 75 | 76 | 77 | ); 78 | } 79 | 80 | export async function generateStaticParams() { 81 | return source.generateParams(); 82 | } 83 | 84 | export async function generateMetadata(props: { 85 | params: Promise<{ slug?: string[] }>; 86 | }) { 87 | const params = await props.params; 88 | const page = source.getPage(params.slug); 89 | if (!page) notFound(); 90 | 91 | return metadataImage.withImage(page.slugs, { 92 | title: page.data.title, 93 | description: page.data.description, 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /apps/docs/app/(docs)/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { source } from "@/app/source"; 2 | import { Overlay } from "@/components/overlay"; 3 | import { DocsLayout } from "fumadocs-ui/layouts/docs"; 4 | import { BookIcon, HomeIcon } from "lucide-react"; 5 | import { Viewport } from "next"; 6 | import { type ReactNode } from "react"; 7 | 8 | export const viewport: Viewport = { 9 | width: "device-width", 10 | initialScale: 1, 11 | maximumScale: 5, 12 | themeColor: [ 13 | { media: "(prefers-color-scheme: dark)", color: "#171717" }, 14 | { media: "(prefers-color-scheme: light)", color: "#fff" }, 15 | ], 16 | }; 17 | 18 | export default function RootDocsLayout({ children }: { children: ReactNode }) { 19 | return ( 20 | , 24 | }} 25 | links={[ 26 | { 27 | icon: , 28 | text: "Home", 29 | url: "/", 30 | }, 31 | { 32 | icon: , 33 | text: "Docs", 34 | url: "/docs", 35 | active: "nested-url", 36 | }, 37 | ]} 38 | tree={source.pageTree} 39 | > 40 | 41 | {children} 42 | 43 | ); 44 | } 45 | 46 | function HeaderLogo() { 47 | return ( 48 |
49 | ZANREAL logo 54 | 66 | 67 | 68 | NEMO 69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /apps/docs/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from "@/app/source"; 2 | import { createFromSource } from "fumadocs-core/search/server"; 3 | 4 | // it should be cached forever 5 | export const revalidate = false; 6 | 7 | export const { staticGET: GET } = createFromSource(source); 8 | -------------------------------------------------------------------------------- /apps/docs/app/docs-og/[...slug]/route.tsx: -------------------------------------------------------------------------------- 1 | import { generateOGImage } from "fumadocs-ui/og"; 2 | import { metadataImage } from "@/lib/metadata"; 3 | 4 | export const GET = metadataImage.createAPI((page) => { 5 | return generateOGImage({ 6 | title: page.data.title, 7 | description: page.data.description, 8 | site: "Rescale / NEMO", 9 | primaryColor: "rgba(138,44,226,0.75)", 10 | primaryTextColor: "#FFFFFF", 11 | }); 12 | }); 13 | 14 | export function generateStaticParams() { 15 | return metadataImage.generateParams(); 16 | } 17 | -------------------------------------------------------------------------------- /apps/docs/app/global.css: -------------------------------------------------------------------------------- 1 | @import 'fumadocs-twoslash/twoslash.css'; 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | @layer base { 7 | * { 8 | @apply border-border; 9 | } 10 | 11 | body { 12 | @apply bg-background text-foreground; 13 | font-feature-settings: 14 | 'rlig' 1, 15 | 'calt' 1; 16 | } 17 | 18 | .fd-codeblock:not(:hover).shiki.has-focused { 19 | .line:not(.focused) { 20 | span { 21 | @apply blur-[2px]; 22 | } 23 | } 24 | } 25 | 26 | .shiki { 27 | counter-reset: step; 28 | counter-increment: step 0; 29 | pre { 30 | @apply px-0; 31 | } 32 | .line { 33 | @apply border-l-2 border-transparent w-full min-w-full box-border transition-all duration-300 relative; 34 | span { 35 | @apply transition-all duration-300; 36 | } 37 | &::before { 38 | counter-increment: step; 39 | @apply inline-block w-8 px-2 border-transparent text-right text-neutral-600 content-[counter(step)]; 40 | } 41 | &.highlighted, 42 | &.diff { 43 | @apply m-0 !px-0; 44 | &::before { 45 | @apply relative left-auto; 46 | } 47 | } 48 | &.highlighted { 49 | @apply border-neutral-500 bg-neutral-800; 50 | } 51 | &.diff { 52 | &.add { 53 | @apply border-violet-500 bg-violet-500/10 before:text-violet-500; 54 | } 55 | &.remove { 56 | @apply border-red-500 bg-red-500/15 opacity-70 *:!text-neutral-400 before:text-orange-500; 57 | &::before { 58 | content: '-'; 59 | counter-increment: none; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /apps/docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { baseUrl, createMetadata } from "@/lib/metadata"; 2 | import { cn } from "@/lib/utils"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | import { SpeedInsights } from "@vercel/speed-insights/next"; 5 | import { Banner } from "fumadocs-ui/components/banner"; 6 | import { RootProvider } from "fumadocs-ui/provider"; 7 | import { GeistMono } from "geist/font/mono"; 8 | import { GeistSans } from "geist/font/sans"; 9 | import { Viewport } from "next"; 10 | import type { ReactNode } from "react"; 11 | import "./global.css"; 12 | 13 | export const viewport: Viewport = { 14 | width: "device-width", 15 | initialScale: 1, 16 | maximumScale: 5, 17 | themeColor: [ 18 | { media: "(prefers-color-scheme: dark)", color: "#171717" }, 19 | { media: "(prefers-color-scheme: light)", color: "#fff" }, 20 | ], 21 | }; 22 | 23 | export const metadata = createMetadata({ 24 | title: { 25 | template: "%s | NEMO", 26 | default: "NEMO - Next.js Easy Middleware", 27 | }, 28 | description: 29 | "The Next.js library for building clean and performant middlewares.", 30 | metadataBase: baseUrl, 31 | }); 32 | 33 | export default function Layout({ children }: { children: ReactNode }) { 34 | return ( 35 | 44 | 45 | {process.env.NEXT_PUBLIC_VERCEL_ENV !== "production" && ( 46 | 47 | This is `canary` version of documentation. It's still under 48 | construction and review. 49 | 50 | )} 51 | 58 | {children} 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /apps/docs/app/llms.txt/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from "@/app/source"; 2 | import { remark } from "remark"; 3 | 4 | export const revalidate = false; 5 | 6 | export async function GET() { 7 | const pages = source.getPages(); 8 | 9 | const scan = pages.map(async (page) => { 10 | const { lastModified, structuredData, toc, title, description, icon } = 11 | page.data; 12 | const processed = await processContent( 13 | structuredData.contents.map((item) => item.content).join("\n\n"), 14 | ); 15 | 16 | return `file: ${page.file.flattenedPath} 17 | meta: ${JSON.stringify( 18 | { 19 | lastModified, 20 | structuredData, 21 | toc, 22 | title, 23 | description, 24 | icon, 25 | }, 26 | null, 27 | 2, 28 | )} 29 | 30 | ${processed}`; 31 | }); 32 | 33 | const scanned = await Promise.all(scan); 34 | 35 | return new Response(scanned.join("\n\n")); 36 | } 37 | 38 | async function processContent(content: string): Promise { 39 | const file = await remark().process(content); 40 | 41 | return String(file); 42 | } 43 | -------------------------------------------------------------------------------- /apps/docs/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z4nr34l/nemo/6ec7db02ee36af3002502667ffc0227de73ca534/apps/docs/app/opengraph-image.png -------------------------------------------------------------------------------- /apps/docs/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { type MetadataRoute } from "next"; 2 | import { url } from "@/app/sitemap"; 3 | 4 | export default function robots(): MetadataRoute.Robots { 5 | return { 6 | rules: { 7 | userAgent: "*", 8 | allow: "/", 9 | }, 10 | sitemap: url("/sitemap"), 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /apps/docs/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from "next"; 2 | import { baseUrl } from "@/lib/metadata"; 3 | import { source } from "@/app/source"; 4 | 5 | export const url = (path: string): string => new URL(path, baseUrl).toString(); 6 | 7 | export default function sitemap(): MetadataRoute.Sitemap { 8 | return [ 9 | { 10 | url: url("/"), 11 | changeFrequency: "monthly", 12 | priority: 1, 13 | }, 14 | ...source.getPages().map((page) => ({ 15 | url: url(page.url), 16 | lastModified: page.data.lastModified 17 | ? new Date(page.data.lastModified) 18 | : undefined, 19 | changeFrequency: "weekly", 20 | priority: 0.5, 21 | })), 22 | ]; 23 | } 24 | -------------------------------------------------------------------------------- /apps/docs/app/source.ts: -------------------------------------------------------------------------------- 1 | import { docs, meta } from "@/.source"; 2 | import { create } from "@/components/icon"; 3 | import { loader } from "fumadocs-core/source"; 4 | import { createMDXSource } from "fumadocs-mdx"; 5 | import { icons } from "lucide-react"; 6 | 7 | export const source = loader({ 8 | baseUrl: "/docs", 9 | source: createMDXSource(docs, meta), 10 | icon(icon) { 11 | if (icon && icon in icons) 12 | return create({ icon: icons[icon as keyof typeof icons] }); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /apps/docs/app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z4nr34l/nemo/6ec7db02ee36af3002502667ffc0227de73ca534/apps/docs/app/twitter-image.png -------------------------------------------------------------------------------- /apps/docs/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/global.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/docs/components/code-block.tsx: -------------------------------------------------------------------------------- 1 | import * as Base from "fumadocs-ui/components/codeblock"; 2 | import type { HTMLAttributes } from "react"; 3 | import { useMemo } from "react"; 4 | import { createHighlighter } from "shiki"; 5 | 6 | const highlighter = await createHighlighter({ 7 | langs: ["bash", "ts", "tsx", "typescript"], 8 | themes: ["github-light", "github-dark"], 9 | }); 10 | 11 | export type CodeBlockProps = HTMLAttributes & { 12 | code: string; 13 | wrapper?: Base.CodeBlockProps; 14 | lang: "bash" | "ts" | "tsx" | "typescript"; 15 | }; 16 | 17 | export function CodeBlock({ 18 | code, 19 | lang, 20 | wrapper, 21 | ...props 22 | }: CodeBlockProps): React.ReactElement { 23 | const html = useMemo( 24 | () => 25 | highlighter.codeToHtml(code, { 26 | lang, 27 | defaultColor: false, 28 | themes: { 29 | light: "github-light", 30 | dark: "github-dark", 31 | }, 32 | transformers: [ 33 | { 34 | name: "fumadocs:remove-escape", 35 | code(element) { 36 | element.children.forEach((line) => { 37 | if (line.type !== "element") return; 38 | 39 | line.children.forEach((child) => { 40 | if (child.type !== "element") return; 41 | const textNode = child.children[0]; 42 | if (!textNode || textNode.type !== "text") return; 43 | 44 | textNode.value = textNode.value.replace( 45 | /\[\\!code/g, 46 | "[!code" 47 | ); 48 | }); 49 | }); 50 | 51 | return element; 52 | }, 53 | }, 54 | ], 55 | }), 56 | [code, lang] 57 | ); 58 | 59 | return ( 60 | 61 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /apps/docs/components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect, useState } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { cn } from "@/lib/utils"; 6 | import { Check, Copy } from "lucide-react"; 7 | 8 | async function copyToClipboardWithMeta(value: string) { 9 | await navigator.clipboard.writeText(value); 10 | } 11 | 12 | export interface CopyButtonProps { 13 | value: string; 14 | className?: string; 15 | } 16 | 17 | export function CopyButton({ 18 | value, 19 | className, 20 | }: CopyButtonProps): React.ReactNode { 21 | const [hasCopied, setHasCopied] = useState(false); 22 | 23 | useEffect(() => { 24 | setTimeout(() => { 25 | setHasCopied(false); 26 | }, 2000); 27 | }, [hasCopied]); 28 | 29 | return ( 30 | // @ts-ignore 31 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /apps/docs/components/homepage/comparizon.tsx: -------------------------------------------------------------------------------- 1 | import { CodeBlock } from "@/components/code-block"; 2 | 3 | export function Comparizon() { 4 | return ( 5 | <> 6 |
7 |

8 | First, let's let the code get the word out... 9 |

10 |
11 | 12 |
13 | 14 | VS 15 | 16 |
17 |
18 | 19 | before 20 | 21 | , 30 | className: "my-0 rounded-none flex-1 h-full", 31 | }} 32 | /> 33 |
34 |
35 | 36 | after 37 | 38 | , 47 | className: "my-0 rounded-none flex-1 h-full", 48 | }} 49 | /> 50 |
51 |
52 |
53 | 54 |
55 |

56 | Do you feel the difference? 57 |

58 |
59 | 60 | ); 61 | } 62 | 63 | const codeBefore = `import { NextRequest } from 'next/server'; 64 | 65 | export const middleware = async (req: NextRequest) => { 66 | let user = undefined; 67 | let team = undefined; 68 | const token = req.headers.get('token'); 69 | 70 | if(req.nextUrl.pathname.startsWith('/auth')) { 71 | user = await getUserByToken(token); 72 | 73 | if(!user) { 74 | return NextResponse.redirect('/login'); 75 | } 76 | 77 | return NextResponse.next(); 78 | } 79 | 80 | if(req.nextUrl.pathname.startsWith('/team/') || req.nextUrl.pathname.startsWith('/t/')) { 81 | user = await getUserByToken(token); 82 | 83 | if(!user) { 84 | return NextResponse.redirect('/login'); 85 | } 86 | 87 | const slug = req.nextUrl.query.slug; 88 | team = await getTeamBySlug(slug); 89 | 90 | if(!team) { 91 | return NextResponse.redirect('/'); 92 | } 93 | 94 | return NextResponse.next(); 95 | } 96 | 97 | return NextResponse.next(); 98 | } 99 | 100 | export const config = { 101 | matcher: ['/((?!_next/|_static|_vercel|[\\\\w-]+\\\\.\\\\w+).*)'], 102 | };`; 103 | 104 | const codeAfter = `import { createNEMO, type MiddlewareFunctionProps } from '@rescale/nemo'; 105 | import { auth } from '@/app/(auth)/auth/_middleware'; 106 | import { team } from '@/app/(team)/team/_middleware'; 107 | 108 | const globalMiddlewares = { 109 | before: auth, // OR: [auth, ...] 110 | }; 111 | 112 | const middlewares = { 113 | '/auth/:path*': auth, 114 | '/(team|t)/:slug/:path*': team, // OR: [team, ...] 115 | }; 116 | 117 | export const middleware = createNEMO(middlewares, globalMiddlewares); 118 | 119 | export const config = { 120 | matcher: ['/((?!_next/|_static|_vercel|[\\\\w-]+\\\\.\\\\w+).*)'], 121 | };`; 122 | 123 | const TSIcon = () => { 124 | return ( 125 | 126 | 130 | 131 | ); 132 | }; 133 | -------------------------------------------------------------------------------- /apps/docs/components/homepage/vercel-oss-program.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowRight } from "lucide-react"; 2 | import Link from "next/link"; 3 | import { Button } from "../ui/button"; 4 | 5 | export function VercelOssProgram() { 6 | return ( 7 | 8 |
9 |
10 | 16 | 20 | 21 |

OSS Program Member

22 |
23 | 24 | 27 |
28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /apps/docs/components/icon.tsx: -------------------------------------------------------------------------------- 1 | import type { LucideIcon } from "lucide-react"; 2 | import { TerminalIcon } from "lucide-react"; 3 | import { ReactElement } from "react"; 4 | 5 | export function create({ icon: Icon }: { icon?: LucideIcon }): ReactElement { 6 | return ( 7 |
8 | {Icon ? : } 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/docs/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | // @ts-ignore 47 | 52 | ); 53 | } 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/api-reference/supabase.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Supabase 3 | description: NEMO middleware functions for Supabase 4 | icon: SquareFunction 5 | --- 6 | 7 | ## Installation 8 | 9 | Integrate Supabase with your project using the official guides: 10 | 11 | [Supabase Quickstart](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs) or [Supabase Next.js App](https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs) 12 | 13 | We will only make one small change to make it work with NEMO. 14 | 15 | ## Integrate NEMO with Supabase middleware 16 | 17 | 18 | 19 | 20 | ### Create `_middleware.ts`. 21 | 22 | Create a new file in your projects lib or supabase directory called `_middleware.ts`. 23 | 24 | And paste middleware code that you've copied from the Supabase documentation. 25 | 26 | It should look like this: 27 | 28 | ```ts title="_middleware.ts" 29 | import { createServerClient } from '@supabase/ssr' 30 | import { NextResponse } from 'next/server' 31 | 32 | export async function updateSession(request) { 33 | let supabaseResponse = NextResponse.next({ 34 | request, 35 | }) 36 | 37 | const supabase = createServerClient( 38 | process.env.NEXT_PUBLIC_SUPABASE_URL, 39 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 40 | { 41 | cookies: { 42 | getAll() { 43 | return request.cookies.getAll() 44 | }, 45 | setAll(cookiesToSet) { 46 | cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value)) 47 | supabaseResponse = NextResponse.next({ 48 | request, 49 | }) 50 | cookiesToSet.forEach(({ name, value, options }) => 51 | supabaseResponse.cookies.set(name, value, options) 52 | ) 53 | }, 54 | }, 55 | } 56 | ) 57 | 58 | // refreshing the auth token 59 | await supabase.auth.getUser() 60 | 61 | return supabaseResponse 62 | } 63 | ``` 64 | 65 | 66 | 67 | ### Integrate supabase middleware with NEMO 68 | 69 | We need to change params implementation in `updateSession` function to use `MiddlewareFunctionProps` type. 70 | 71 | ```ts title="_middleware.ts" 72 | import { type MiddlewareFunctionProps } from '@rescale/nemo'; // [!code focus] [!code ++] 73 | // prev imports... 74 | 75 | export async function updateSession(request) { // [!code focus] [!code --] 76 | export async function updateSession({ request, forward }: MiddlewareFunctionProps) { // [!code focus] [!code ++] 77 | 78 | // prev code... 79 | 80 | return supabaseResponse // [!code focus] [!code --] 81 | forward(supabaseResponse) // [!code focus] [!code ++] 82 | } 83 | ``` 84 | 85 | 86 | 87 | 88 | ### Replace `middleware.ts` code 89 | 90 | We need to edit primary `middleware.ts` file to use the new middleware function. 91 | 92 | ```typescript title="_middleware.ts" 93 | import { createMiddleware, forward, type MiddlewareFunctionProps } from '@rescale/nemo'; 94 | 95 | const globalMiddlewares = { 96 | before: updateSession, // REMEMBER TO IMPORT updateSession 97 | } 98 | 99 | const middlewares = { 100 | '/': [ 101 | async ({ request }: MiddlewareFunctionProps) => { 102 | console.log('There is NEMO', request.nextUrl.pathname); 103 | }, 104 | ], 105 | }; 106 | 107 | export const middleware = createMiddleware(middlewares, globalMiddlewares); 108 | 109 | export const config = { 110 | matcher: ['/((?!_next/|_static|_vercel|[\\w-]+\\.\\w+).*)'], 111 | }; 112 | ``` 113 | 114 | 115 | 116 | ### (Optional) Add user infomation to context 117 | 118 | To add user information to the context, you can use the following code: 119 | 120 | ```typescript title="_middleware.ts" 121 | // imports 122 | 123 | export async function updateSession({ request, context }: MiddlewareFunctionProps) { // [!code focus] [!code ++] 124 | 125 | // prev code 126 | 127 | // refreshing the auth token 128 | await supabase.auth.getUser() 129 | 130 | // add user to context 131 | context.set('user', user ?? undefined); // [!code focus] [!code ++] 132 | 133 | return supabaseResponse 134 | } 135 | ``` 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/configuration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | description: Package configuration and explanation 4 | icon: Wrench 5 | --- 6 | 7 | import { SquareFunctionIcon, PlayIcon } from "lucide-react"; 8 | 9 | The `createMiddleware` function is the main entry point for the Next Easy Middlewares package. It allows you to create a middleware helper that can be used to define middleware functions for your Next.js application. 10 | 11 | ## Configuration 12 | 13 | This is our basic construction which we will use to create a middleware helper. 14 | 15 | ```ts title="middleware.ts" 16 | const middlewares = { 17 | '/api': async ({ request }: MiddlewareFunctionProps) => { 18 | // middleware functions for /api route 19 | }, 20 | }; 21 | 22 | const globalMiddlewares = { 23 | before: async () => { 24 | // global middleware function that will be executed before any route middleware 25 | }, 26 | after: async () => { 27 | // global middleware function that will be executed after any route middleware 28 | } 29 | } 30 | 31 | export const middleware = createMiddleware(middlewares, globalMiddlewares); 32 | ``` 33 | 34 | ## Path middlewares construction (`middlewares`) 35 | 36 | **Type**: [`Record`](/docs/1.4/functions) 37 | 38 | This property contains a map of middleware functions that will be executed for specific routes. The key is the route path, and the value is the middleware function. 39 | 40 | Let's break down the construction: 41 | 42 | ### Matcher 43 | 44 | **Type**: `string` (`middlewares` object key) 45 | 46 | In this library is used [path-to-regexp](https://github.com/pillarjs/path-to-regexp) package to match the middleware routes path in a same way as Next.js does in config's matcher prop. 47 | 48 | 49 | 50 | ```ts title="middleware.ts" 51 | const middlewares = { 52 | '/api': // middleware functions for /api route // [!code focus] 53 | }; 54 | ``` 55 | 56 | 57 | ```ts title="middleware.ts" 58 | const middlewares = { 59 | '/team/:slug{/*path}': // middleware functions for /team/* route // [!code focus] 60 | }; 61 | ``` 62 | 63 | 64 | 65 | 66 | This library uses latest version of `path-to-regexp` due to DoS vulnerability in previous versions that Next.js uses. Please refer to [path-to-regexp](https://github.com/pillarjs/path-to-regexp?tab=readme-ov-file#errors) for current regex support information. 67 | 68 | 69 | ### Middleware function 70 | 71 | **Type**: [`MiddlewareFunction | MiddlewareFunction[]`](/docs/1.4/functions) 72 | 73 | This is a function that will be executed for the specific route. It can be a single function or an array of functions that will be executed in order. 74 | 75 | 76 | 77 | ```ts title="middleware.ts" 78 | const middlewares = { 79 | '/api': async ({ request }: MiddlewareFunctionProps) => { 80 | // middleware functions for /api route 81 | }, 82 | }; 83 | ``` 84 | 85 | 86 | ```ts title="middleware.ts" 87 | const middlewares = { 88 | '/api': [ 89 | async ({ request, context }: MiddlewareFunctionProps) => { 90 | // middleware functions for /api route 91 | }, 92 | async ({ request, context }: MiddlewareFunctionProps) => { 93 | // middleware functions for /api route 94 | } 95 | ] 96 | }; 97 | ``` 98 | 99 | 100 | 101 | }/> 102 | 103 | ## Global middlewares construction (`globalMiddlewares`) 104 | 105 | **Type**: [`Record<"before" | "after", MiddlewareFunction | MiddlewareFunction[]>`](/docs/1.4/functions) 106 | 107 | This property contains global middleware functions that will be executed before and after any route middleware. 108 | 109 | ### Before 110 | 111 | **Type**: [`MiddlewareFunction | MiddlewareFunction[]`](/docs/1.4/functions) 112 | 113 | This is a global middleware function(s) that will be executed before any route middleware. 114 | 115 | ### After 116 | 117 | **Type**: [`MiddlewareFunction | MiddlewareFunction[]`](/docs/1.4/functions) 118 | 119 | This is a global middleware function(s) that will be executed after any route middleware. 120 | 121 | 122 | 123 | ```ts title="middleware.ts" 124 | const globalMiddlewares = { 125 | // you can also define `after:` here 126 | before: async ({ request }: MiddlewareFunctionProps) => { 127 | // middleware functions for /api route 128 | }, 129 | }; 130 | ``` 131 | 132 | 133 | ```ts title="middleware.ts" 134 | const globalMiddlewares = { 135 | // you can also define `after:` here 136 | before: [ 137 | async ({ request, context }: MiddlewareFunctionProps) => { 138 | // middleware functions for /api route 139 | }, 140 | async ({ request, context }: MiddlewareFunctionProps) => { 141 | // middleware functions for /api route 142 | } 143 | ] 144 | }; 145 | ``` 146 | 147 | 148 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/context.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Shared context 3 | description: Shared context between functions across the execution chain 4 | icon: Waypoints 5 | --- 6 | 7 | 8 | The shared context is a key factor in the middleware functions standardization. It helps to share data between functions, make it easier to maintain, and improve the development experience. A well-structured context can help developers to understand the codebase faster, find the data they need, and make changes more efficiently. 9 | 10 | 11 | It also has a big impact on the performance of the application. By sharing data between functions, you can avoid unnecessary data fetching and processing, which can improve the overall performance of the application. 12 | 13 | 14 | ## Usage 15 | 16 | Below you can see an example of how to use the shared context in your middleware functions. 17 | 18 | 19 | Remember that each request's middleware execution is a separate event instance, so the context is not shared between different requests. 20 | 21 | 22 | ```ts title="_middleware.ts" 23 | async ({ request, response, context, event }: MiddlewareFunctionProps) => { 24 | let user = undefined; // [!code focus] 25 | 26 | if(!context.has('user')) { // [!code focus] 27 | user = await fetchUser(); // [!code focus] 28 | context.set('user', user); // [!code focus] 29 | } else { // [!code focus] 30 | user = context.get('user'); // [!code focus] 31 | } // [!code focus] 32 | 33 | if(!user) { 34 | return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); 35 | } 36 | } 37 | ``` 38 | 39 | ## Explanation 40 | 41 | In this example, we are using the shared context to store the user data. We are checking if the user data is already stored in the context. If not, we are fetching the user data and storing it in the context. Then we are using the user data to set a custom header in the response object. 42 | 43 | By using the shared context, we can avoid fetching the user data multiple times in the same request, which can improve the performance of the application. We can also share the user data between multiple functions in the execution chain, which can make the code more maintainable and easier to understand. 44 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/conventions/functions-naming.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Functions naming 3 | description: Naming conventions for middleware functions 4 | icon: SquareFunction 5 | --- 6 | 7 | import { FolderIcon } from "lucide-react"; 8 | 9 | Avoid using the default `middleware` name or prefix/suffix for your functions. Instead, use a more descriptive name that reflects the purpose of the function. This will make your code easier to understand and maintain. 10 | 11 | ## Proposal 12 | 13 | ```ts title="/app/(internal)/_middleware.ts" /internal/ 14 | export const internal = async ({ request }: MiddlewareFunctionProps) => { // [!code focus] 15 | // function body 16 | } 17 | ``` 18 | 19 | ```ts title="/app/(team)/t/_middleware.ts" /team/ 20 | export const team = async ({ request }: MiddlewareFunctionProps) => { // [!code focus] 21 | // function body 22 | } 23 | ``` 24 | 25 | ```ts title="/app/_middleware.ts" /analytics/ 26 | export const analytics = async ({ request }: MiddlewareFunctionProps) => { // [!code focus] 27 | // function body 28 | } 29 | ``` 30 | 31 | ### Related topics 32 | 33 | 34 | } description="How to organize middleware functions" href="/docs/1.4/conventions/project-structure"/> 35 | 36 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/conventions/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Conventions", 3 | "pages": ["project-structure", "functions-naming"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/conventions/project-structure.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Project structure 3 | description: Project structure and organization proposal 4 | icon: Folder 5 | --- 6 | 7 | import { SquareFunctionIcon } from "lucide-react"; 8 | 9 | ### Motivation 10 | 11 | The project structure is a key factor in the development process. It helps to organize the codebase, make it easier to maintain, and improve the development experience. A well-structured project can help developers to understand the codebase faster, find the files they need, and make changes more efficiently. 12 | 13 | ### Proposal 14 | 15 | 16 | 17 | 18 | }/> 19 | 20 | }/> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | }/> 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | }/> 40 | 41 | 42 | 43 | 44 | ### Explanation 45 | 46 | `app` folder contains the main application code. It is divided into two subfolders: `(internal)` and `(public)`. The `(internal)` folder contains the internal pages, while the `(public)` folder contains the public pages. Each grouped layout has its own `layout.tsx` file and a `page.tsx` file. The `_middleware.ts` file is used to define middleware functions for route groups or pages that it's used in. 47 | 48 | 49 | Use imports inside the `middleware.ts` file to use separate middleware functions inside global middleware configuration. 50 | 51 | 52 | ### Related topics 53 | 54 | 55 | } description="How to name middleware functions" href="/docs/1.4/conventions/functions-naming"/> 56 | 57 | 58 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/forward-functions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Forward 3 | description: Documentation for forward functions in NEMO 4 | icon: ArrowRight 5 | --- 6 | 7 | import { ArrowRightIcon } from "lucide-react"; 8 | 9 | Forward functions in NEMO allow you to forward the response from one middleware function to another. This can be useful for chaining middleware functions together and creating more complex middleware logic. 10 | 11 | ## Forward Function Schema 12 | 13 | This example shows how to use the forward function in a middleware. 14 | 15 | ```ts title="middleware.ts" 16 | import { forward, type MiddlewareFunctionProps } from '@rescale/nemo'; 17 | 18 | const firstMiddleware = async ({ request, response, forward }: MiddlewareFunctionProps) => { 19 | console.log('First middleware'); 20 | forward(response); 21 | }; 22 | 23 | const secondMiddleware = async ({ request }: MiddlewareFunctionProps) => { 24 | console.log('Second middleware'); 25 | }; 26 | 27 | const middlewares = { 28 | '/api': [firstMiddleware, secondMiddleware], 29 | }; 30 | 31 | export const middleware = createMiddleware(middlewares); 32 | ``` 33 | 34 | ## Explanation 35 | 36 | ### Prop: `forward` 37 | 38 | Type: `(response: MiddlewareReturn) => void` 39 | 40 | The `forward` function allows you to forward the response from one middleware function to another. This can be useful for chaining middleware functions together and creating more complex middleware logic. 41 | 42 | ### Example Usage 43 | 44 | In this example, the `firstMiddleware` logs a message and forwards the response to the `secondMiddleware`, which logs another message and returns the response. 45 | 46 | ```ts title="middleware.ts" 47 | import { forward, type MiddlewareFunctionProps } from '@rescale/nemo'; 48 | 49 | const firstMiddleware = async ({ request, response, forward }: MiddlewareFunctionProps) => { 50 | console.log('First middleware'); 51 | forward(response); 52 | }; 53 | 54 | const secondMiddleware = async ({ request }: MiddlewareFunctionProps) => { 55 | console.log('Second middleware'); 56 | }; 57 | 58 | const middlewares = { 59 | '/api': [firstMiddleware, secondMiddleware], 60 | }; 61 | 62 | export const middleware = createMiddleware(middlewares); 63 | ``` 64 | 65 | 66 | The `forward` function should be called with the response object that you want to forward to the next middleware function. 67 | 68 | 69 | ### Related Topics 70 | 71 | 72 | } description="Learn more about middleware functions" href="/docs/1.4/functions"/> 73 | 74 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/functions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Functions 3 | description: Middleware functions standardization, explanation and usage 4 | icon: Play 5 | --- 6 | 7 | import {WaypointsIcon, ArrowRightIcon} from "lucide-react"; 8 | 9 | This package introduces middleware functions standardization for more elastic approach to development and feature delivery. 10 | 11 | ## Function schema 12 | 13 | This example shows all props that are provided for your usage. 14 | 15 | ```ts title="_middleware.ts" 16 | async ({ request, response, context, event, forward, params }: MiddlewareFunctionProps) => { 17 | // function body 18 | } 19 | ``` 20 | 21 | ## Explanation 22 | 23 | ### Prop:  `request` 24 | 25 | Type: [NextRequest](https://nextjs.org/docs/app/functions/next-request) 26 | 27 | That's a user middleware's request passed to function, which cookies (and only them) can be later **updated** by **forwarded** functions in chain. 28 | 29 | This props cookies will only deffer from the original user's request if you've forwarded any response it in the chain. 30 | 31 | ### Prop:  `response` 32 | 33 | Type: [NextResponse](https://nextjs.org/docs/app/api-reference/functions/next-response) | `undefined` 34 | 35 | This property contains (optional) resposne object that were forwarded in prev middleware function using `forward()` function. 36 | 37 | }/> 38 | 39 | It can be used for example for checking custom headers from external packages middlewares output that was forwarded in chain. 40 | 41 | ```ts title="_middleware.ts" 42 | async ({ response }: MiddlewareFunctionProps) => { 43 | if (response) { 44 | console.log(response.headers.get('x-custom-header')); 45 | } 46 | } 47 | ``` 48 | 49 | 50 | If forwarded middleware added any custom headers or cookies, they will be passed to user at the end of the chain no matter if you handled that. 51 | 52 | 53 | ### Prop:  `context` 54 | 55 | Type: [Map\](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) 56 | 57 | This property contains context shared across whole execution chain for every function. 58 | 59 | }/> 60 | 61 | 62 | If you want to know more about Map interface usage please refer to these [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map). 63 | 64 | 65 | ### Prop:  `event` 66 | 67 | Type: [NextFetchEvent](https://nextjs.org/docs/app/building-your-application/routing/middleware#waituntil-and-nextfetchevent) 68 | 69 | This property contains event object for serverless functions execution. 70 | 71 | 72 | It can be used to use Next.js 15 new features like ` event.waitUntil()`. 73 | 74 | You can read more there: [Next.js 15 waitUntil](https://nextjs.org/docs/app/building-your-application/routing/middleware#waituntil-and-nextfetchevent) 75 | 76 | 77 | ### Prop:  `forward` 78 | 79 | Type: `(response: MiddlewareReturn) => void` 80 | 81 | The `forward` function allows you to forward the response from one middleware function to another. This can be useful for chaining middleware functions together and creating more complex middleware logic. 82 | 83 | }/> 84 | 85 | ### Prop:  `params` 86 | 87 | Type: `() => Partial>` 88 | 89 | This property contains route parameters parsed from the URL path. Just like it's working in Next.js pages/routes/layouts but without awaiting. 90 | 91 | ```ts title="_middleware.ts" 92 | // Example URL: /team/123 93 | // Matcher: '/team/:slug' 94 | 95 | async ({ params }: MiddlewareFunctionProps) => { 96 | console.log(params().slug); // Output: 123 97 | } 98 | ```import { WaypointsIcon, ArrowRightIcon } from "lucide-react"; 99 | import { WaypointsIcon, ArrowRightIcon } from "lucide-react"; 100 | import { WaypointsIcon, ArrowRightIcon } from "lucide-react"; 101 | import { WaypointsIcon, ArrowRightIcon } from "lucide-react"; 102 | import { WaypointsIcon, ArrowRightIcon } from "lucide-react"; 103 | 104 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | description: Supercharge your Next.js middleware with NEMO 4 | icon: Book 5 | --- 6 | 7 | import { SquareFunctionIcon, WrenchIcon } from "lucide-react"; 8 | 9 | ## Installation 10 | 11 | 12 | 13 | ### Paste installation command into your terminal to install package. 14 | 15 | ```package-install 16 | @rescale/nemo 17 | ``` 18 | 19 | 20 | ### Import the package inside middleware.ts file. 21 | 22 | (or create it if you don't have one yet) 23 | 24 | ```typescript title="middleware.ts" 25 | import { createMiddleware } from '@rescale/nemo'; // [!code ++] 26 | 27 | // [...] 28 | ``` 29 | 30 | 31 | ### Use the createMiddleware function to create a middleware helper. 32 | 33 | ```typescript title="middleware.ts" 34 | import { createMiddleware } from '@rescale/nemo'; 35 | 36 | export const middleware = createMiddleware(/* your functions will go here */); // [!code ++] 37 | 38 | // [...] 39 | ``` 40 | 41 | 42 | ### Define your middlewares config and update the createMiddleware function parameters. 43 | 44 | ```typescript title="middleware.ts" 45 | import { createMiddleware } from '@rescale/nemo'; // [!code --] 46 | import { createMiddleware, type MiddlewareFunctionProps } from '@rescale/nemo'; // [!code ++] 47 | 48 | const middlewares = { // [!code ++] 49 | '/': [ // [!code ++] 50 | async ({ request }: MiddlewareFunctionProps) => { // [!code ++] 51 | console.log('There is NEMO', request.nextUrl.pathname); // [!code ++] 52 | }, // [!code ++] 53 | ], // [!code ++] 54 | }; // [!code ++] 55 | 56 | export const middleware = createMiddleware(/* your functions will go here */); // [!code --] 57 | export const middleware = createMiddleware(middlewares); // [!code ++] 58 | 59 | // [...] 60 | ``` 61 | 62 | After that step, you should see an `There is NEMO` message in your console for every request made to your application. 63 | 64 | 65 | }/> 66 | }/> 67 | 68 | 69 | 70 | ### Optimize your middleware execution to not execute it on every signle request. 71 | 72 | ```typescript title="middleware.ts" 73 | // [...] 74 | 75 | export const config = { // [!code ++] 76 | matcher: ['/((?!_next/|_static|_vercel|[\\w-]+\\.\\w+).*)'], // [!code ++] 77 | }; // [!code ++] 78 | ``` 79 | 80 | That will prevent your middleware from executing on routes: 81 | 1. `/_next/` (Next.js internals) 82 | 2. `/_static` (inside /public) 83 | 3. `/_vercel` (Vercel internals) 84 | 4. Static files (e.g. `/favicon.ico`, `/sitemap.xml`, `/robots.txt`, etc.) 85 | 86 | 87 | Reed more about [Next.js middleware configuration](https://nextjs.org/docs/app/file-conventions/middleware#config-object-optional) 88 | 89 | 90 | 91 | ### Finally, let's put it all together. 92 | 93 | ```typescript title="middleware.ts" 94 | import { createMiddleware, type MiddlewareFunctionProps } from '@rescale/nemo'; 95 | 96 | const middlewares = { 97 | '/': [ 98 | async ({ request }: MiddlewareFunctionProps) => { 99 | console.log('There is NEMO', request.nextUrl.pathname); 100 | }, 101 | ], 102 | }; 103 | 104 | export const middleware = createMiddleware(middlewares); 105 | 106 | export const config = { 107 | matcher: ['/((?!_next/|_static|_vercel|[\\w-]+\\.\\w+).*)'], 108 | }; 109 | ``` 110 | 111 | That's how should your `middleware.ts` file looks like after all steps. 112 | 113 | 114 | 115 | ## Motivation 116 | 117 | I'm working with Next.js project for a few years now, after Vercel moved multiple `/**/_middleware.ts` files to a single `/middleware.ts` file, there was a unfilled gap - but just for now. After a 2023 retro I had found that there is no good solution for that problem, so I took matters into my own hands. I wanted to share that motivation with everyone here, as I think that we all need to remember how it all started. 118 | 119 | Hope it will save you some time and would make your project DX better! 120 | 121 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/matcher.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Route matchers 3 | description: path-to-regexp matchers for middleware functions 4 | icon: Variable 5 | --- 6 | 7 | import { WrenchIcon } from "lucide-react"; 8 | 9 | This package uses [path-to-regexp](https://github.com/component/path-to-regexp) package to match the middleware routes path and parse the route params in a same way as Next.js does in config's matcher prop. 10 | 11 | ## Examples 12 | 13 | To make it easier to understand, you can check the below examples: 14 | 15 | 16 | ### Simple route 17 | 18 | Matches `/dashboard` route and returns no params. 19 | 20 | ```plaintext title="Simple route" 21 | /dashboard 22 | ``` 23 | 24 | ### Prams 25 | 26 | General structure of the params is `:paramName` where `paramName` is the name of the param that will be returned in the middleware function. 27 | 28 | #### Single 29 | 30 | Matches `/dashboard/anything` route and returns `team` param with `anything value`. 31 | 32 | ```plaintext title="Single" 33 | /dashboard/:team 34 | ``` 35 | 36 | You can also define segments in the middle of URL with is matching `/team/anything/dashboard` and returns `team` param with `anything` value. 37 | 38 | ```plaintext title="Single with suffix" 39 | /dashboard/:team/delete 40 | ``` 41 | 42 | #### Optional 43 | 44 | Matches `/dashboard` and `/dashboard/anything` routes and returns `team` param with `anything` value if there is value provided in url. 45 | 46 | ```plaintext title="Optional" 47 | /dashboard{/:team} 48 | ``` 49 | 50 | ```plaintext title="Optional wildcard" 51 | /dashboard{/*team} 52 | ``` 53 | 54 | #### Wildcard 55 | 56 | Matches `/dashboard` and `/dashboard/anything/test` routes and returns `team` param with `[anything, test]` value if there is value provided in url. 57 | 58 | ```plaintext title="Wildcard" 59 | /dashboard/*team 60 | ``` 61 | -------------------------------------------------------------------------------- /apps/docs/content/1.4/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Archive version", 3 | "icon": "Archive", 4 | "pages": [ 5 | "---Guide---", 6 | "index", 7 | "configuration", 8 | "matcher", 9 | "functions", 10 | "context", 11 | "forward-functions", 12 | "---Conventions---", 13 | "...conventions", 14 | "---API Reference---", 15 | "...api-reference" 16 | ], 17 | "root": true, 18 | "title": "v1.4" 19 | } -------------------------------------------------------------------------------- /apps/docs/content/2.0/3rd-parties/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "supabase", 4 | "next-auth" 5 | ], 6 | "title": "Packages" 7 | } -------------------------------------------------------------------------------- /apps/docs/content/2.0/3rd-parties/next-auth.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: NextAuth (Auth.js) 3 | description: NEMO middleware functions for NextAuth (Auth.js) 4 | icon: SquareFunction 5 | --- 6 | 7 | ## Installation 8 | 9 | Integrate NextAuth with your project using the official guides: 10 | 11 | [Auth.js Quickstart](https://authjs.dev/getting-started/installation?framework=Next.js) or [Next.js App Example](https://github.com/vercel/next.js/tree/canary/examples/auth). 12 | 13 | Just skip the part of setting up the middleware and follow the steps below. 14 | 15 | 16 | 17 | 18 | ### Replace `middleware.ts` code 19 | 20 | We need to edit primary `middleware.ts` file to use the new middleware function. 21 | 22 | ```typescript title="@/middleware.ts" 23 | import { auth as authMiddleware } from "@/auth"; 24 | import { type MiddlewareConfig, type GlobalMiddlewareConfig, createNEMO } from '@rescale/nemo'; 25 | 26 | const globalMiddlewares: GlobalMiddlewareConfig = { 27 | before: async (request, event) => { 28 | await authMiddleware((_request, _event) => { 29 | const { auth } = _request; 30 | event.storage.set("user", auth?.user); 31 | })(request, event); 32 | } 33 | } 34 | 35 | const middlewares: MiddlewareConfig = { 36 | '/': [ 37 | async (request, event) => { 38 | console.log('There is NEMO', event.storage.get("user")); 39 | }, 40 | ], 41 | }; 42 | 43 | export const middleware = createNEMO(middlewares, globalMiddlewares); 44 | 45 | export const config = { 46 | matcher: ['/((?!_next/|_static|_vercel|[\\w-]+\\.\\w+).*)'], 47 | }; 48 | 49 | ``` 50 | 51 | 52 | 53 | Just use the `user` key inside storage as you need! 🎉 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/3rd-parties/supabase.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Supabase 3 | description: NEMO middleware functions for Supabase 4 | icon: SquareFunction 5 | --- 6 | 7 | ## Installation 8 | 9 | Integrate Supabase with your project using the official guides: 10 | 11 | [Supabase Quickstart](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs) or [Supabase Next.js App](https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs) 12 | 13 | We will only make one small change to make it work with NEMO. 14 | 15 | ## Integrate NEMO with Supabase middleware 16 | 17 | 18 | 19 | 20 | ### Create `_middleware.ts`. 21 | 22 | Create a new file in your projects lib or supabase directory called `_middleware.ts`. 23 | 24 | And paste middleware code that you've copied from the Supabase documentation. 25 | 26 | It should look something like this: 27 | 28 | ```ts title="@/lib/supabase/middleware.ts" 29 | import { createServerClient } from '@supabase/ssr'; 30 | import { type NextRequest, NextResponse } from 'next/server'; 31 | 32 | export async function updateSession(request: NextRequest) { 33 | let supabaseResponse = NextResponse.next({ 34 | request, 35 | }) 36 | 37 | const supabase = createServerClient( 38 | process.env.NEXT_PUBLIC_SUPABASE_URL, 39 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 40 | { 41 | cookies: { 42 | getAll() { 43 | return request.cookies.getAll() 44 | }, 45 | setAll(cookiesToSet) { 46 | cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value)) 47 | supabaseResponse = NextResponse.next({ 48 | request, 49 | }) 50 | cookiesToSet.forEach(({ name, value, options }) => 51 | supabaseResponse.cookies.set(name, value, options) 52 | ) 53 | }, 54 | }, 55 | } 56 | ); 57 | 58 | // refreshing the auth token 59 | await supabase.auth.getUser(); 60 | 61 | return supabaseResponse; 62 | } 63 | 64 | ``` 65 | 66 | 67 | 68 | ### Replace `middleware.ts` code 69 | 70 | We need to edit primary `middleware.ts` file to use the new middleware function. 71 | 72 | ```typescript title="@/middleware.ts" 73 | import { type MiddlewareConfig, type GlobalMiddlewareConfig, createNEMO } from '@rescale/nemo'; 74 | import { updateSession } from '@/lib/supabase/middleware'; 75 | 76 | const globalMiddlewares: GlobalMiddlewareConfig = { 77 | before: updateSession, // REMEMBER TO IMPORT updateSession 78 | } 79 | 80 | const middlewares: MiddlewareConfig = { 81 | '/': [ 82 | async (request) => { 83 | console.log('There is NEMO', request.nextUrl.pathname); 84 | }, 85 | ], 86 | }; 87 | 88 | export const middleware = createNEMO(middlewares, globalMiddlewares); 89 | 90 | export const config = { 91 | matcher: ['/((?!_next/|_static|_vercel|[\\w-]+\\.\\w+).*)'], 92 | }; 93 | 94 | ``` 95 | 96 | 97 | 98 | ### (Optional) Add user infomation to context 99 | 100 | To add user information to the context, you can use the following code: 101 | 102 | ```typescript title="@/lib/supabase/middleware.ts" 103 | // imports 104 | 105 | export async function updateSession(request, { storage }) { // [!code focus] [!code ++] 106 | 107 | // prev code 108 | 109 | // refreshing the auth token 110 | await supabase.auth.getUser(); // [!code focus] [!code --] 111 | const { user } = await supabase.auth.getUser(); // [!code focus] [!code ++] 112 | 113 | // add user to storage 114 | storage.set('user', user ?? undefined); // [!code focus] [!code ++] 115 | 116 | return supabaseResponse; 117 | } 118 | 119 | ``` 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/advanced-matching.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advanced Route Matching 3 | description: Learn advanced patterns for route matching in NEMO 4 | icon: Variable 5 | --- 6 | 7 | # Advanced Route Matching 8 | 9 | NEMO provides powerful route matching capabilities that go beyond simple path matching. This guide covers advanced techniques for fine-tuning your route patterns. 10 | 11 | ## Parameter Constraints 12 | 13 | ### Matching Specific Values 14 | 15 | You can constrain route parameters to match only a specific set of values using the `(option1|option2)` syntax: 16 | 17 | ```typescript 18 | const nemo = new NEMO({ 19 | "/:lang(en|fr|es)/documentation": [ 20 | (req) => { 21 | const { lang } = req.params; 22 | // lang will be either 'en', 'fr', or 'es' 23 | return NextResponse.next(); 24 | }, 25 | ], 26 | }); 27 | ``` 28 | 29 | This route will match: 30 | - `/en/documentation` 31 | - `/fr/documentation` 32 | - `/es/documentation` 33 | 34 | But will NOT match: 35 | - `/de/documentation` 36 | - `/jp/documentation` 37 | 38 | ### Excluding Specific Values 39 | 40 | You can exclude specific values from matching by using the `!` operator in the constraint: 41 | 42 | ```typescript 43 | const nemo = new NEMO({ 44 | "/:section(!api)/details": [ 45 | (req) => { 46 | const { section } = req.params; 47 | // section will be anything EXCEPT 'api' 48 | return NextResponse.next(); 49 | }, 50 | ], 51 | }); 52 | ``` 53 | 54 | This route will match: 55 | - `/products/details` 56 | - `/users/details` 57 | - `/settings/details` 58 | 59 | But will NOT match: 60 | - `/api/details` 61 | 62 | ### Combining Multiple Constraints 63 | 64 | You can use multiple parameter constraints within a single route: 65 | 66 | ```typescript 67 | const nemo = new NEMO({ 68 | "/:project/:env(dev|staging|prod)/:resource(!secrets)": [ 69 | (req) => { 70 | const { project, env, resource } = req.params; 71 | // env will be either 'dev', 'staging', or 'prod' 72 | // resource will be anything EXCEPT 'secrets' 73 | return NextResponse.next(); 74 | }, 75 | ], 76 | }); 77 | ``` 78 | 79 | ## Examples 80 | 81 | ### Language-specific Routes 82 | 83 | ```typescript 84 | const nemo = new NEMO({ 85 | "/:lang(en|cn)/blog/:postId": [ 86 | // Only matches English and Chinese blog pages 87 | (req) => { 88 | const { lang, postId } = req.params; 89 | // lang will be either 'en' or 'cn' 90 | return NextResponse.next(); 91 | }, 92 | ], 93 | }); 94 | ``` 95 | 96 | ### Protected Routes Exclusion 97 | 98 | ```typescript 99 | const nemo = new NEMO({ 100 | "/:area(!admin|!settings)/:page": [ 101 | // This middleware runs for any routes EXCEPT in admin or settings areas 102 | (req) => { 103 | const { area, page } = req.params; 104 | return NextResponse.next(); 105 | }, 106 | ], 107 | }); 108 | ``` 109 | 110 | ## Using Regular Expressions 111 | 112 | For even more complex matching requirements, NEMO supports full regular expressions within parameter constraints: 113 | 114 | ```typescript 115 | const nemo = new NEMO({ 116 | "/:id([0-9]{5})/profile": [ 117 | // Only matches if id consists of exactly 5 digits 118 | (req) => { 119 | const { id } = req.params; 120 | return NextResponse.next(); 121 | }, 122 | ], 123 | }); 124 | ``` 125 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/context.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Storage 3 | description: Storage between functions across the execution chain 4 | icon: Waypoints 5 | --- 6 | 7 | 8 | NEMO provides a storage that allows you to share data between functions in the middleware execution chain. This can be useful when you need to pass data between functions or store data that needs to be accessed by multiple functions. 9 | 10 | By default it operates using in-memory storage, but you can extend it with your own storage solution or database of choice. 11 | 12 | 13 | **Warning!** Be careful using database as storage adapter, as it can slow down the execution chain and make your site's TTFB skyrocket. 14 | 15 | **Recommendation**: Use KV databases/solutions like Redis, Vercel EdgeConfig etc. 16 | 17 | [Read more about good practices](/docs/2.0/best-practices) 18 | 19 | 20 | ## Usage example 21 | 22 | Below you can see an example of how to use the storage in your middleware functions. 23 | 24 | 25 | Remember that each request's middleware execution is a separate event instance, so the context is not shared between different requests. 26 | 27 | 28 | ```ts title="_middleware.ts" 29 | import type { NextMiddleware } from "@rescale/nemo"; 30 | 31 | const example: NextMiddleware = async (req, { storage }) => {// [!code focus] 32 | let user = undefined; // [!code focus] 33 | 34 | if(!storage.has('user')) { // [!code focus] 35 | user = await fetchUser(); // [!code focus] 36 | storage.set('user', user); // [!code focus] 37 | } else { // [!code focus] 38 | user = storage.get('user'); // [!code focus] 39 | } // [!code focus] 40 | 41 | if(!user) { 42 | return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); 43 | } 44 | } 45 | 46 | ``` 47 | 48 | ### Type Safety 49 | 50 | The storage API preserves type information when you use TypeScript generics: 51 | 52 | ```typescript 53 | interface UserData { 54 | id: number; 55 | name: string; 56 | roles: string[]; 57 | } 58 | 59 | // Store data with type 60 | storage.set('user', { 61 | id: 1, 62 | name: 'John', 63 | roles: ['admin', 'editor'] 64 | }); 65 | 66 | // Retrieve with correct type 67 | const user = storage.get('user'); 68 | if (user) { 69 | // TypeScript knows the shape of user 70 | console.log(user.name); 71 | console.log(user.roles.join(', ')); 72 | } 73 | ``` 74 | 75 | ## Custom Storage adapter 76 | 77 | You can extend the default in-memory storage with your own storage adapter. To do this, you need to create a class that implements the `StorageAdapter` interface. 78 | 79 | ```ts title="StorageAdapter.ts" 80 | import { StorageAdapter } from "@rescale/nemo"; 81 | 82 | export class CustomStorageAdapter extends StorageAdapter { 83 | // Implement required methods 84 | async get(key: string): T | undefined { 85 | // Your implementation 86 | return undefined; 87 | } 88 | 89 | async set(key: string, value: T): void { 90 | // Your implementation 91 | } 92 | 93 | async has(key: string): boolean { 94 | // Your implementation 95 | return false; 96 | } 97 | 98 | async delete(key: string): boolean { 99 | // Your implementation 100 | return false; 101 | } 102 | 103 | async clear(): void { 104 | // Your implementation 105 | } 106 | 107 | // Implement other required methods 108 | entries(): IterableIterator<[string, unknown]> { 109 | // Your implementation 110 | return [][Symbol.iterator](); 111 | } 112 | 113 | keys(): IterableIterator { 114 | // Your implementation 115 | return [][Symbol.iterator](); 116 | } 117 | 118 | values(): IterableIterator { 119 | // Your implementation 120 | return [][Symbol.iterator](); 121 | } 122 | 123 | get size(): number { 124 | // Your implementation 125 | return 0; 126 | } 127 | } 128 | 129 | ``` 130 | 131 | After creating your storage adapter, you can use it by passing it to the `NEMO` constructor. 132 | 133 | 134 | 135 | ```ts title="middleware.ts" 136 | import { NEMO } from "@rescale/nemo"; 137 | import { CustomStorageAdapter } from "./StorageAdapter"; 138 | 139 | export const middleware = createNEMO(middlewares, globalMiddleware, { 140 | storage: new CustomStorageAdapter() 141 | }); 142 | 143 | ``` 144 | 145 | 146 | ```ts title="middleware.ts" 147 | import { NEMO } from "@rescale/nemo"; 148 | import { CustomStorageAdapter } from "./StorageAdapter"; 149 | 150 | export const middleware = createNEMO(middlewares, globalMiddleware, { 151 | storage: () => new CustomStorageAdapter() 152 | }); 153 | 154 | ``` 155 | 156 | 157 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/conventions/functions-naming.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Functions naming 3 | description: Naming conventions for middleware functions 4 | icon: SquareFunction 5 | --- 6 | 7 | import { FolderIcon } from "lucide-react"; 8 | 9 | Avoid using the default `middleware` name or prefix/suffix for your functions. Instead, use a more descriptive name that reflects the purpose of the function. This will make your code easier to understand and maintain. 10 | 11 | ## Proposal 12 | 13 | ```ts title="/app/(internal)/_middleware.ts" /internal/ 14 | export const internal = async ({ request }: MiddlewareFunctionProps) => { 15 | // function body 16 | }; 17 | 18 | ``` 19 | 20 | ```ts title="/app/(team)/t/_middleware.ts" /team/ 21 | export const team = async ({ request }: MiddlewareFunctionProps) => { 22 | // function body 23 | }; 24 | 25 | ``` 26 | 27 | ```ts title="/app/_middleware.ts" /analytics/ 28 | export const analytics = async ({ request }: MiddlewareFunctionProps) => { 29 | // function body 30 | }; 31 | 32 | ``` 33 | 34 | ### Related topics 35 | 36 | 37 | } 40 | description="How to organize middleware functions" 41 | href="/docs/2.0/conventions/project-structure" 42 | /> 43 | 44 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/conventions/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Conventions", 3 | "pages": ["project-structure", "functions-naming"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/conventions/project-structure.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Project structure 3 | description: Project structure and organization proposal 4 | icon: Folder 5 | --- 6 | 7 | import { SquareFunctionIcon } from "lucide-react"; 8 | 9 | ### Motivation 10 | 11 | The project structure is a key factor in the development process. It helps to organize the codebase, make it easier to maintain, and improve the development experience. A well-structured project can help developers to understand the codebase faster, find the files they need, and make changes more efficiently. 12 | 13 | ### Proposal 14 | 15 | 16 | 17 | 18 | } 21 | /> 22 | 23 | } 26 | /> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | } 38 | /> 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | } 51 | /> 52 | 53 | 54 | 55 | ### Explanation 56 | 57 | `app` folder contains the main application code. It is divided into two subfolders: `(internal)` and `(public)`. The `(internal)` folder contains the internal pages, while the `(public)` folder contains the public pages. Each grouped layout has its own `layout.tsx` file and a `page.tsx` file. The `_middleware.ts` file is used to define middleware functions for route groups or pages that it's used in. 58 | 59 | 60 | Use imports inside the `middleware.ts` file to use separate middleware 61 | functions inside global middleware configuration. 62 | 63 | 64 | ### Related topics 65 | 66 | 67 | } 70 | description="How to name middleware functions" 71 | href="/docs/2.0/conventions/functions-naming" 72 | /> 73 | 74 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/functions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Functions 3 | description: Middleware functions standardization, explanation and usage 4 | icon: Play 5 | --- 6 | 7 | import { WaypointsIcon, ArrowRightIcon } from "lucide-react"; 8 | 9 | NEMO uses Next.js native middleware function schema to provide you with a familiar way of writing middleware functions and enabling you to use 3rd party packages without any issues. 10 | 11 | ## Function schema 12 | 13 | NEMO middleware function API is fully compatible with Next.js native API. It's just extending the event prop with some additional props and functions. 14 | 15 | ```ts twoslash title="_middleware.ts" 16 | import { NextMiddleware } from "@rescale/nemo"; 17 | 18 | const example: NextMiddleware = async (request, event) => { 19 | // function body 20 | }; 21 | 22 | ``` 23 | 24 | 25 | #### Info 26 | 27 | You can check detailed types by hovering on function parts and props. 28 | 29 | 30 | ## Explanation 31 | 32 | ### Prop:  `request` 33 | 34 | Type: [NextRequest](https://nextjs.org/docs/app/functions/next-request) 35 | 36 | That's a user middleware's request passed to middleware and can be later **updated** by functions in chain. 37 | 38 | This props cookies will only deffer from the original user's request if you've forwarded any response it in the chain. 39 | 40 | ### Prop:  `event` 41 | 42 | Type: `NemoEvent` extends [NextFetchEvent](https://nextjs.org/docs/app/building-your-application/routing/middleware#waituntil-and-nextfetchevent) 43 | 44 | This property contains event object for serverless functions execution extended via `nemo` prop which contains chain execution storage. 45 | 46 | } 51 | /> 52 | 53 | #### Logger methods 54 | 55 | The event object includes built-in logging capabilities that use the same logger as NEMO internally: 56 | 57 | ```ts 58 | // Debug logging (only displayed when debug is enabled in config) 59 | event.log("Processing user request", userId); 60 | 61 | // Error logging (always displayed) 62 | event.error("Failed to process request", error); 63 | 64 | // Warning logging (always displayed) 65 | event.warn("Deprecated feature used", featureName); 66 | ``` 67 | 68 | These logger methods maintain the "[NEMO]" prefix for consistent logging throughout your middleware chain. 69 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | description: Supercharge your Next.js middleware with NEMO 4 | icon: Book 5 | --- 6 | 7 | import { SquareFunctionIcon, WrenchIcon } from "lucide-react"; 8 | 9 | ## Installation & usage 10 | 11 | 12 | 13 | ### Paste installation command into your terminal to install package. 14 | 15 | ```package-install 16 | @rescale/nemo 17 | ``` 18 | 19 | 20 | 21 | ### Import the package inside middleware.ts file. 22 | 23 | (or create it if you don't have one yet) 24 | 25 | ```typescript title="middleware.ts" 26 | import { createNEMO } from '@rescale/nemo'; // [!code ++] 27 | 28 | // [...] 29 | 30 | ``` 31 | 32 | 33 | 34 | ### Use the createNEMO function to create a middleware helper. 35 | 36 | ```typescript title="middleware.ts" 37 | import { createNEMO } from '@rescale/nemo'; 38 | 39 | export const middleware = createNEMO(/* your functions will go here */); // [!code ++] 40 | 41 | // [...] 42 | 43 | ``` 44 | 45 | 46 | 47 | ### Define your middlewares config and update the createNEMO function parameters. 48 | 49 | ```typescript title="middleware.ts" 50 | import { createNEMO, type MiddlewareConfig } from '@rescale/nemo'; // [!code ++] 51 | 52 | const middlewares: MiddlewareConfig = { // [!code ++] 53 | '/': [ // [!code ++] 54 | async (request) => { // [!code ++] 55 | console.log('There is NEMO', request.nextUrl.pathname); // [!code ++] 56 | }, // [!code ++] 57 | ], // [!code ++] 58 | }; // [!code ++] 59 | 60 | export const middleware = createNEMO(/* your functions will go here */); // [!code --] 61 | export const middleware = createNEMO(middlewares); // [!code ++] 62 | 63 | // [...] 64 | 65 | ``` 66 | 67 | After that step, you should see an `There is NEMO` message in your console for every request made to your application. 68 | 69 | 70 | }/> 71 | }/> 72 | 73 | 74 | 75 | 76 | ### Optimize your middleware execution to not execute it on every signle request. 77 | 78 | ```typescript title="middleware.ts" 79 | // [...] 80 | 81 | export const config = { // [!code ++] 82 | matcher: ['/((?!_next/|_static|_vercel|[\\w-]+\\.\\w+).*)'], // [!code ++] 83 | }; // [!code ++] 84 | 85 | ``` 86 | 87 | That will prevent your middleware from executing on routes: 88 | 1. `/_next/` (Next.js internals) 89 | 2. `/_static` (inside /public) 90 | 3. `/_vercel` (Vercel internals) 91 | 4. Static files (e.g. `/favicon.ico`, `/sitemap.xml`, `/robots.txt`, etc.) 92 | 93 | 94 | Reed more about [Next.js middleware configuration](https://nextjs.org/docs/app/file-conventions/middleware#config-object-optional) 95 | 96 | 97 | 98 | 99 | ### Finally, let's put it all together. 100 | 101 | ```typescript twoslash title="middleware.ts" 102 | import { createNEMO, type MiddlewareConfig } from '@rescale/nemo'; 103 | 104 | const middlewares = { 105 | '/': [ 106 | async (request) => { 107 | console.log('There is NEMO', request.nextUrl.pathname); 108 | }, 109 | ], 110 | } satisfies MiddlewareConfig; 111 | 112 | export const middleware = createNEMO(middlewares); 113 | 114 | export const config = { 115 | matcher: ['/((?!_next/|_static|_vercel|[\\w-]+\\.\\w+).*)'], 116 | }; 117 | 118 | ``` 119 | 120 | That's how should your `middleware.ts` file looks like after all steps. 121 | 122 | 123 | 124 | 125 | ## Motivation 126 | 127 | I'm working with Next.js project for a few years now, after Vercel moved multiple `/**/_middleware.ts` files to a single `/middleware.ts` file, there was a unfilled gap - but just for now. After a 2023 retro I had found that there is no good solution for that problem, so I took matters into my own hands. I wanted to share that motivation with everyone here, as I think that we all need to remember how it all started. 128 | 129 | Hope it will save you some time and would make your project DX better! 130 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/matcher.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Route matchers 3 | description: path-to-regexp matchers for middleware functions 4 | icon: Variable 5 | --- 6 | 7 | import { WrenchIcon } from "lucide-react"; 8 | 9 | This package uses [path-to-regexp](https://github.com/component/path-to-regexp) package to match the middleware routes path and parse the route params in a same way as Next.js does in config's matcher prop. 10 | 11 | ## Examples 12 | 13 | To make it easier to understand, you can check the below examples: 14 | 15 | ### Simple route 16 | 17 | Matches `/dashboard` route and returns no params. 18 | 19 | 20 | 21 | ```plaintext 22 | /dashboard 23 | ``` 24 | 25 | 26 | ```ts 27 | / 28 | /home 29 | /dashboard [PASS] // [!code ++] 30 | /settings 31 | ``` 32 | 33 | 34 | 35 | ### Grouped routes 36 | 37 | Matches `/v1` and `/v2` prefixed routes. 38 | 39 | 40 | 41 | ```plaintext 42 | /(v1|v2)/:path* 43 | ``` 44 | 45 | 46 | ```ts 47 | / 48 | /home 49 | /v1/anything [PASS] // [!code ++] 50 | /v2/anything [PASS] // [!code ++] 51 | /settings 52 | ``` 53 | 54 | 55 | 56 | ### Prams 57 | 58 | General structure of the params is `:paramName` where `paramName` is the name of the param that will be returned in the middleware function. 59 | 60 | #### Single 61 | 62 | Matches `/dashboard/anything` route and returns `team` param with `anything` value. 63 | 64 | 65 | 66 | ```plaintext 67 | /dashboard/:team 68 | ``` 69 | 70 | 71 | ```ts 72 | / 73 | /home 74 | /dashboard 75 | /dashboard/team1 [PASS] // [!code ++] 76 | /dashboard/team2 [PASS] // [!code ++] 77 | /settings 78 | ``` 79 | 80 | 81 | 82 | You can also define segments in the middle of URL with is matching `/team/anything/dashboard` and returns `team` param with `anything` value. 83 | 84 | 85 | 86 | ```plaintext 87 | /dashboard/:team/delete 88 | ``` 89 | 90 | 91 | ```ts 92 | / 93 | /home 94 | /dashboard 95 | /dashboard/team1 96 | /dashboard/team2/delete [PASS] // [!code ++] 97 | /settings 98 | ``` 99 | 100 | 101 | 102 | #### Optional 103 | 104 | Matches `/dashboard` and `/dashboard/anything` routes and returns `team` param with `anything` value if there is value provided in url. 105 | 106 | 107 | 108 | ```plaintext 109 | /team/:slug? 110 | ``` 111 | 112 | 113 | ```ts 114 | / 115 | /home 116 | /dashboard 117 | /team [PASS] // [!code ++] 118 | /team/team1 [PASS] // [!code ++] 119 | /team/team2/settings 120 | /settings 121 | ``` 122 | 123 | 124 | 125 | #### Optional Wildcard 126 | 127 | 128 | Zero or more params 129 | 130 | 131 | Matches `/dashboard` and `/dashboard/anything/test` routes and returns `team` param with `[anything, test]` value if there is value provided in url. 132 | 133 | 134 | 135 | ```plaintext 136 | /team/:slug* 137 | ``` 138 | 139 | 140 | ```ts 141 | / 142 | /home 143 | /dashboard 144 | /team/team1 [PASS] // [!code ++] 145 | /team/team2/settings [PASS] // [!code ++] 146 | /settings 147 | ``` 148 | 149 | 150 | 151 | #### Required Wildcard 152 | 153 | 154 | One or more params 155 | 156 | 157 | Matches `/dashboard` and `/dashboard/anything/test` routes and returns `team` param with `[anything, test]` value if there is value provided in url. 158 | 159 | 160 | 161 | ```plaintext 162 | /team/:slug+ 163 | ``` 164 | 165 | 166 | ```ts 167 | / 168 | /home 169 | /dashboard 170 | /team/team1 [PASS] // [!code ++] 171 | /team/team2/settings [PASS] // [!code ++] 172 | /settings 173 | ``` 174 | 175 | 176 | -------------------------------------------------------------------------------- /apps/docs/content/2.0/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Latest version", 3 | "icon": "Tag", 4 | "pages": [ 5 | "---Getting started---", 6 | "index", 7 | "configuration", 8 | "matcher", 9 | "advanced-matching", 10 | "functions", 11 | "context", 12 | "best-practices", 13 | "---Conventions---", 14 | "...conventions", 15 | "---3rd parties---", 16 | "3rd-parties" 17 | ], 18 | "root": true, 19 | "title": "v2.0" 20 | } -------------------------------------------------------------------------------- /apps/docs/content/2.0/nesting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nested Routes 3 | description: How to work with nested routes in NEMO middleware 4 | icon: GitFork 5 | --- 6 | 7 | # Nested Routes 8 | 9 | NEMO supports hierarchical organization of middleware through nested routes, allowing you to create advanced routing patterns while maintaining clean code structure. 10 | 11 | ## Basic Nested Routes 12 | 13 | You can define nested routes by using objects that contain both a `middleware` property and additional route keys: 14 | 15 | ```ts 16 | import { createNEMO } from "@rescale/nemo"; 17 | 18 | const middleware = createNEMO({ 19 | "/admin": { 20 | // Middleware for /admin route 21 | middleware: async (req) => { 22 | req.headers.set("x-section", "admin"); 23 | return NextResponse.next(); 24 | }, 25 | 26 | // Nested route for /admin/users 27 | "/users": async (req) => { 28 | // This middleware only runs for /admin/users 29 | return NextResponse.next({ 30 | headers: { "x-admin-users": "true" } 31 | }); 32 | } 33 | } 34 | }); 35 | ``` 36 | 37 | In this example: 38 | - The parent middleware runs for `/admin` and any matching child routes 39 | - The child middleware runs only for `/admin/users` 40 | - Both middlewares execute in sequence for matching routes 41 | 42 | ## Execution Order 43 | 44 | When a request matches a nested route, NEMO executes the middleware in this order: 45 | 1. Global `before` middleware (if defined) 46 | 2. Root path middleware (`/`) for all non-root requests 47 | 3. Parent middleware 48 | 4. Child middleware 49 | 5. Global `after` middleware (if defined) 50 | 51 | 52 | If any middleware in the chain returns a response (like a redirect), the chain stops and that response is returned immediately. 53 | 54 | 55 | ## Chain Breaking 56 | 57 | When a parent middleware returns a response, child middleware will not be executed: 58 | 59 | ```ts 60 | const middleware = createNEMO({ 61 | "/protected": { 62 | middleware: (req) => { 63 | // If user is not authenticated, redirect to login 64 | if (!isAuthenticated(req)) { 65 | return NextResponse.redirect("/login"); 66 | } 67 | // Otherwise continue to child routes 68 | return NextResponse.next(); 69 | }, 70 | "/dashboard": (req) => { 71 | // This won't run if the parent redirected 72 | return NextResponse.next({ 73 | headers: { "x-dashboard": "true" } 74 | }); 75 | } 76 | } 77 | }); 78 | ``` 79 | 80 | ## Deep Nesting with Parameters 81 | 82 | You can create deeply nested routes with URL parameters: 83 | 84 | ```ts 85 | const middleware = createNEMO({ 86 | "/shop": { 87 | middleware: shopMiddleware, 88 | "/categories": { 89 | middleware: categoriesMiddleware, 90 | "/:categoryId": { 91 | middleware: (req, event) => { 92 | // Access the categoryId parameter 93 | const categoryId = event.params.categoryId; 94 | req.headers.set("x-category-id", categoryId); 95 | return NextResponse.next(); 96 | }, 97 | "/products": { 98 | middleware: productsMiddleware, 99 | "/:productId": (req, event) => { 100 | const { categoryId, productId } = event.params; 101 | // Both parent and child parameters are available 102 | return NextResponse.next({ 103 | headers: { 104 | "x-product-id": productId, 105 | "x-full-path": `category-${categoryId}/product-${productId}`, 106 | }, 107 | }); 108 | } 109 | } 110 | } 111 | } 112 | } 113 | }); 114 | ``` 115 | 116 | In this advanced example, a path like `/shop/categories/electronics/products/laptop` would execute all middleware in the chain, with `event.params` containing: 117 | - `categoryId: "electronics"` 118 | - `productId: "laptop"` 119 | 120 | ## Important Notes 121 | 122 | 1. **Top-level routes** without nesting **only match exact paths** by default. 123 | ```ts 124 | // This will ONLY match /foo exactly, not /foo/bar 125 | const middleware = createNEMO({ 126 | "/foo": fooMiddleware 127 | }); 128 | ``` 129 | 130 | 2. **Parent routes** with nested children will match both the exact path and child paths. 131 | ```ts 132 | // This matches both /parent and /parent/child 133 | const middleware = createNEMO({ 134 | "/parent": { 135 | middleware: parentMiddleware, 136 | "/child": childMiddleware 137 | } 138 | }); 139 | ``` 140 | 141 | 3. **Parameter matching** follows the same rules as regular route matching - parent parameters are available to child routes. 142 | -------------------------------------------------------------------------------- /apps/docs/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import pluginNext from "@next/eslint-plugin-next"; 3 | import eslintConfigPrettier from "eslint-config-prettier"; 4 | import * as mdx from "eslint-plugin-mdx"; 5 | import onlyWarn from "eslint-plugin-only-warn"; 6 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 7 | import pluginReact from "eslint-plugin-react"; 8 | import pluginReactHooks from "eslint-plugin-react-hooks"; 9 | import turboPlugin from "eslint-plugin-turbo"; 10 | import globals from "globals"; 11 | import tseslint from "typescript-eslint"; 12 | 13 | /** 14 | * A shared ESLint configuration for the repository. 15 | * 16 | * @type {import("eslint").Linter.Config} 17 | * */ 18 | export default [ 19 | { 20 | plugins: { 21 | turbo: turboPlugin, 22 | }, 23 | rules: { 24 | "turbo/no-undeclared-env-vars": "warn", 25 | }, 26 | }, 27 | { 28 | plugins: { 29 | onlyWarn, 30 | }, 31 | }, 32 | { 33 | ignores: ["dist/**"], 34 | }, 35 | js.configs.recommended, 36 | eslintConfigPrettier, 37 | eslintPluginPrettierRecommended, 38 | ...tseslint.configs.recommended, 39 | { 40 | ...pluginReact.configs.flat.recommended, 41 | languageOptions: { 42 | ...pluginReact.configs.flat.recommended.languageOptions, 43 | globals: { 44 | ...globals.serviceworker, 45 | }, 46 | }, 47 | }, 48 | { 49 | plugins: { 50 | "@next/next": pluginNext, 51 | }, 52 | rules: { 53 | ...pluginNext.configs.recommended.rules, 54 | ...pluginNext.configs["core-web-vitals"].rules, 55 | }, 56 | }, 57 | { 58 | plugins: { 59 | "react-hooks": pluginReactHooks, 60 | }, 61 | settings: { react: { version: "detect" } }, 62 | rules: { 63 | ...pluginReactHooks.configs.recommended.rules, 64 | // React scope no longer necessary with new JSX transform. 65 | "react/react-in-jsx-scope": "off", 66 | }, 67 | }, 68 | { 69 | ...mdx.flat, 70 | // optional, if you want to lint code blocks at the same 71 | processor: mdx.createRemarkProcessor({ 72 | lintCodeBlocks: true, 73 | // optional, if you want to disable language mapper, set it to `false` 74 | // if you want to override the default language mapper inside, you can provide your own 75 | languageMapper: {}, 76 | }), 77 | rules: { 78 | ...mdx.flat.rules, 79 | "react/jsx-no-undef": "off", 80 | "prettier/prettier": "off", 81 | }, 82 | }, 83 | { 84 | ...mdx.flatCodeBlocks, 85 | rules: { 86 | ...mdx.flatCodeBlocks.rules, 87 | // if you want to override some rules for code blocks 88 | "no-var": "error", 89 | "prefer-const": "error", 90 | "@typescript-eslint/no-unused-vars": "off", 91 | }, 92 | }, 93 | ]; 94 | -------------------------------------------------------------------------------- /apps/docs/lib/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next/types"; 2 | 3 | import { source } from "@/app/source"; 4 | import { createMetadataImage } from "fumadocs-core/server"; 5 | 6 | export const metadataImage = createMetadataImage({ 7 | imageRoute: "/docs-og", 8 | source, 9 | }); 10 | 11 | export function createMetadata(override: Metadata): Metadata { 12 | return { 13 | ...override, 14 | openGraph: { 15 | title: override.title ?? undefined, 16 | description: override.description ?? undefined, 17 | url: "https://nemo.zanreal.com", 18 | siteName: "NEMO", 19 | ...override.openGraph, 20 | }, 21 | twitter: { 22 | card: "summary_large_image", 23 | creator: "@z4nr34l", 24 | title: override.title ?? undefined, 25 | description: override.description ?? undefined, 26 | ...override.twitter, 27 | }, 28 | }; 29 | } 30 | 31 | export const baseUrl = 32 | process.env.NODE_ENV === "development" 33 | ? new URL("http://localhost:3000") 34 | : new URL(`https://nemo.zanreal.com`); 35 | -------------------------------------------------------------------------------- /apps/docs/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]): string { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from "fumadocs-mdx/next"; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const config = { 7 | reactStrictMode: true, 8 | poweredByHeader: false, 9 | compiler: { 10 | ...(process.env.VERCEL_ENV === "production" 11 | ? { 12 | removeConsole: { 13 | exclude: ["error"], 14 | }, 15 | } 16 | : {}), 17 | }, 18 | images: { 19 | remotePatterns: [ 20 | { 21 | protocol: "https", 22 | hostname: "cdn.zanreal.com", 23 | pathname: "/public/**", 24 | }, 25 | ], 26 | }, 27 | redirects: () => { 28 | return [ 29 | { 30 | source: "/docs", 31 | destination: "/docs/2.0", 32 | permanent: process.env.NODE_ENV === "production", 33 | }, 34 | ]; 35 | }, 36 | async rewrites() { 37 | return { 38 | beforeFiles: [], 39 | afterFiles: [], 40 | fallback: [], 41 | }; 42 | }, 43 | }; 44 | 45 | export default withMDX(config); 46 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@radix-ui/react-slot": "^1.0.2", 4 | "@rescale/nemo": "workspace:*", 5 | "@types/flexsearch": "^0.7.6", 6 | "@vercel/analytics": "^1.5.0", 7 | "@vercel/speed-insights": "^1.2.0", 8 | "class-variance-authority": "^0.7.0", 9 | "clsx": "^2.1.0", 10 | "fumadocs-core": "14.4.0", 11 | "fumadocs-docgen": "^1.3.2", 12 | "fumadocs-mdx": "^11.1.2", 13 | "fumadocs-twoslash": "^2.0.1", 14 | "fumadocs-typescript": "^3.0.3", 15 | "fumadocs-ui": "14.4.0", 16 | "geist": "^1.2.2", 17 | "lucide-react": "^0.414.0", 18 | "next": "^15.4.0-canary.33", 19 | "react": "latest", 20 | "react-dom": "latest", 21 | "shiki": "^1.12.1", 22 | "tailwind-merge": "^2.2.1", 23 | "tailwindcss-animate": "^1.0.7", 24 | "ts-morph": "^24.0.0", 25 | "zod": "^3.23.8" 26 | }, 27 | "devDependencies": { 28 | "@next/eslint-plugin-next": "^15.1.7", 29 | "@shikijs/transformers": "^1.12.1", 30 | "@types/mdx": "^2.0.13", 31 | "@types/node": "^22.13.1", 32 | "@types/react": "^19.0.1", 33 | "@types/react-dom": "^19.0.2", 34 | "autoprefixer": "10.4.16", 35 | "eslint": "^9.15.0", 36 | "eslint-config-prettier": "^10.0.1", 37 | "eslint-plugin-mdx": "^3.1.5", 38 | "eslint-plugin-only-warn": "^1.1.0", 39 | "eslint-plugin-prettier": "^5.2.2", 40 | "eslint-plugin-react": "^7.37.2", 41 | "eslint-plugin-react-hooks": "^5.0.0", 42 | "eslint-plugin-turbo": "^2.3.0", 43 | "postcss": "8.4.32", 44 | "tailwindcss": "^3.4.17", 45 | "typescript": "^5.7.3", 46 | "typescript-eslint": "^8.24.1" 47 | }, 48 | "name": "docs", 49 | "private": true, 50 | "scripts": { 51 | "build": "next build", 52 | "dev": "next dev --turbopack", 53 | "postinstall": "fumadocs-mdx", 54 | "start": "next start" 55 | }, 56 | "version": "0.0.0" 57 | } -------------------------------------------------------------------------------- /apps/docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "tailwindcss/nesting": {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /apps/docs/source.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | transformerMetaWordHighlight, 3 | transformerNotationDiff, 4 | transformerNotationErrorLevel, 5 | transformerNotationFocus, 6 | transformerNotationHighlight, 7 | } from "@shikijs/transformers"; 8 | import { 9 | rehypeCodeDefaultOptions, 10 | RehypeCodeOptions, 11 | remarkImage, 12 | remarkStructure, 13 | } from "fumadocs-core/mdx-plugins"; 14 | import { remarkInstall } from "fumadocs-docgen"; 15 | import { defineConfig, defineDocs } from "fumadocs-mdx/config"; 16 | import { transformerTwoslash } from "fumadocs-twoslash"; 17 | 18 | export const { docs, meta } = defineDocs({ 19 | dir: "content", 20 | }); 21 | 22 | export default defineConfig({ 23 | lastModifiedTime: "git", 24 | mdxOptions: { 25 | remarkPlugins: [remarkInstall, remarkImage, remarkStructure], 26 | rehypeCodeOptions: { 27 | transformers: [ 28 | ...(rehypeCodeDefaultOptions.transformers as never), 29 | transformerTwoslash(), 30 | transformerNotationHighlight(), 31 | transformerNotationDiff(), 32 | transformerNotationFocus(), 33 | transformerNotationErrorLevel(), 34 | transformerMetaWordHighlight(), 35 | ], 36 | } as RehypeCodeOptions, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /apps/docs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { createPreset } from "fumadocs-ui/tailwind-plugin"; 2 | import type { Config } from "tailwindcss"; 3 | 4 | const config = { 5 | darkMode: ["class"], 6 | content: [ 7 | "./components/**/*.{ts,tsx}", 8 | "./app/**/*.{ts,tsx}", 9 | "./content/**/*.{md,mdx}", 10 | "./mdx-components.{ts,tsx}", 11 | "./node_modules/fumadocs-ui/dist/**/*.js", 12 | "../../node_modules/fumadocs-ui/dist/**/*.js", 13 | ], 14 | prefix: "", 15 | theme: { 16 | container: { 17 | center: true, 18 | padding: "2rem", 19 | screens: { 20 | "2xl": "1400px", 21 | }, 22 | }, 23 | extend: { 24 | fontFamily: { 25 | serif: ["var(--font-geist-sans)"], 26 | sans: ["var(--font-geist-sans)"], 27 | mono: ["var(--font-geist-mono)"], 28 | heading: ["var(--font-geist-sans)"], 29 | display: ["var(--font-geist-sans)"], 30 | body: ["var(--font-geist-sans)"], 31 | }, 32 | colors: { 33 | border: "hsl(var(--border))", 34 | input: "hsl(var(--input))", 35 | ring: "hsl(var(--ring))", 36 | background: "hsl(var(--background))", 37 | foreground: "hsl(var(--foreground))", 38 | primary: { 39 | DEFAULT: "hsl(var(--primary))", 40 | foreground: "hsl(var(--primary-foreground))", 41 | }, 42 | secondary: { 43 | DEFAULT: "hsl(var(--secondary))", 44 | foreground: "hsl(var(--secondary-foreground))", 45 | }, 46 | destructive: { 47 | DEFAULT: "hsl(var(--destructive))", 48 | foreground: "hsl(var(--destructive-foreground))", 49 | }, 50 | muted: { 51 | DEFAULT: "hsl(var(--muted))", 52 | foreground: "hsl(var(--muted-foreground))", 53 | }, 54 | accent: { 55 | DEFAULT: "hsl(var(--accent))", 56 | foreground: "hsl(var(--accent-foreground))", 57 | }, 58 | popover: { 59 | DEFAULT: "hsl(var(--popover))", 60 | foreground: "hsl(var(--popover-foreground))", 61 | }, 62 | card: { 63 | DEFAULT: "hsl(var(--card))", 64 | foreground: "hsl(var(--card-foreground))", 65 | }, 66 | }, 67 | borderRadius: { 68 | lg: "var(--radius)", 69 | md: "calc(var(--radius) - 2px)", 70 | sm: "calc(var(--radius) - 4px)", 71 | }, 72 | keyframes: { 73 | "accordion-down": { 74 | from: { height: "0" }, 75 | to: { height: "var(--radix-accordion-content-height)" }, 76 | }, 77 | "accordion-up": { 78 | from: { height: "var(--radix-accordion-content-height)" }, 79 | to: { height: "0" }, 80 | }, 81 | }, 82 | animation: { 83 | "accordion-down": "accordion-down 0.2s ease-out", 84 | "accordion-up": "accordion-up 0.2s ease-out", 85 | }, 86 | }, 87 | }, 88 | plugins: [require("tailwindcss-animate")], 89 | presets: [createPreset()], 90 | } satisfies Config; 91 | 92 | export default config; 93 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | }, 23 | "target": "ES2017" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/docs/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": [ 4 | "//" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "dependsOn": [ 9 | "^build" 10 | ], 11 | "env": [], 12 | "outputs": [ 13 | ".next/**", 14 | "!.next/cache/**" 15 | ] 16 | }, 17 | "dev": { 18 | "cache": false, 19 | "persistent": true 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /apps/docs/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { NextFetchEvent, NextRequest, NextResponse } from "next/server"; 2 | 3 | type NextMiddleware = ( 4 | request: NextRequest, 5 | event: NextFetchEvent 6 | ) => Response | NextResponse | Promise; 7 | interface MiddlewareFunctionProps { 8 | request: NextRequest; 9 | response: NextResponse; 10 | context: Map; 11 | event: NextFetchEvent; 12 | } 13 | type MiddlewareFunction = ( 14 | props: MiddlewareFunctionProps 15 | ) => NextResponse | Response | Promise; 16 | type MiddlewareConfig = Record< 17 | string, 18 | MiddlewareFunction | MiddlewareFunction[] 19 | >; 20 | declare function createMiddleware( 21 | pathMiddlewareMap: MiddlewareConfig, 22 | globalMiddleware?: Record< 23 | "before" | "after", 24 | MiddlewareFunction | MiddlewareFunction[] 25 | > 26 | ): NextMiddleware; 27 | 28 | export { 29 | type MiddlewareConfig, 30 | type MiddlewareFunction, 31 | type MiddlewareFunctionProps, 32 | type NextMiddleware, 33 | createMiddleware, 34 | }; 35 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | coverage = true # always enable coverage 3 | coverageReporter = ["text","lcov"] 4 | coverageDir = "packages/nemo/coverage" -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/basic/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import onlyWarn from "eslint-plugin-only-warn"; 4 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 5 | import turboPlugin from "eslint-plugin-turbo"; 6 | import tseslint from "typescript-eslint"; 7 | 8 | /** 9 | * A shared ESLint configuration for the repository. 10 | * 11 | * @type {import("eslint").Linter.Config} 12 | * */ 13 | export default [ 14 | { 15 | plugins: { 16 | turbo: turboPlugin, 17 | }, 18 | rules: { 19 | "turbo/no-undeclared-env-vars": "warn", 20 | }, 21 | }, 22 | { 23 | plugins: { 24 | onlyWarn, 25 | }, 26 | }, 27 | { 28 | ignores: ["dist/**"], 29 | }, 30 | js.configs.recommended, 31 | eslintConfigPrettier, 32 | eslintPluginPrettierRecommended, 33 | ...tseslint.configs.recommended, 34 | { 35 | ...pluginReact.configs.flat.recommended, 36 | languageOptions: { 37 | ...pluginReact.configs.flat.recommended.languageOptions, 38 | globals: { 39 | ...globals.serviceworker, 40 | }, 41 | }, 42 | }, 43 | { 44 | plugins: { 45 | "@next/next": pluginNext, 46 | }, 47 | rules: { 48 | ...pluginNext.configs.recommended.rules, 49 | ...pluginNext.configs["core-web-vitals"].rules, 50 | }, 51 | }, 52 | { 53 | plugins: { 54 | "react-hooks": pluginReactHooks, 55 | }, 56 | settings: { react: { version: "detect" } }, 57 | rules: { 58 | ...pluginReactHooks.configs.recommended.rules, 59 | // React scope no longer necessary with new JSX transform. 60 | "react/react-in-jsx-scope": "off", 61 | }, 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /examples/basic/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@rescale/nemo": "workspace:*", 4 | "next": "^15.3.1-canary.0", 5 | "react": "latest", 6 | "react-dom": "latest" 7 | }, 8 | "devDependencies": { 9 | "@types/node": "^20", 10 | "@types/react": "^19.0.1", 11 | "@types/react-dom": "^19.0.2", 12 | "autoprefixer": "10.4.16", 13 | "eslint": "^9", 14 | "eslint-config-next": "14.1.0", 15 | "postcss": "^8", 16 | "tailwindcss": "^3.4.17", 17 | "typescript": "^5.7.3" 18 | }, 19 | "name": "basic-example", 20 | "private": true, 21 | "scripts": { 22 | "examples": "next dev -p 3001", 23 | "lint": "next lint" 24 | }, 25 | "version": "0.1.0" 26 | } -------------------------------------------------------------------------------- /examples/basic/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/basic/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/basic/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/basic/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z4nr34l/nemo/6ec7db02ee36af3002502667ffc0227de73ca534/examples/basic/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/basic/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/basic/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/basic/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createNEMO, type MiddlewareConfig } from "@rescale/nemo"; 2 | import { NextResponse, type NextRequest } from "next/server"; 3 | 4 | const globlMiddleware = { 5 | before: () => {} 6 | } 7 | 8 | const middlewares = { 9 | "/page1/:path*": [ 10 | (request) => { 11 | console.log("middleware 1 before", request.headers.get('x-custom-header')); 12 | request.headers.set("x-custom-header", "custom-value"); 13 | console.log("middleware 1 after", request.headers.get('x-custom-header')); 14 | }, 15 | (request) => { 16 | console.log("middleware 2", "x-custom-sent-header", request.headers.get('x-custom-sent-header')); 17 | console.log("middleware 2", "x-custom-header", request.headers.get('x-custom-header')); 18 | } 19 | ], 20 | "/page2": [ 21 | (request: NextRequest) => { 22 | return NextResponse.next({ 23 | headers: { "x-test": "value" }, 24 | }) 25 | } 26 | ] 27 | } satisfies MiddlewareConfig; 28 | 29 | // Create middlewares helper 30 | export const middleware = createNEMO(middlewares, globlMiddleware, { 31 | debug: true, 32 | enableTiming: true 33 | }); 34 | 35 | export const config = { 36 | matcher: ["/page2", "/page1/:path*"], 37 | }; 38 | -------------------------------------------------------------------------------- /examples/basic/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | }, 23 | "target": "ES2017" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/basic/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "dev": { 6 | "cache": false, 7 | "persistent": true 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/cookies/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/cookies/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/cookies/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import onlyWarn from "eslint-plugin-only-warn"; 4 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 5 | import turboPlugin from "eslint-plugin-turbo"; 6 | import tseslint from "typescript-eslint"; 7 | 8 | /** 9 | * A shared ESLint configuration for the repository. 10 | * 11 | * @type {import("eslint").Linter.Config} 12 | * */ 13 | export default [ 14 | { 15 | plugins: { 16 | turbo: turboPlugin, 17 | }, 18 | rules: { 19 | "turbo/no-undeclared-env-vars": "warn", 20 | }, 21 | }, 22 | { 23 | plugins: { 24 | onlyWarn, 25 | }, 26 | }, 27 | { 28 | ignores: ["dist/**"], 29 | }, 30 | js.configs.recommended, 31 | eslintConfigPrettier, 32 | eslintPluginPrettierRecommended, 33 | ...tseslint.configs.recommended, 34 | { 35 | ...pluginReact.configs.flat.recommended, 36 | languageOptions: { 37 | ...pluginReact.configs.flat.recommended.languageOptions, 38 | globals: { 39 | ...globals.serviceworker, 40 | }, 41 | }, 42 | }, 43 | { 44 | plugins: { 45 | "@next/next": pluginNext, 46 | }, 47 | rules: { 48 | ...pluginNext.configs.recommended.rules, 49 | ...pluginNext.configs["core-web-vitals"].rules, 50 | }, 51 | }, 52 | { 53 | plugins: { 54 | "react-hooks": pluginReactHooks, 55 | }, 56 | settings: { react: { version: "detect" } }, 57 | rules: { 58 | ...pluginReactHooks.configs.recommended.rules, 59 | // React scope no longer necessary with new JSX transform. 60 | "react/react-in-jsx-scope": "off", 61 | }, 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /examples/cookies/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /examples/cookies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@rescale/nemo": "workspace:*", 4 | "next": "^15.3.1-canary.0", 5 | "react": "latest", 6 | "react-dom": "latest" 7 | }, 8 | "devDependencies": { 9 | "@types/node": "^20", 10 | "@types/react": "^19.0.1", 11 | "@types/react-dom": "^19.0.2", 12 | "autoprefixer": "10.4.16", 13 | "eslint": "^9", 14 | "eslint-config-next": "14.1.0", 15 | "postcss": "^8", 16 | "tailwindcss": "^3.4.17", 17 | "typescript": "^5.7.3" 18 | }, 19 | "name": "cookies-example", 20 | "private": true, 21 | "scripts": { 22 | "examples": "next dev -p 3003", 23 | "lint": "next lint" 24 | }, 25 | "version": "0.1.0" 26 | } -------------------------------------------------------------------------------- /examples/cookies/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/cookies/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/cookies/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/cookies/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z4nr34l/nemo/6ec7db02ee36af3002502667ffc0227de73ca534/examples/cookies/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/cookies/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/cookies/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/cookies/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createMiddleware, 3 | type MiddlewareConfig, 4 | MiddlewareFunctionProps, 5 | } from "@rescale/nemo"; 6 | import { NextResponse } from "next/server"; 7 | 8 | const middlewares = { 9 | "/": [ 10 | async ({ request, forward }: MiddlewareFunctionProps) => { 11 | // Loop prevention 12 | if (request.nextUrl.pathname.startsWith("/demo")) { 13 | return NextResponse.next(); 14 | } 15 | 16 | request.nextUrl.pathname = "demo/" + request.nextUrl.pathname; 17 | 18 | const response = NextResponse.redirect(request.nextUrl); 19 | 20 | // Set a cookie 21 | response.cookies.set("nemo", "demo"); 22 | 23 | forward(response); 24 | }, 25 | async ({ request }: MiddlewareFunctionProps) => { 26 | console.log(request.cookies.get("nemo")); 27 | }, 28 | ], 29 | } satisfies MiddlewareConfig; 30 | 31 | // Create middlewares helper 32 | export const middleware = createMiddleware(middlewares); 33 | 34 | export const config = { 35 | matcher: ["/((?!api/|_next/|_static|_vercel|[\\w-]+\\.\\w+).*)"], 36 | }; 37 | -------------------------------------------------------------------------------- /examples/cookies/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /examples/cookies/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | }, 23 | "target": "ES2017" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/cookies/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": {} 5 | } 6 | -------------------------------------------------------------------------------- /examples/headers/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/headers/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/headers/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import onlyWarn from "eslint-plugin-only-warn"; 4 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 5 | import turboPlugin from "eslint-plugin-turbo"; 6 | import tseslint from "typescript-eslint"; 7 | 8 | /** 9 | * A shared ESLint configuration for the repository. 10 | * 11 | * @type {import("eslint").Linter.Config} 12 | * */ 13 | export default [ 14 | { 15 | plugins: { 16 | turbo: turboPlugin, 17 | }, 18 | rules: { 19 | "turbo/no-undeclared-env-vars": "warn", 20 | }, 21 | }, 22 | { 23 | plugins: { 24 | onlyWarn, 25 | }, 26 | }, 27 | { 28 | ignores: ["dist/**"], 29 | }, 30 | js.configs.recommended, 31 | eslintConfigPrettier, 32 | eslintPluginPrettierRecommended, 33 | ...tseslint.configs.recommended, 34 | { 35 | ...pluginReact.configs.flat.recommended, 36 | languageOptions: { 37 | ...pluginReact.configs.flat.recommended.languageOptions, 38 | globals: { 39 | ...globals.serviceworker, 40 | }, 41 | }, 42 | }, 43 | { 44 | plugins: { 45 | "@next/next": pluginNext, 46 | }, 47 | rules: { 48 | ...pluginNext.configs.recommended.rules, 49 | ...pluginNext.configs["core-web-vitals"].rules, 50 | }, 51 | }, 52 | { 53 | plugins: { 54 | "react-hooks": pluginReactHooks, 55 | }, 56 | settings: { react: { version: "detect" } }, 57 | rules: { 58 | ...pluginReactHooks.configs.recommended.rules, 59 | // React scope no longer necessary with new JSX transform. 60 | "react/react-in-jsx-scope": "off", 61 | }, 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /examples/headers/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /examples/headers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@rescale/nemo": "workspace:*", 4 | "next": "^15.3.1-canary.0", 5 | "react": "latest", 6 | "react-dom": "latest" 7 | }, 8 | "devDependencies": { 9 | "@types/node": "^20", 10 | "@types/react": "^19.0.1", 11 | "@types/react-dom": "^19.0.2", 12 | "autoprefixer": "10.4.16", 13 | "eslint": "^9", 14 | "eslint-config-next": "14.1.0", 15 | "postcss": "^8", 16 | "tailwindcss": "^3.4.17", 17 | "typescript": "^5.7.3" 18 | }, 19 | "name": "headers-example", 20 | "private": true, 21 | "scripts": { 22 | "examples": "next dev -p 3004", 23 | "lint": "next lint" 24 | }, 25 | "version": "0.1.0" 26 | } -------------------------------------------------------------------------------- /examples/headers/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/headers/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/headers/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/headers/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z4nr34l/nemo/6ec7db02ee36af3002502667ffc0227de73ca534/examples/headers/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/headers/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/headers/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/headers/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { 3 | createMiddleware, 4 | type MiddlewareConfig, 5 | type MiddlewareFunctionProps, 6 | } from "@rescale/nemo"; 7 | 8 | const middlewares = { 9 | "/": [ 10 | async ({ forward }: MiddlewareFunctionProps) => { 11 | console.log("middleware"); 12 | 13 | const response = NextResponse.next(); 14 | 15 | response.headers.set("x-test-header", "test-value"); 16 | response.headers.set("x-another-header", "another-value"); 17 | 18 | forward(response); 19 | }, 20 | async ({ request, response }: MiddlewareFunctionProps) => { 21 | const _response = NextResponse.next(); 22 | 23 | // Copy headers from the request to the response 24 | response?.headers.forEach((value, key) => { 25 | _response.headers.set(key, value); 26 | }); 27 | 28 | // Modify headers to test if they are carried forward 29 | _response.headers.set("x-demo-header", "demo-value"); 30 | 31 | // Check if the previous headers are present 32 | if ( 33 | !response?.headers.has("x-test-header") || 34 | response?.headers.get("x-test-header") !== "test-value" 35 | ) { 36 | _response.headers.set("x-test-header-error", "missing or incorrect"); 37 | } 38 | if ( 39 | !response?.headers.has("x-another-header") || 40 | response?.headers.get("x-another-header") !== "another-value" 41 | ) { 42 | _response.headers.set("x-another-header-error", "missing or incorrect"); 43 | } 44 | 45 | // Returning new response with custom headers to user 46 | return _response; 47 | }, 48 | ], 49 | } satisfies MiddlewareConfig; 50 | 51 | // Create middlewares helper 52 | export const middleware = createMiddleware(middlewares); 53 | 54 | export const config = { 55 | matcher: ["/((?!api/|_next/|_static|_vercel|[\\w-]+\\.\\w+).*)"], 56 | }; 57 | -------------------------------------------------------------------------------- /examples/headers/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /examples/headers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | }, 23 | "target": "ES2017" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/headers/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": {} 5 | } 6 | -------------------------------------------------------------------------------- /examples/next-auth/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env* 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/next-auth/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/next-auth/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import onlyWarn from "eslint-plugin-only-warn"; 4 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 5 | import turboPlugin from "eslint-plugin-turbo"; 6 | import tseslint from "typescript-eslint"; 7 | 8 | /** 9 | * A shared ESLint configuration for the repository. 10 | * 11 | * @type {import("eslint").Linter.Config} 12 | * */ 13 | export default [ 14 | { 15 | plugins: { 16 | turbo: turboPlugin, 17 | }, 18 | rules: { 19 | "turbo/no-undeclared-env-vars": "warn", 20 | }, 21 | }, 22 | { 23 | plugins: { 24 | onlyWarn, 25 | }, 26 | }, 27 | { 28 | ignores: ["dist/**"], 29 | }, 30 | js.configs.recommended, 31 | eslintConfigPrettier, 32 | eslintPluginPrettierRecommended, 33 | ...tseslint.configs.recommended, 34 | { 35 | ...pluginReact.configs.flat.recommended, 36 | languageOptions: { 37 | ...pluginReact.configs.flat.recommended.languageOptions, 38 | globals: { 39 | ...globals.serviceworker, 40 | }, 41 | }, 42 | }, 43 | { 44 | plugins: { 45 | "@next/next": pluginNext, 46 | }, 47 | rules: { 48 | ...pluginNext.configs.recommended.rules, 49 | ...pluginNext.configs["core-web-vitals"].rules, 50 | }, 51 | }, 52 | { 53 | plugins: { 54 | "react-hooks": pluginReactHooks, 55 | }, 56 | settings: { react: { version: "detect" } }, 57 | rules: { 58 | ...pluginReactHooks.configs.recommended.rules, 59 | // React scope no longer necessary with new JSX transform. 60 | "react/react-in-jsx-scope": "off", 61 | }, 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /examples/next-auth/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /examples/next-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@rescale/nemo": "workspace:*", 4 | "next": "^15.3.1-canary.0", 5 | "next-auth": "5.0.0-beta.19", 6 | "react": "latest", 7 | "react-dom": "latest" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "^20", 11 | "@types/react": "^19.0.1", 12 | "@types/react-dom": "^19.0.2", 13 | "autoprefixer": "10.4.16", 14 | "eslint": "^9", 15 | "eslint-config-next": "14.1.0", 16 | "postcss": "^8", 17 | "tailwindcss": "^3.4.17", 18 | "typescript": "^5.7.3" 19 | }, 20 | "name": "next-auth-example", 21 | "private": true, 22 | "scripts": { 23 | "examples": "next dev -p 3002", 24 | "lint": "next lint" 25 | }, 26 | "version": "0.1.0" 27 | } -------------------------------------------------------------------------------- /examples/next-auth/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/next-auth/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-auth/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-auth/src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth"; 2 | 3 | export const { GET, POST } = handlers; 4 | -------------------------------------------------------------------------------- /examples/next-auth/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z4nr34l/nemo/6ec7db02ee36af3002502667ffc0227de73ca534/examples/next-auth/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/next-auth/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/next-auth/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/next-auth/src/auth.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from "next-auth"; 2 | import NextAuth from "next-auth"; 3 | import CredentialsProvider from "next-auth/providers/credentials"; 4 | 5 | export const config = { 6 | theme: { 7 | logo: "https://next-auth.js.org/img/logo/logo-sm.png", 8 | }, 9 | providers: [ 10 | CredentialsProvider({ 11 | name: "Credentials", 12 | credentials: { 13 | username: { label: "Username", type: "text", placeholder: "jsmith" }, 14 | password: { label: "Password", type: "password" }, 15 | }, 16 | async authorize() { 17 | return { id: 1, name: "J Smith", email: "jsmith@example.com" } as never; 18 | }, 19 | }), 20 | ], 21 | callbacks: { 22 | authorized: async ({ auth }) => { 23 | return !!auth; 24 | }, 25 | }, 26 | } satisfies NextAuthConfig; 27 | 28 | export const { handlers, auth, signIn, signOut } = NextAuth(config); 29 | -------------------------------------------------------------------------------- /examples/next-auth/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { auth as authMiddleware } from "@/auth"; 2 | import { 3 | createNEMO, 4 | type GlobalMiddlewareConfig, 5 | type MiddlewareConfig, 6 | } from "@rescale/nemo"; 7 | import { NextResponse } from "next/server"; 8 | 9 | const globalMiddleware: GlobalMiddlewareConfig = { 10 | before: async (request, event) => { 11 | await authMiddleware((_request, _event) => { 12 | const { auth } = _request; 13 | event.storage.set("user", auth?.user); 14 | })(request, event); 15 | } 16 | } 17 | 18 | const middlewares = { 19 | "/page1": [ 20 | async (request) => { 21 | console.log("Middleware for /page1", request.nextUrl.pathname); 22 | const response = NextResponse.next(); 23 | response.headers.set("x-page1-header", "page1-value"); 24 | return response; 25 | }, 26 | async (request) => { 27 | console.log("Chained middleware for /page1", request.nextUrl.pathname); 28 | console.log("Page1 header value:", request.headers.get("x-page1-header")); 29 | }, 30 | ], 31 | "/page2": [ 32 | async (request, event) => { 33 | if (event.storage.get('user')) { 34 | const response = NextResponse.next(); 35 | response.headers.set("x-authenticated", "true"); 36 | return response; 37 | } else { 38 | return NextResponse.json( 39 | { success: false, message: "Unauthorized" }, 40 | { status: 401 } 41 | ); 42 | } 43 | }, 44 | async (request) => { 45 | console.log("Middleware for /page2", request.nextUrl.pathname); 46 | console.log( 47 | "Authenticated header value:", 48 | request.headers.get("x-authenticated") 49 | ); 50 | return NextResponse.redirect("http://localhost:3002/page1"); 51 | }, 52 | ], 53 | } satisfies MiddlewareConfig; 54 | 55 | // Create middlewares helper 56 | export const middleware = createNEMO(middlewares, globalMiddleware); 57 | 58 | export const config = { 59 | matcher: ["/((?!api/|_next/|_static|_vercel|[\\w-]+\\.\\w+).*)"], 60 | }; 61 | -------------------------------------------------------------------------------- /examples/next-auth/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /examples/next-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | }, 23 | "target": "ES2017" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/next-auth/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": {} 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Z4NR34L ", 3 | "bugs": { 4 | "url": "https://github.com/z4nr34l/nemo/issues" 5 | }, 6 | "dependencies": { 7 | "@fumadocs/mdx-remote": "^1.2.0" 8 | }, 9 | "devDependencies": { 10 | "@changesets/cli": "^2.27.12", 11 | "@commitlint/cli": "^19.7.1", 12 | "@commitlint/config-conventional": "^19.7.1", 13 | "husky": "^9.1.7", 14 | "turbo": "^2.5.0" 15 | }, 16 | "homepage": "https://nemo.zanreal.com", 17 | "keywords": [ 18 | "Next.js", 19 | "Middleware", 20 | "Multiple middleware", 21 | "Pathname", 22 | "Route", 23 | "Path" 24 | ], 25 | "license": "MIT", 26 | "name": "nemo", 27 | "packageManager": "bun@1.2.2", 28 | "publishConfig": { 29 | "provenance": true 30 | }, 31 | "repository": "https://github.com/z4nr34l/nemo", 32 | "scripts": { 33 | "build": "turbo build", 34 | "clean": "turbo clean", 35 | "dev": "turbo dev --concurrency 20", 36 | "examples": "turbo examples", 37 | "prepare": "husky", 38 | "release": "turbo build --filter='@rescale/nemo' && changeset publish", 39 | "version-packages": "changeset version" 40 | }, 41 | "trustedDependencies": [ 42 | "@swc/core", 43 | "docs" 44 | ], 45 | "workspaces": [ 46 | "packages/*", 47 | "examples/*", 48 | "apps/*" 49 | ] 50 | } -------------------------------------------------------------------------------- /packages/nemo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .turbo 5 | -------------------------------------------------------------------------------- /packages/nemo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # NEMO 2 | 3 | ## 2.0.1 4 | 5 | ### Patch Changes 6 | 7 | - 75d4a18: Fixed module resolution due to missing storage primitives and adapters 8 | 9 | ## 2.0.0 10 | 11 | ### Major Changes 12 | 13 | - a61236e: # Breaking Changes 14 | 15 | - Complete package refactoring with potential API changes 16 | - Migration from custom solution to Next.js native middleware API 17 | 18 | # Improvements 19 | 20 | - Achieved 100% test coverage for improved reliability 21 | - Enhanced performance and maintainability through code refactoring 22 | - Better integration with Next.js ecosystem 23 | 24 | # Technical Details 25 | 26 | - Restructured codebase architecture for better maintainability 27 | - Implemented comprehensive test suite with full coverage 28 | - Updated middleware implementation to leverage Next.js native capabilities 29 | 30 | ## 1.4 31 | 32 | ### Minor Changes 33 | 34 | - b4ce176: Added optional response prop that contains last forwarded function's response 35 | 36 | ## 1.3.3 37 | 38 | ### Patch Changes 39 | 40 | - 0d32698: Updated npmjs readme 41 | 42 | ## 1.3.2 43 | 44 | ### Patch Changes 45 | 46 | - 1d739d6: Added params to middleware functions, improved docs 47 | 48 | ## 1.3.1 49 | 50 | ### Patch Changes 51 | 52 | - 9b19520: Fixed headers forwarding due to server actions issues 53 | 54 | ## 1.3.0 55 | 56 | ### Minor Changes 57 | 58 | - 8518674: Fixed many issues, added tests, improving docs 59 | 60 | ## 1.2.4 61 | 62 | ### Patch Changes 63 | 64 | - d171568: Bump version of `@rescale/nemo` package to 1.2.3. 65 | 66 | ## 1.2.2 67 | 68 | ### Patch Changes 69 | 70 | - 403c89d: Fixed global middlewares type to requiere at least one (before or after), not both 71 | 72 | ## 1.2.1 73 | 74 | ### Patch Changes 75 | 76 | - 5a48796: Renamed package for easier to remember name 77 | 78 | ## 1.2.0 79 | 80 | ### Minor Changes 81 | 82 | - 7c85643: Added middleware shared context and refactored middleware function props to object for more elastic approach 83 | 84 | Global middlewares now support chaining 85 | 86 | ### Patch Changes 87 | 88 | - be63923: Added support for NextFetchEvent in middleware - next15 event.waitUntil 89 | 90 | Improved peerDeps config and types compatibility 91 | 92 | Removed default export - supports only named exports from now 93 | 94 | ## 1.1.6 95 | 96 | ### Patch Changes 97 | 98 | - ecc3827: Adding provenance 99 | - fe9e197: Adding provenance 100 | 101 | ## 1.1.5 102 | 103 | ### Patch Changes 104 | 105 | - 65a1e1a: Automating package publishing 106 | -------------------------------------------------------------------------------- /packages/nemo/__tests__/edge-cases.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, mock, test } from "bun:test"; 2 | import type { NextFetchEvent } from "next/server"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | import { NEMO, type NextMiddleware } from "../src"; 5 | 6 | describe("NEMO Edge Cases", () => { 7 | const mockRequest = (path: string = "/") => 8 | new NextRequest(`http://localhost${path}`); 9 | 10 | const mockEvent: NextFetchEvent = { 11 | waitUntil: mock(() => {}), 12 | } as never as NextFetchEvent; 13 | 14 | test("should handle deeply nested paths", async () => { 15 | const middleware = mock(() => NextResponse.next()); 16 | const nemo = new NEMO({ 17 | "/very/deep/nested/:param/path": middleware, 18 | }); 19 | 20 | await nemo.middleware(mockRequest("/very/deep/nested/123/path"), mockEvent); 21 | expect(middleware).toHaveBeenCalled(); 22 | }); 23 | 24 | test("should handle concurrent requests", async () => { 25 | const order: number[] = []; 26 | const slowMiddleware: NextMiddleware = async () => { 27 | await new Promise((resolve) => setTimeout(resolve, 50)); 28 | order.push(1); 29 | }; 30 | const fastMiddleware: NextMiddleware = async () => { 31 | await new Promise((resolve) => setTimeout(resolve, 10)); 32 | order.push(2); 33 | }; 34 | 35 | const nemo = new NEMO({ 36 | "/slow": slowMiddleware, 37 | "/fast": fastMiddleware, 38 | }); 39 | 40 | await Promise.all([ 41 | nemo.middleware(mockRequest("/slow"), mockEvent), 42 | nemo.middleware(mockRequest("/fast"), mockEvent), 43 | ]); 44 | 45 | expect(order).toEqual([2, 1]); 46 | }); 47 | 48 | test("should handle empty middleware arrays", async () => { 49 | const nemo = new NEMO({ "/": [] }); 50 | const response = await nemo.middleware(mockRequest(), mockEvent); 51 | expect(response instanceof NextResponse).toBe(true); 52 | }); 53 | 54 | test("should handle unicode paths", async () => { 55 | const middleware = mock(() => NextResponse.next()); 56 | const nemo = new NEMO({ 57 | "/测试/:param/路径": middleware, 58 | }); 59 | 60 | await nemo.middleware(mockRequest("/测试/123/路径"), mockEvent); 61 | expect(middleware).toHaveBeenCalled(); 62 | }); 63 | 64 | test("should handle very large headers", async () => { 65 | const largeValue = "x".repeat(10000); 66 | const middleware: NextMiddleware = (req) => { 67 | req.headers.set("x-large", largeValue); 68 | }; 69 | 70 | const nemo = new NEMO({ "/": middleware }); 71 | const response = await nemo.middleware(mockRequest(), mockEvent); 72 | 73 | expect(response?.headers.get("x-large")).toBe(largeValue); 74 | }); 75 | 76 | test("should handle middleware returning undefined", async () => { 77 | const middleware: NextMiddleware = () => undefined; 78 | const nemo = new NEMO({ "/": middleware }); 79 | 80 | const response = await nemo.middleware(mockRequest(), mockEvent); 81 | expect(response instanceof NextResponse).toBe(true); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/nemo/__tests__/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { NemoMiddlewareError } from "../src/errors"; 3 | import type { MiddlewareMetadata } from "../src/types"; 4 | 5 | describe("NemoMiddlewareError", () => { 6 | test("should create error with context", () => { 7 | const metadata = { 8 | chain: "main", 9 | index: 0, 10 | pathname: "/test", 11 | routeKey: "/test", 12 | } satisfies MiddlewareMetadata; 13 | const originalError = new Error("Test error"); 14 | 15 | const error = new NemoMiddlewareError( 16 | "Test message", 17 | metadata, 18 | originalError, 19 | ); 20 | 21 | expect(error.message).toBe( 22 | "Test message [main chain at path /test (matched by /test), index 0]", 23 | ); 24 | expect(error.metadata).toEqual(metadata); 25 | expect(error.originalError).toBe(originalError); 26 | }); 27 | 28 | test("should create error without original error", () => { 29 | const metadata = { 30 | chain: "main", 31 | index: 0, 32 | pathname: "/test", 33 | routeKey: "/test", 34 | } satisfies MiddlewareMetadata; 35 | 36 | const error = new NemoMiddlewareError("Test message", metadata); 37 | 38 | expect(error.message).toBe( 39 | "Test message [main chain at path /test (matched by /test), index 0]", 40 | ); 41 | expect(error.metadata).toEqual(metadata); 42 | expect(error.originalError).toBeUndefined(); 43 | }); 44 | 45 | test("should be instance of Error", () => { 46 | const error = new NemoMiddlewareError("Test message", { 47 | chain: "main", 48 | index: 0, 49 | pathname: "/test", 50 | routeKey: "/test", 51 | }); 52 | 53 | expect(error).toBeInstanceOf(Error); 54 | expect(error).toBeInstanceOf(NemoMiddlewareError); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /packages/nemo/__tests__/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; 2 | import { Logger } from "../src/logger"; 3 | 4 | describe("Logger", () => { 5 | const originalConsoleLog = console.log; 6 | const originalConsoleError = console.error; 7 | const originalConsoleWarn = console.warn; 8 | let consoleLogSpy: typeof console.log; 9 | let consoleErrorSpy: typeof console.error; 10 | let consoleWarnSpy: typeof console.warn; 11 | 12 | beforeEach(() => { 13 | consoleLogSpy = mock((...args: any[]) => {}); 14 | consoleErrorSpy = mock((...args: any[]) => {}); 15 | consoleWarnSpy = mock((...args: any[]) => {}); 16 | console.log = consoleLogSpy; 17 | console.error = consoleErrorSpy; 18 | console.warn = consoleWarnSpy; 19 | }); 20 | 21 | afterEach(() => { 22 | console.log = originalConsoleLog; 23 | console.error = originalConsoleError; 24 | console.warn = originalConsoleWarn; 25 | }); 26 | 27 | test("should not log when debug is disabled", () => { 28 | const logger = new Logger(false); 29 | logger.log("test message"); 30 | expect(consoleLogSpy).not.toHaveBeenCalled(); 31 | }); 32 | 33 | test("should log when debug is enabled", () => { 34 | const logger = new Logger(true); 35 | logger.log("test message"); 36 | expect(consoleLogSpy).toHaveBeenCalledWith("[NEMO]", "test message"); 37 | }); 38 | 39 | test("should log multiple arguments", () => { 40 | const logger = new Logger(true); 41 | logger.log("test message", { data: 123 }, "extra"); 42 | expect(consoleLogSpy).toHaveBeenCalledWith( 43 | "[NEMO]", 44 | "test message", 45 | { data: 123 }, 46 | "extra", 47 | ); 48 | }); 49 | 50 | test("should always log errors regardless of debug mode", () => { 51 | const debugLogger = new Logger(true); 52 | const nonDebugLogger = new Logger(false); 53 | const error = new Error("test error"); 54 | 55 | debugLogger.error("error message", error); 56 | nonDebugLogger.error("error message", error); 57 | 58 | expect(consoleErrorSpy).toHaveBeenCalledTimes(2); 59 | expect(consoleErrorSpy).toHaveBeenCalledWith( 60 | "[NEMO]", 61 | "error message", 62 | error, 63 | ); 64 | }); 65 | 66 | test("should always log warnings regardless of debug mode", () => { 67 | const debugLogger = new Logger(true); 68 | const nonDebugLogger = new Logger(false); 69 | 70 | debugLogger.warn("warning message", { details: "test" }); 71 | nonDebugLogger.warn("warning message", { details: "test" }); 72 | 73 | expect(consoleWarnSpy).toHaveBeenCalledTimes(2); 74 | expect(consoleWarnSpy).toHaveBeenCalledWith("[NEMO]", "warning message", { 75 | details: "test", 76 | }); 77 | }); 78 | 79 | test("should handle empty arguments", () => { 80 | const logger = new Logger(true); 81 | 82 | logger.log(); 83 | logger.error(); 84 | logger.warn(); 85 | 86 | expect(consoleLogSpy).toHaveBeenCalledWith("[NEMO]"); 87 | expect(consoleErrorSpy).toHaveBeenCalledWith("[NEMO]"); 88 | expect(consoleWarnSpy).toHaveBeenCalledWith("[NEMO]"); 89 | }); 90 | 91 | test("should handle undefined and null arguments", () => { 92 | const logger = new Logger(true); 93 | 94 | logger.log(undefined, null); 95 | logger.error(undefined, null); 96 | logger.warn(undefined, null); 97 | 98 | expect(consoleLogSpy).toHaveBeenCalledWith("[NEMO]", undefined, null); 99 | expect(consoleErrorSpy).toHaveBeenCalledWith("[NEMO]", undefined, null); 100 | expect(consoleWarnSpy).toHaveBeenCalledWith("[NEMO]", undefined, null); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /packages/nemo/__tests__/path-matching.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, mock, test } from "bun:test"; 2 | import type { NextFetchEvent } from "next/server"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | import { NEMO } from "../src"; 5 | 6 | describe("NEMO Path Matching", () => { 7 | const mockRequest = (path: string = "/") => 8 | new NextRequest(`http://localhost${path}`); 9 | 10 | const mockEvent: NextFetchEvent = { 11 | waitUntil: mock(() => {}), 12 | } as never as NextFetchEvent; 13 | 14 | test("should handle invalid regex patterns gracefully", async () => { 15 | // Test error handling in matchesPath method 16 | // Use a pattern that would cause pathToRegexp to throw an error 17 | // For example, unbalanced parentheses or invalid regex syntax 18 | const invalidPattern = "/(test(/:id"; 19 | const middleware = mock(() => NextResponse.next()); 20 | 21 | // Access private methods for testing 22 | const nemo = new NEMO({ [invalidPattern]: middleware }); 23 | 24 | // This should throw 25 | await expect( 26 | nemo.middleware(mockRequest("/test/123"), mockEvent), 27 | ).resolves.toThrow(); 28 | 29 | // Middleware should not be called since pattern is invalid 30 | expect(middleware).not.toHaveBeenCalled(); 31 | }); 32 | 33 | test("should handle URL-encoded characters in paths", async () => { 34 | const middleware = mock(() => NextResponse.next()); 35 | const nemo = new NEMO({ "/test/:name": middleware }); 36 | 37 | await nemo.middleware(mockRequest("/test/john%20doe"), mockEvent); 38 | expect(middleware).toHaveBeenCalled(); 39 | }); 40 | 41 | test("should handle malformed URI components", async () => { 42 | const middleware = mock(() => NextResponse.next()); 43 | const nemo = new NEMO({ "/user/:name": middleware }); 44 | 45 | // Use a path with malformed percent encoding 46 | // This would normally cause decodeURIComponent to throw 47 | const badPath = "/user/bad%2-encoding"; 48 | 49 | // This should throw 50 | await expect( 51 | nemo.middleware(new NextRequest(`http://localhost${badPath}`), mockEvent), 52 | ).resolves.toThrow(); 53 | }); 54 | 55 | test("should handle unicode path patterns correctly", async () => { 56 | const middleware = mock(() => NextResponse.next()); 57 | // Use a pattern with unicode characters 58 | const nemo = new NEMO({ "/café/:item": middleware }); 59 | 60 | await nemo.middleware(mockRequest("/café/croissant"), mockEvent); 61 | expect(middleware).toHaveBeenCalled(); 62 | }); 63 | 64 | test("should match parameters correctly", async () => { 65 | const middleware = mock((req, event) => { 66 | expect(event.params).toEqual({ 67 | category: "electronics", 68 | id: "123", 69 | }); 70 | return NextResponse.next(); 71 | }); 72 | 73 | const nemo = new NEMO({ 74 | "/products/:category/:id": middleware, 75 | }); 76 | 77 | await nemo.middleware(mockRequest("/products/electronics/123"), mockEvent); 78 | expect(middleware).toHaveBeenCalled(); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/nemo/__tests__/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, test } from "bun:test"; 2 | import { MemoryStorageAdapter } from "../src/storage/adapters/memory"; 3 | 4 | describe("MemoryStorageAdapter", () => { 5 | let storage: MemoryStorageAdapter; 6 | 7 | beforeEach(() => { 8 | storage = new MemoryStorageAdapter(); 9 | }); 10 | 11 | describe("basic operations", () => { 12 | test("should set and get values", () => { 13 | storage.set("key", "value"); 14 | expect(storage.get("key")).toBe("value"); 15 | }); 16 | 17 | test("should return undefined for non-existent keys", () => { 18 | expect(storage.get("nonexistent")).toBeUndefined(); 19 | }); 20 | 21 | test("should check if key exists", () => { 22 | storage.set("key", "value"); 23 | expect(storage.has("key")).toBe(true); 24 | expect(storage.has("nonexistent")).toBe(false); 25 | }); 26 | 27 | test("should delete keys", () => { 28 | storage.set("key", "value"); 29 | expect(storage.delete("key")).toBe(true); 30 | expect(storage.has("key")).toBe(false); 31 | expect(storage.delete("nonexistent")).toBe(false); 32 | }); 33 | 34 | test("should clear all entries", () => { 35 | storage.set("key1", "value1"); 36 | storage.set("key2", "value2"); 37 | storage.clear(); 38 | expect(storage.size).toBe(0); 39 | expect(storage.has("key1")).toBe(false); 40 | }); 41 | }); 42 | 43 | describe("iteration methods", () => { 44 | beforeEach(() => { 45 | storage.set("key1", "value1"); 46 | storage.set("key2", "value2"); 47 | }); 48 | 49 | test("should iterate over entries", () => { 50 | const entries = Array.from(storage.entries()); 51 | expect(entries).toEqual([ 52 | ["key1", "value1"], 53 | ["key2", "value2"], 54 | ]); 55 | }); 56 | 57 | test("should iterate over keys", () => { 58 | const keys = Array.from(storage.keys()); 59 | expect(keys).toEqual(["key1", "key2"]); 60 | }); 61 | 62 | test("should iterate over values", () => { 63 | const values = Array.from(storage.values()); 64 | expect(values).toEqual(["value1", "value2"]); 65 | }); 66 | 67 | test("should report correct size", () => { 68 | expect(storage.size).toBe(2); 69 | }); 70 | }); 71 | 72 | describe("serialization", () => { 73 | test("should convert to string", () => { 74 | storage.set("key1", "value1"); 75 | storage.set("key2", 42); 76 | const str = storage.toString(); 77 | expect(JSON.parse(str)).toEqual({ 78 | key1: "value1", 79 | key2: 42, 80 | }); 81 | }); 82 | 83 | test("should load from string", () => { 84 | const json = '{"key1":"value1","key2":42}'; 85 | expect(storage.fromString(json)).toBe(true); 86 | expect(storage.get("key1")).toBe("value1"); 87 | expect(storage.get("key2")).toBe(42); 88 | }); 89 | 90 | test("should handle invalid JSON strings", () => { 91 | expect(storage.fromString("invalid json")).toBe(false); 92 | expect(storage.fromString("null")).toBe(false); 93 | expect(storage.fromString('"string"')).toBe(false); 94 | }); 95 | }); 96 | 97 | describe("fromEntries", () => { 98 | test("should load from entries", () => { 99 | const entries = [ 100 | ["key1", "value1"], 101 | ["key2", 42], 102 | ] as const; 103 | 104 | storage.fromEntries(entries); 105 | expect(storage.get("key1")).toBe("value1"); 106 | expect(storage.get("key2")).toBe(42); 107 | }); 108 | 109 | test("should override existing entries", () => { 110 | storage.set("existing", "old"); 111 | storage.fromEntries([["existing", "new"]]); 112 | expect(storage.get("existing")).toBe("new"); 113 | }); 114 | }); 115 | 116 | describe("type safety", () => { 117 | test("should preserve value types", () => { 118 | storage.set("string", "value"); 119 | storage.set("number", 42); 120 | storage.set("boolean", true); 121 | storage.set("object", { key: "value" }); 122 | storage.set("array", [1, 2, 3]); 123 | 124 | expect(storage.get("string")).toBe("value"); 125 | expect(storage.get("number")).toBe(42); 126 | expect(storage.get("boolean")).toBe(true); 127 | expect(storage.get("object")).toEqual({ key: "value" }); 128 | expect(storage.get("array")).toEqual([1, 2, 3]); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /packages/nemo/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { NextResponse } from "next/server"; 3 | import { areResponsesEqual } from "../src/utils"; 4 | 5 | describe("NEMO Utils", () => { 6 | describe("areResponsesEqual", () => { 7 | test("should return true for identical response objects", () => { 8 | const response1 = NextResponse.next(); 9 | const response2 = response1; 10 | expect(areResponsesEqual(response1, response2)).toBe(true); 11 | }); 12 | 13 | test("should return true for equivalent response objects", () => { 14 | const response1 = NextResponse.next({ 15 | headers: { "x-custom-header": "value" }, 16 | }); 17 | 18 | const response2 = NextResponse.next({ 19 | headers: { "x-custom-header": "value" }, 20 | }); 21 | 22 | expect(areResponsesEqual(response1, response2)).toBe(true); 23 | }); 24 | 25 | test("should return false for responses with different status codes", () => { 26 | const response1 = NextResponse.next(); 27 | const response2 = new NextResponse(null, { status: 404 }); 28 | 29 | expect(areResponsesEqual(response1, response2)).toBe(false); 30 | }); 31 | 32 | test("should return false for responses with different status text", () => { 33 | const response1 = new NextResponse(null, { 34 | status: 200, 35 | statusText: "OK", 36 | }); 37 | 38 | const response2 = new NextResponse(null, { 39 | status: 200, 40 | statusText: "Custom Status", 41 | }); 42 | 43 | expect(areResponsesEqual(response1, response2)).toBe(false); 44 | }); 45 | 46 | test("should handle null or undefined responses", () => { 47 | const response = NextResponse.next(); 48 | 49 | expect(areResponsesEqual(response, null)).toBe(false); 50 | expect(areResponsesEqual(null, response)).toBe(false); 51 | expect(areResponsesEqual(null, null)).toBe(true); 52 | expect(areResponsesEqual(undefined, undefined)).toBe(true); 53 | }); 54 | 55 | test("should compare redirect URLs properly", () => { 56 | const redirect1 = NextResponse.redirect("http://example.com/page1"); 57 | const redirect2 = NextResponse.redirect("http://example.com/page1"); 58 | const redirect3 = NextResponse.redirect("http://example.com/page2"); 59 | 60 | expect(areResponsesEqual(redirect1, redirect2)).toBe(true); 61 | expect(areResponsesEqual(redirect1, redirect3)).toBe(false); 62 | }); 63 | 64 | test("should compare headers with different counts", () => { 65 | const response1 = NextResponse.next({ 66 | headers: { 67 | "x-header-1": "value1", 68 | "x-header-2": "value2", 69 | }, 70 | }); 71 | 72 | const response2 = NextResponse.next({ 73 | headers: { 74 | "x-header-1": "value1", 75 | }, 76 | }); 77 | 78 | expect(areResponsesEqual(response1, response2)).toBe(false); 79 | }); 80 | 81 | test("should compare headers with same keys but different values", () => { 82 | const response1 = NextResponse.next({ 83 | headers: { 84 | "x-header-1": "value1", 85 | "x-header-2": "value2", 86 | }, 87 | }); 88 | 89 | const response2 = NextResponse.next({ 90 | headers: { 91 | "x-header-1": "value1", 92 | "x-header-2": "different", 93 | }, 94 | }); 95 | 96 | expect(areResponsesEqual(response1, response2)).toBe(false); 97 | }); 98 | 99 | test("should handle responses with complex headers", () => { 100 | const response1 = NextResponse.next(); 101 | response1.headers.set("x-custom-1", "value1"); 102 | response1.headers.set("x-custom-2", "value2"); 103 | 104 | const response2 = NextResponse.next(); 105 | response2.headers.set("x-custom-1", "value1"); 106 | response2.headers.set("x-custom-2", "value2"); 107 | 108 | expect(areResponsesEqual(response1, response2)).toBe(true); 109 | 110 | // Modify one header 111 | response2.headers.set("x-custom-2", "modified"); 112 | expect(areResponsesEqual(response1, response2)).toBe(false); 113 | }); 114 | 115 | test("should handle JSON responses", () => { 116 | const json1 = NextResponse.json({ data: "test" }); 117 | const json2 = NextResponse.json({ data: "test" }); 118 | const json3 = NextResponse.json({ data: "different" }); 119 | 120 | expect(areResponsesEqual(json1, json2)).toBe(true); 121 | expect(areResponsesEqual(json1, json3)).toBe(true); // Should be true as we don't compare body content 122 | }); 123 | 124 | test("should handle rewrite responses", () => { 125 | const response1 = NextResponse.next(); 126 | response1.headers.set( 127 | "x-middleware-rewrite", 128 | "http://example.com/new-path", 129 | ); 130 | 131 | const response2 = NextResponse.next(); 132 | response2.headers.set( 133 | "x-middleware-rewrite", 134 | "http://example.com/new-path", 135 | ); 136 | 137 | const response3 = NextResponse.next(); 138 | response3.headers.set( 139 | "x-middleware-rewrite", 140 | "http://example.com/different-path", 141 | ); 142 | 143 | expect(areResponsesEqual(response1, response2)).toBe(true); 144 | expect(areResponsesEqual(response1, response3)).toBe(false); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /packages/nemo/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import onlyWarn from "eslint-plugin-only-warn"; 4 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; 5 | import pluginReact from "eslint-plugin-react"; 6 | import pluginReactHooks from "eslint-plugin-react-hooks"; 7 | import pluginSecurity from "eslint-plugin-security"; 8 | import sonarjs from 'eslint-plugin-sonarjs'; 9 | import turboPlugin from "eslint-plugin-turbo"; 10 | import globals from "globals"; 11 | import tseslint from "typescript-eslint"; 12 | 13 | /** 14 | * A shared ESLint configuration for the repository. 15 | * 16 | * @type {import("eslint").Linter.Config} 17 | * */ 18 | export const config = [ 19 | { 20 | plugins: { 21 | turbo: turboPlugin, 22 | }, 23 | rules: { 24 | "turbo/no-undeclared-env-vars": "warn", 25 | }, 26 | }, 27 | { 28 | plugins: { 29 | onlyWarn, 30 | }, 31 | }, 32 | { 33 | ignores: ["dist/**"], 34 | }, 35 | js.configs.recommended, 36 | eslintConfigPrettier, 37 | eslintPluginPrettierRecommended, 38 | pluginSecurity.configs.recommended, 39 | ...tseslint.configs.recommended, 40 | { 41 | plugins: { sonarjs }, 42 | rules: { 43 | 'sonarjs/no-implicit-dependencies': 'error', 44 | }, 45 | }, 46 | { 47 | ...pluginReact.configs.flat.recommended, 48 | languageOptions: { 49 | ...pluginReact.configs.flat.recommended.languageOptions, 50 | globals: { 51 | ...globals.serviceworker, 52 | }, 53 | }, 54 | }, 55 | { 56 | plugins: { 57 | "react-hooks": pluginReactHooks, 58 | }, 59 | settings: { react: { version: "detect" } }, 60 | rules: { 61 | ...pluginReactHooks.configs.recommended.rules, 62 | // React scope no longer necessary with new JSX transform. 63 | "react/react-in-jsx-scope": "off", 64 | }, 65 | }, 66 | ]; 67 | 68 | export default config; -------------------------------------------------------------------------------- /packages/nemo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rescale/nemo", 3 | "version": "2.0.1", 4 | "keywords": [ 5 | "Next.js", 6 | "Middleware", 7 | "Multiple middleware", 8 | "Pathname", 9 | "Route", 10 | "Path" 11 | ], 12 | "homepage": "https://nemo.zanreal.com", 13 | "bugs": { 14 | "url": "https://github.com/z4nr34l/nemo/issues" 15 | }, 16 | "repository": "https://github.com/z4nr34l/nemo", 17 | "license": "MIT", 18 | "author": "Mateusz Janota ", 19 | "type": "module", 20 | "main": "./dist/index.js", 21 | "types": "./dist/index.d.ts", 22 | "files": [ 23 | "dist", 24 | "README.md", 25 | "package.json", 26 | "CHANGELOG.md" 27 | ], 28 | "scripts": { 29 | "build": "tsup --clean --splitting", 30 | "clean": "rm -rf node_modules && rm -rf .turbo && rm -rf dist", 31 | "dev": "tsup --watch", 32 | "lint": "eslint \"**/*.ts*\"", 33 | "test": "jest --coverage" 34 | }, 35 | "exports": { 36 | ".": { 37 | "require": "./dist/index.js", 38 | "import": "./dist/index.js", 39 | "types": "./dist/index.d.ts" 40 | }, 41 | "./storage": { 42 | "require": "./dist/storage/index.js", 43 | "import": "./dist/storage/index.js", 44 | "types": "./dist/storage/index.d.ts" 45 | }, 46 | "./storage/adapters/memory": { 47 | "require": "./dist/storage/adapters/memory/index.js", 48 | "import": "./dist/storage/adapters/memory/index.js", 49 | "types": "./dist/storage/adapters/memory/index.d.ts" 50 | }, 51 | "./package.json": "./package.json" 52 | }, 53 | "dependencies": { 54 | "path-to-regexp": "^6.1.0" 55 | }, 56 | "devDependencies": { 57 | "@eslint/js": "^9.17.0", 58 | "@next/eslint-plugin-next": "^15.1.0", 59 | "@swc/core": "^1.5.24", 60 | "@types/bun": "^1.2.2", 61 | "@types/node": "^22.13.1", 62 | "@types/react": "^19.0.1", 63 | "eslint": "^9.15.0", 64 | "eslint-config-prettier": "^10.0.1", 65 | "eslint-plugin-only-warn": "^1.1.0", 66 | "eslint-plugin-prettier": "^5.2.2", 67 | "eslint-plugin-react": "^7.37.2", 68 | "eslint-plugin-react-hooks": "^5.0.0", 69 | "eslint-plugin-turbo": "^2.3.0", 70 | "eslint-plugin-security": "^3.0.1", 71 | "eslint-plugin-sonarjs": "^3.0.2", 72 | "globals": "^16.0.0", 73 | "jest": "^29.7.0", 74 | "prettier": "^3.4.2", 75 | "tsup": "^8.1.0", 76 | "typescript": "^5.7.3", 77 | "typescript-eslint": "^8.24.0" 78 | }, 79 | "peerDependencies": { 80 | "next": "^13.0.0-0 || ^14.0.0-0 || ^15.0.0-0" 81 | }, 82 | "publishConfig": { 83 | "provenance": true 84 | } 85 | } -------------------------------------------------------------------------------- /packages/nemo/src/errors.ts: -------------------------------------------------------------------------------- 1 | import type { MiddlewareMetadata } from "./types"; 2 | 3 | export class NemoMiddlewareError extends Error { 4 | constructor( 5 | message: string, 6 | public readonly metadata: MiddlewareMetadata, 7 | public readonly originalError?: unknown, 8 | ) { 9 | super( 10 | `${message} [${metadata.chain} chain at path ${metadata.pathname}${ 11 | metadata.routeKey ? ` (matched by ${metadata.routeKey})` : "" 12 | }, index ${metadata.index}]`, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/nemo/src/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any -- library */ 2 | export class Logger { 3 | private readonly debug: boolean; 4 | private readonly prefix: string = "[NEMO]"; 5 | 6 | constructor(debug: boolean) { 7 | this.debug = debug; 8 | } 9 | 10 | log(...args: any[]) { 11 | if (this.debug) { 12 | console.log(this.prefix, ...args); 13 | } 14 | } 15 | 16 | error(...args: any[]) { 17 | console.error(this.prefix, ...args); 18 | } 19 | 20 | warn(...args: any[]) { 21 | console.warn(this.prefix, ...args); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/nemo/src/storage/adapter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars -- library */ 2 | export abstract class StorageAdapter { 3 | abstract get(key: string): T | undefined; 4 | abstract set(key: string, value: T): void; 5 | abstract has(key: string): boolean; 6 | abstract delete(key: string): boolean; 7 | abstract clear(): void; 8 | abstract entries(): IterableIterator<[string, unknown]>; 9 | abstract keys(): IterableIterator; 10 | abstract values(): IterableIterator; 11 | abstract get size(): number; 12 | abstract fromEntries(entries: Iterable): void; 13 | abstract toString(): string; 14 | abstract fromString(json: string): boolean; 15 | 16 | constructor(initialContext?: Record) {} 17 | } 18 | -------------------------------------------------------------------------------- /packages/nemo/src/storage/adapters/memory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any -- library */ 2 | /* eslint-disable security/detect-object-injection -- library */ 3 | import { StorageAdapter } from "../adapter"; 4 | 5 | export class MemoryStorageAdapter extends StorageAdapter { 6 | private storage: Record = {}; 7 | 8 | constructor(initialContext?: Record) { 9 | super(); 10 | if (initialContext) { 11 | this.storage = { ...initialContext }; 12 | } 13 | } 14 | 15 | get(key: string): T | any { 16 | return this.storage[key] as T; 17 | } 18 | 19 | set(key: string, value: T): void { 20 | this.storage[key] = value; 21 | } 22 | 23 | has(key: string): boolean { 24 | return key in this.storage; 25 | } 26 | 27 | delete(key: string): boolean { 28 | const exists = key in this.storage; 29 | delete this.storage[key]; 30 | return exists; 31 | } 32 | 33 | clear(): void { 34 | this.storage = {}; 35 | } 36 | 37 | entries(): IterableIterator<[string, unknown]> { 38 | return Object.entries(this.storage)[Symbol.iterator](); 39 | } 40 | 41 | keys(): IterableIterator { 42 | return Object.keys(this.storage)[Symbol.iterator](); 43 | } 44 | 45 | values(): IterableIterator { 46 | return Object.values(this.storage)[Symbol.iterator](); 47 | } 48 | 49 | get size(): number { 50 | return Object.keys(this.storage).length; 51 | } 52 | 53 | fromEntries(entries: Iterable): void { 54 | this.storage = Object.fromEntries(entries); 55 | } 56 | 57 | toString(): string { 58 | return JSON.stringify(this.storage); 59 | } 60 | 61 | fromString(json: string): boolean { 62 | try { 63 | const parsed = JSON.parse(json); 64 | if (typeof parsed !== "object" || parsed === null) { 65 | return false; 66 | } 67 | this.storage = parsed; 68 | return true; 69 | } catch { 70 | return false; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/nemo/src/storage/index.ts: -------------------------------------------------------------------------------- 1 | export { StorageAdapter } from "./adapter"; 2 | export { MemoryStorageAdapter } from "./adapters/memory"; 3 | -------------------------------------------------------------------------------- /packages/nemo/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest, NextResponse } from "next/server"; 2 | import type { NemoEvent } from "./event"; 3 | import type { StorageAdapter } from "./storage/adapter"; 4 | 5 | export type NextMiddlewareResult = 6 | | NextResponse 7 | | Response 8 | | null 9 | | undefined 10 | | void; 11 | 12 | export type NextMiddleware = ( 13 | request: NextRequest, 14 | event: NemoEvent, 15 | ) => NextMiddlewareResult | Promise; 16 | 17 | export type MiddlewareContext = Map; 18 | 19 | export type ErrorHandler = ( 20 | error: Error, 21 | metadata: MiddlewareMetadata, 22 | ) => NextMiddlewareResult | Promise; 23 | 24 | export type MiddlewareChain = NextMiddleware | NextMiddleware[]; 25 | 26 | export type MiddlewareConfigValue = 27 | | NextMiddleware 28 | | NextMiddleware[] 29 | | { [key: `/${string}`]: MiddlewareConfigValue } 30 | | { 31 | middleware: NextMiddleware | NextMiddleware[]; 32 | [key: `/${string}`]: MiddlewareConfigValue; 33 | }; 34 | 35 | export type MiddlewareConfig = Record; 36 | 37 | export type GlobalMiddlewareConfig = Partial< 38 | Record<"before" | "after", MiddlewareChain> 39 | >; 40 | 41 | export interface Storage { 42 | get(key: string): Promise; 43 | set(key: string, value: unknown): Promise; 44 | delete(key: string): Promise; 45 | clear(): Promise; 46 | } 47 | 48 | export interface NemoConfig { 49 | debug?: boolean; 50 | silent?: boolean; 51 | errorHandler?: ErrorHandler; 52 | enableTiming?: boolean; 53 | storage?: StorageAdapter | (() => StorageAdapter); // Updated storage type 54 | } 55 | 56 | export interface MiddlewareMetadata { 57 | chain: "before" | "main" | "after"; 58 | index: number; 59 | pathname: string; 60 | routeKey: string; 61 | nestLevel?: number; 62 | } 63 | 64 | export type NextMiddlewareWithMeta = NextMiddleware & { 65 | __nemo?: MiddlewareMetadata; 66 | }; 67 | -------------------------------------------------------------------------------- /packages/nemo/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { NextMiddlewareResult } from "./types"; 2 | 3 | /** 4 | * Compares two NextResponse instances for deep equality 5 | * 6 | * @param response1 - First NextResponse to compare 7 | * @param response2 - Second NextResponse to compare 8 | * @returns boolean indicating if the responses are equivalent 9 | */ 10 | export function areResponsesEqual( 11 | response1: NextMiddlewareResult, 12 | response2: NextMiddlewareResult, 13 | ): boolean { 14 | // If both are null/undefined or references to the same object, they're equal 15 | if (response1 === response2) return true; 16 | 17 | // If only one is null/undefined, they're not equal 18 | if (!response1 || !response2) return false; 19 | 20 | // Compare status codes and text 21 | if ( 22 | response1.status !== response2.status || 23 | response1.statusText !== response2.statusText 24 | ) { 25 | return false; 26 | } 27 | 28 | // Compare redirect URL if present (optimization: check this before comparing all headers) 29 | const url1 = response1.headers.get("Location"); 30 | const url2 = response2.headers.get("Location"); 31 | if ((url1 !== null || url2 !== null) && url1 !== url2) return false; 32 | 33 | // Optimize header comparison: convert to arrays once and compare counts first 34 | const headers1Entries = Array.from(response1.headers.entries()); 35 | const headers2Entries = Array.from(response2.headers.entries()); 36 | 37 | // Quick check: if header count differs, they're not equal 38 | if (headers1Entries.length !== headers2Entries.length) return false; 39 | 40 | // Create a Map from headers1 for faster lookups 41 | const headers1Map = new Map(headers1Entries); 42 | 43 | // Check if all headers in headers2 match headers1 44 | for (const [key, value] of headers2Entries) { 45 | if (headers1Map.get(key) !== value) { 46 | return false; 47 | } 48 | } 49 | 50 | return true; 51 | } 52 | -------------------------------------------------------------------------------- /packages/nemo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 10 | "module": "ESNext", 11 | "moduleDetection": "force", 12 | "moduleResolution": "bundler", 13 | "noUncheckedIndexedAccess": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "ESNext" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/nemo/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import type { Options } from "tsup"; 3 | import { defineConfig } from "tsup"; 4 | 5 | // eslint-disable-next-line import/no-default-export -- tsup config 6 | export default defineConfig((options: Options) => ({ 7 | entry: ["src/**/*.ts"], 8 | format: ["esm"], 9 | clean: true, 10 | splitting: false, 11 | dts: true, 12 | minify: true, 13 | external: ["next"], 14 | target: "node20", 15 | tsconfig: path.resolve(__dirname, "./tsconfig.json"), 16 | ...options, 17 | })); 18 | -------------------------------------------------------------------------------- /packages/nemo/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "build": { 6 | "outputs": ["dist/**"] 7 | }, 8 | "dev": { 9 | "cache": false, 10 | "outputs": ["dist/**"], 11 | "persistent": true 12 | }, 13 | "test": { 14 | "outputs": ["coverage/**"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=z4nr34l_nemo 2 | sonar.organization=z4nr34l 3 | sonar.project.monorepo.enabled=true 4 | sonar.sources=./packages/nemo/src 5 | sonar.inclusions=**/*.ts 6 | sonar.tests=./packages/nemo/__tests__ 7 | sonar.test.inclusions=**/*.test.ts 8 | sonar.typescript.lcov.reportPaths=./packages/nemo/coverage/lcov.info 9 | sonar.javascript.lcov.reportPaths=./packages/nemo/coverage/lcov.info 10 | sonar.exclusions=examples/**,apps/docs/**,**/*.d.ts,**/*.spec.ts,**/*.test.ts 11 | sonar.coverage.exclusions=examples/**,apps/docs/**,**/*.d.ts,**/*.spec.ts,**/*.test.ts 12 | sonar.test.exclusions=examples/**,apps/docs/**,**/*.d.ts 13 | sonar.cpd.exclusions=examples/**,apps/docs/** -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "dev": { 5 | "cache": false, 6 | "persistent": true 7 | }, 8 | "examples": { 9 | "cache": false, 10 | "persistent": true 11 | }, 12 | "test": {} 13 | }, 14 | "ui": "tui" 15 | } --------------------------------------------------------------------------------