├── .npmrc ├── src ├── utils │ ├── dynamic-import.ts │ ├── errors.ts │ ├── search-params.ts │ └── async-storages.ts ├── index.ts ├── action.test.ts ├── pathname.ts ├── context.tsx └── action.ts ├── .prettierignore ├── tsup.config.ts ├── .github ├── renovate.json └── workflows │ ├── ci.yml │ └── release.yml ├── tsconfig.json ├── vitest.config.ts ├── eslint.config.mjs ├── LICENSE ├── .gitignore ├── package.json ├── README.md └── pnpm-lock.yaml /.npmrc: -------------------------------------------------------------------------------- 1 | hoist=true 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /src/utils/dynamic-import.ts: -------------------------------------------------------------------------------- 1 | export function safeImport(path: string): T | undefined { 2 | try { 3 | // eslint-disable-next-line @typescript-eslint/no-require-imports 4 | return require(path); 5 | } catch (_) { 6 | return; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | dist 5 | dev 6 | tests/output 7 | /.svelte-kit 8 | /package 9 | .env 10 | .env.* 11 | !.env.example 12 | 13 | # Ignore files for PNPM, NPM and YARN 14 | pnpm-lock.yaml 15 | /.changeset 16 | package-lock.json 17 | yarn.lock 18 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig((options) => ({ 4 | clean: !options.watch, 5 | dts: true, 6 | splitting: true, 7 | entry: ['src/*.ts', 'src/*.tsx', '!src/*.test.ts', '!src/*.test.tsx'], 8 | format: ['cjs', 'esm'], 9 | target: 'esnext', 10 | outDir: 'dist', 11 | })); 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { ReadonlyURLSearchParams } from '@/utils/search-params'; 2 | 3 | // ----------------------- 4 | 5 | export { ActionError } from '@/utils/errors'; 6 | export type { ActionErrorPlain } from '@/utils/errors'; 7 | 8 | // ----------------------- 9 | 10 | export type { SafeReturn } from 'p-safe'; 11 | 12 | // ----------------------- 13 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "packageRules": [ 5 | { 6 | "groupName": "All Dependencies", 7 | "groupSlug": "minor-patch", 8 | "matchPackagePatterns": ["*"], 9 | "matchUpdateTypes": ["minor", "patch"] 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@shahrad/tsconfig", 4 | "compilerOptions": { 5 | "moduleResolution": "node", 6 | "module": "esnext", 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"], 10 | "@/tests/*": ["./tests/*"] 11 | }, 12 | "outDir": "dist" 13 | }, 14 | "include": ["**/*.ts"], 15 | "exclude": ["node_modules", "dist", "dev"] 16 | } 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | environment: 'node', 7 | testTimeout: 60_000, 8 | hookTimeout: 120_000, 9 | globals: true, 10 | exclude: ['**/node_modules/**', '**/dist/**'], 11 | }, 12 | resolve: { 13 | alias: { 14 | '@/tests': path.resolve(__dirname, './tests'), 15 | '@': path.resolve(__dirname, './src'), 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export class ActionError extends Error { 2 | override name = 'ActionError'; 3 | constructor( 4 | public code: string, 5 | message: string 6 | ) { 7 | super(message); 8 | Error.captureStackTrace(this, ActionError); 9 | } 10 | 11 | toPlain(): ActionErrorPlain { 12 | return { 13 | code: this.code, 14 | message: this.message, 15 | }; 16 | } 17 | } 18 | 19 | export interface ActionErrorPlain { 20 | code: string; 21 | message: string; 22 | } 23 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@shahrad/eslint-config'; 2 | import globals from 'globals'; 3 | 4 | export default defineConfig( 5 | { 6 | ignores: ['**/dist', '**/dev'], 7 | }, 8 | { 9 | files: ['**/*.test.ts', '**/*.spec.ts'], 10 | rules: { 11 | '@typescript-eslint/no-unused-expressions': 'off', 12 | }, 13 | }, 14 | { 15 | languageOptions: { 16 | ecmaVersion: 'latest', 17 | sourceType: 'module', 18 | globals: { 19 | ...globals.node, 20 | ...globals.browser, 21 | }, 22 | }, 23 | rules: { 24 | 'no-console': 'error', 25 | }, 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Shahrad Elahi 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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | format: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: pnpm/action-setup@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 22 17 | cache: 'pnpm' 18 | 19 | - run: pnpm install --frozen-lockfile 20 | - run: pnpm format:check 21 | 22 | lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: pnpm/action-setup@v4 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version: 22 30 | cache: 'pnpm' 31 | 32 | - run: pnpm install --frozen-lockfile 33 | - run: pnpm lint 34 | 35 | test: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: pnpm/action-setup@v4 40 | - uses: actions/setup-node@v4 41 | with: 42 | node-version: 22 43 | cache: 'pnpm' 44 | 45 | - run: pnpm install --frozen-lockfile 46 | - run: pnpm test 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | create-github-release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | if: startsWith(github.ref, 'refs/tags/v') 14 | steps: 15 | - name: Calculate release name 16 | run: | 17 | GITHUB_REF=${{ github.ref }} 18 | RELEASE_NAME=${GITHUB_REF#"refs/tags/"} 19 | echo "RELEASE_NAME=${RELEASE_NAME}" >> $GITHUB_ENV 20 | 21 | - name: Publish release 22 | uses: actions/create-release@v1 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | tag_name: ${{ github.ref }} 27 | release_name: ${{ env.RELEASE_NAME }} 28 | draft: false 29 | prerelease: false 30 | 31 | publish-npm-release: 32 | runs-on: ubuntu-latest 33 | permissions: 34 | contents: read 35 | id-token: write 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: pnpm/action-setup@v4 39 | - uses: actions/setup-node@v4 40 | with: 41 | node-version: 22 42 | cache: 'pnpm' 43 | registry-url: 'https://registry.npmjs.org' 44 | 45 | - run: pnpm install --frozen-lockfile 46 | - run: npm publish --provenance --access public 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /src/utils/search-params.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ 4 | export class ReadonlyURLSearchParamsError extends Error { 5 | constructor() { 6 | super('Method unavailable on `ReadonlyURLSearchParams`.'); 7 | } 8 | } 9 | 10 | export class ReadonlyURLSearchParams extends URLSearchParams { 11 | /** 12 | * Creates a ReadonlyURLSearchParams instance from a full pathname. 13 | * 14 | * @param pathname The full pathname 15 | * @returns 16 | */ 17 | public static from(pathname: URL | string): ReadonlyURLSearchParams { 18 | if (pathname instanceof URL) { 19 | return new ReadonlyURLSearchParams(pathname.searchParams); 20 | } 21 | 22 | const url = new URL(pathname, 'http://localhost'); 23 | if (url.searchParams) { 24 | return new ReadonlyURLSearchParams(url.searchParams); 25 | } 26 | 27 | return new ReadonlyURLSearchParams(); 28 | } 29 | 30 | /** @deprecated Method unavailable on `ReadonlyURLSearchParams`. */ 31 | override append() { 32 | throw new ReadonlyURLSearchParamsError(); 33 | } 34 | 35 | /** @deprecated Method unavailable on `ReadonlyURLSearchParams`. */ 36 | override delete() { 37 | throw new ReadonlyURLSearchParamsError(); 38 | } 39 | 40 | /** @deprecated Method unavailable on `ReadonlyURLSearchParams`. */ 41 | override set() { 42 | throw new ReadonlyURLSearchParamsError(); 43 | } 44 | 45 | /** @deprecated Method unavailable on `ReadonlyURLSearchParams`. */ 46 | override sort() { 47 | throw new ReadonlyURLSearchParamsError(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/action.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { createAction } from '@/action'; 4 | 5 | describe('Action', () => { 6 | describe('Errors', () => { 7 | it('should throw on error', async () => { 8 | const action = createAction((name: string) => { 9 | if (name === 'foo') { 10 | throw new Error('foo'); 11 | } 12 | return name; 13 | }); 14 | try { 15 | expect(await action('bar')).property('data', 'bar'); 16 | await action('foo'); 17 | expect(true).to.be.false; 18 | } catch (e) { 19 | expect(e).to.be.instanceOf(Error); 20 | expect(e).have.property('name', 'Error'); 21 | expect(e).have.property('message', 'foo'); 22 | } 23 | }); 24 | 25 | it('should use reject and resolve', async () => { 26 | const action = createAction(function (name: string) { 27 | if (name === 'foo') { 28 | this.reject({ code: 'foo', message: 'foo' }); 29 | } 30 | if (name === 'poo') { 31 | throw new Error('NOt tHe PoO!'); 32 | } 33 | this.resolve(name); 34 | }); 35 | try { 36 | expect(await action('bar')).property('data', 'bar'); 37 | expect(await action('foo')) 38 | .property('error') 39 | .deep.eq({ code: 'foo', message: 'foo' }); 40 | await action('poo'); 41 | expect(true).to.be.false; 42 | } catch (e) { 43 | expect(e).to.be.instanceOf(Error); 44 | expect(e).have.property('name', 'Error'); 45 | expect(e).have.property('message', 'NOt tHe PoO!'); 46 | } 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/utils/async-storages.ts: -------------------------------------------------------------------------------- 1 | import type { AsyncLocalStorage } from 'node:async_hooks'; 2 | 3 | import { safeImport } from '@/utils/dynamic-import'; 4 | 5 | type WUAS = 6 | | { 7 | getExpectedRequestStore: any; 8 | } 9 | | { 10 | workUnitAsyncStorage: AsyncLocalStorage; 11 | }; 12 | 13 | async function getRequestStore(): Promise { 14 | try { 15 | return process.env['TURBOPACK'] 16 | ? await import('next/dist/server/app-render/work-unit-async-storage.external') 17 | : safeImport('next/dist/server/app-render/work-unit-async-storage.external'); 18 | } catch { 19 | return Promise.resolve(undefined); 20 | } 21 | } 22 | 23 | export async function getExpectedRequestStore(callingExpression: string) { 24 | const workUnitStoreModule = await getRequestStore(); 25 | if (workUnitStoreModule) { 26 | if ('getExpectedRequestStore' in workUnitStoreModule) { 27 | return workUnitStoreModule.getExpectedRequestStore(); 28 | } 29 | return workUnitStoreModule.workUnitAsyncStorage.getStore(); 30 | } 31 | 32 | const requestStoreModule = safeImport<{ 33 | requestAsyncStorage: AsyncLocalStorage; 34 | }>('next/dist/client/components/request-async-storage.external'); 35 | if (requestStoreModule) { 36 | const store = requestStoreModule.requestAsyncStorage.getStore(); 37 | if (store) return store; 38 | throw new Error( 39 | `\`${callingExpression}\` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context` 40 | ); 41 | } 42 | 43 | throw new Error( 44 | `Invariant: \`${callingExpression}\` expects to have requestAsyncStorage, none available.` 45 | ); 46 | } 47 | 48 | export function getStaticGenerationStore(callingExpression: string) { 49 | const staticGenerationStoreModule = safeImport<{ 50 | staticGenerationAsyncStorage: any; 51 | }>('next/dist/client/components/static-generation-async-storage.external'); 52 | if (staticGenerationStoreModule) { 53 | const staticGenerationStore = 54 | (fetch as any).__nextGetStaticStore?.() ?? 55 | staticGenerationStoreModule.staticGenerationAsyncStorage; 56 | 57 | const store = staticGenerationStore.getStore(); 58 | if (!store) { 59 | throw new Error( 60 | `Invariant: \`${callingExpression}\` expects to have staticGenerationAsyncStorage, none available.` 61 | ); 62 | } 63 | 64 | return store; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | dev 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-extra", 3 | "version": "0.7.0", 4 | "description": "next-extra offers additional methods not included in the standard Next package, such as searchParams and pathname.", 5 | "keywords": [ 6 | "next", 7 | "nextjs", 8 | "react", 9 | "next-extra" 10 | ], 11 | "homepage": "https://github.com/shahradelahi/next-extra", 12 | "repository": "github:shahradelahi/next-extra", 13 | "license": "MIT", 14 | "author": "Shahrad Elahi (https://github.com/shahradelahi)", 15 | "type": "module", 16 | "exports": { 17 | ".": { 18 | "require": "./dist/index.cjs", 19 | "import": "./dist/index.js" 20 | }, 21 | "./action": { 22 | "require": "./dist/action.cjs", 23 | "import": "./dist/action.js" 24 | }, 25 | "./context": { 26 | "require": "./dist/context.cjs", 27 | "import": "./dist/context.js" 28 | }, 29 | "./pathname": { 30 | "require": "./dist/pathname.cjs", 31 | "import": "./dist/pathname.js" 32 | } 33 | }, 34 | "main": "dist/index.js", 35 | "types": "./dist", 36 | "typesVersions": { 37 | "*": { 38 | "action": [ 39 | "dist/action.d.cts", 40 | "dist/action.d.ts" 41 | ], 42 | "context": [ 43 | "dist/context.d.cts", 44 | "dist/context.d.ts" 45 | ], 46 | "pathname": [ 47 | "dist/pathname.d.cts", 48 | "dist/pathname.d.ts" 49 | ] 50 | } 51 | }, 52 | "files": [ 53 | "/dist/**/*", 54 | "!**/*.test.*" 55 | ], 56 | "scripts": { 57 | "build": "tsup", 58 | "clean": "git clean -dfx \"**/node_modules/**\" dist .tsbuildinfo", 59 | "dev": "tsup --watch", 60 | "format": "prettier --write .", 61 | "format:check": "prettier --check .", 62 | "lint": "pnpm typecheck && eslint .", 63 | "lint:fix": "eslint --fix .", 64 | "prepublishOnly": "pnpm test && pnpm lint && pnpm format:check && pnpm build", 65 | "test": "vitest --run", 66 | "typecheck": "tsc --noEmit" 67 | }, 68 | "prettier": "@shahrad/prettier-config", 69 | "dependencies": { 70 | "@edge-runtime/cookies": "^6.0.0", 71 | "@se-oss/deep-merge": "^1.0.0", 72 | "@se-oss/ip-address": "^1.1.0", 73 | "p-safe": "^1.0.0" 74 | }, 75 | "devDependencies": { 76 | "@shahrad/eslint-config": "^1.0.1", 77 | "@shahrad/prettier-config": "^1.2.2", 78 | "@shahrad/tsconfig": "^1.2.0", 79 | "@types/node": "^25.0.2", 80 | "@types/react": "^19.2.2", 81 | "eslint": "^9.39.2", 82 | "globals": "^16.5.0", 83 | "next": "^16.0.10", 84 | "prettier": "^3.7.4", 85 | "react": "^19.2.0", 86 | "tsup": "^8.5.1", 87 | "tsx": "^4.21.0", 88 | "typescript": "^5.9.3", 89 | "vitest": "^4.0.16" 90 | }, 91 | "peerDependencies": { 92 | "next": "14 || 15 || 16", 93 | "react": "18 || 19" 94 | }, 95 | "packageManager": "pnpm@10.26.0+sha512.3b3f6c725ebe712506c0ab1ad4133cf86b1f4b687effce62a9b38b4d72e3954242e643190fc51fa1642949c735f403debd44f5cb0edd657abe63a8b6a7e1e402" 96 | } 97 | -------------------------------------------------------------------------------- /src/pathname.ts: -------------------------------------------------------------------------------- 1 | import { headers } from 'next/headers.js'; 2 | 3 | import { getExpectedRequestStore, getStaticGenerationStore } from '@/utils/async-storages'; 4 | import { ReadonlyURLSearchParams } from '@/utils/search-params'; 5 | 6 | // -- Internal ------------------------ 7 | 8 | async function getRequestOrigin() { 9 | const requestHeaders = await headers(); 10 | const protocol = requestHeaders.get('x-forwarded-proto') || 'http'; 11 | const host = 12 | requestHeaders.get('x-forwarded-host') || requestHeaders.get('host') || 'localhost:3000'; 13 | return `${protocol}://${host}`; 14 | } 15 | 16 | async function getRequestURL(callingExpression: string): Promise { 17 | const origin = await getRequestOrigin(); 18 | 19 | const requestStore = await getExpectedRequestStore(callingExpression); 20 | if (requestStore && 'url' in requestStore && !!requestStore.url) { 21 | return new URL(`${requestStore.url.pathname}${requestStore.url.search}`, origin); 22 | } 23 | 24 | const staticStore = getStaticGenerationStore(callingExpression); 25 | if (staticStore && 'urlPathname' in staticStore && !!staticStore.urlPathname) { 26 | return new URL(staticStore.urlPathname, origin); 27 | } 28 | 29 | // We should never get here. 30 | throw new Error( 31 | `\`${callingExpression}\` could not access the request URL. Probably you should report this as a bug. GitHub: https://github.com/shahradelahi/next-extra/issues` 32 | ); 33 | } 34 | 35 | // -- Exported ------------------------ 36 | 37 | /** @deprecated This method will be removed in the next major release. */ 38 | export async function invokeUrl(): Promise { 39 | return getRequestURL('invokeUrl'); 40 | } 41 | 42 | /** 43 | * A [Server Component](https://nextjs.org/docs/app/building-your-application/rendering/server-components) method 44 | * that lets you read the current URL's pathname. 45 | * 46 | * @example 47 | * ```ts 48 | * import { pathname } from 'next-extra/pathname' 49 | * 50 | * export default async function Layout({ children }: Readonly<{ children: React.ReactNode }>) { 51 | * const route = await pathname() // returns "/dashboard" on /dashboard?foo=bar 52 | * // ... 53 | * } 54 | * ``` 55 | */ 56 | export async function pathname(): Promise { 57 | const expression = 'pathname'; 58 | const url = await getRequestURL(expression); 59 | 60 | return url.pathname; 61 | } 62 | 63 | /** 64 | * A [Server Component](https://nextjs.org/docs/app/building-your-application/rendering/server-components) method 65 | * that lets you *read* the current URL's search parameters. 66 | * 67 | * Learn more about [`URLSearchParams` on MDN](https://developer.mozilla.org/docs/Web/API/URLSearchParams) 68 | * 69 | * @example 70 | * ```ts 71 | * import { searchParams } from 'next-extra/pathname' 72 | * 73 | * export default async function Layout({ children }: Readonly<{ children: React.ReactNode }>) { 74 | * const params = await searchParams() 75 | * params.get('foo') // returns 'bar' when ?foo=bar 76 | * // ... 77 | * } 78 | * ``` 79 | */ 80 | export async function searchParams(): Promise { 81 | const expression = 'searchParams'; 82 | const url = await getRequestURL(expression); 83 | 84 | return new ReadonlyURLSearchParams(url.searchParams); 85 | } 86 | -------------------------------------------------------------------------------- /src/context.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { deepMerge } from '@se-oss/deep-merge'; 4 | import React, { useContext, useMemo } from 'react'; 5 | 6 | export interface PageContextProps { 7 | /** Defaults to `merge`. */ 8 | strategy?: PageContextStrategy; 9 | data: T; 10 | children?: React.ReactNode; 11 | } 12 | 13 | export type Context = { [key: string]: any }; 14 | 15 | export type PageContextStrategy = 'deepmerge' | 'merge'; 16 | 17 | declare global { 18 | interface Window { 19 | __next_c?: [PageContextStrategy, { data: any }][]; 20 | } 21 | } 22 | 23 | const PageContext = React.createContext(null); 24 | if (process.env['NODE_ENV'] !== 'production') { 25 | PageContext.displayName = 'PageContext'; 26 | } 27 | 28 | /** 29 | * A component that provides context data to its children. 30 | * 31 | * @param props - The properties for the PageContext component. 32 | * @returns A JSX element that provides the context to its children. 33 | * 34 | * @exmaple 35 | * ```typescript jsx 36 | * import { PageContext } from 'next-extra/context'; 37 | * 38 | * export default async function Layout({ children }: { children: React.ReactNode }) { 39 | * // ... 40 | * return {children}; 41 | * } 42 | * ``` 43 | */ 44 | export function PageContextProvider(props: PageContextProps) { 45 | const { data, strategy, children } = props; 46 | const serializedData = JSON.stringify([strategy ?? 'merge', { data }]); 47 | return ( 48 | 49 |