├── .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 |

54 |
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 |
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 |
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