├── .editorconfig ├── .eslintrc.json ├── .github ├── funding.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── ServerContextContext.mjs ├── ServerContextContext.test.mjs ├── changelog.md ├── cjsDefaultImport.mjs ├── cjsDefaultImport.test.mjs ├── jsconfig.json ├── license.md ├── package.json ├── readme.md ├── test ├── ReactHookTest.mjs ├── assertBundleSize.mjs ├── execFilePromise.mjs ├── fixtures │ ├── withServerContext-app-getInitialProps-absent │ │ ├── next.config.mjs │ │ └── pages │ │ │ ├── _app.mjs │ │ │ └── index.mjs │ ├── withServerContext-app-getInitialProps-present │ │ ├── next.config.mjs │ │ └── pages │ │ │ ├── _app.mjs │ │ │ └── index.mjs │ ├── withServerContext-page-getInitialProps-absent │ │ ├── next.config.mjs │ │ └── pages │ │ │ └── index.mjs │ └── withServerContext-page-getInitialProps-present │ │ ├── next.config.mjs │ │ └── pages │ │ └── index.mjs ├── listen.mjs └── startNext.mjs ├── useServerContext.mjs ├── useServerContext.test.mjs ├── withServerContext.mjs └── withServerContext.test.mjs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | max_line_length = 80 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:optimal-modules/recommended", 5 | "plugin:react-hooks/recommended" 6 | ], 7 | "env": { 8 | "es2022": true, 9 | "node": true, 10 | "browser": true 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": "latest" 14 | }, 15 | "plugins": ["simple-import-sort"], 16 | "rules": { 17 | "simple-import-sort/imports": "error", 18 | "simple-import-sort/exports": "error" 19 | }, 20 | "overrides": [ 21 | { 22 | "files": ["*.mjs"], 23 | "parserOptions": { 24 | "sourceType": "module" 25 | }, 26 | "globals": { 27 | "__dirname": "off", 28 | "__filename": "off", 29 | "exports": "off", 30 | "module": "off", 31 | "require": "off" 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: jaydenseric 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, workflow_dispatch] 3 | jobs: 4 | test: 5 | name: Test with Node.js v${{ matrix.node }} and ${{ matrix.os }} 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest] 10 | node: ["18", "20", "21"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup Node.js v${{ matrix.node }} 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ matrix.node }} 17 | - name: npm install and test 18 | run: npm install-test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .next 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | .next 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "never" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.disableAutomaticTypeAcquisition": true, 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "typescript.tsdk": "node_modules/typescript/lib" 5 | } 6 | -------------------------------------------------------------------------------- /ServerContextContext.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from "react"; 4 | 5 | /** 6 | * [Node.js](https://nodejs.org) HTTP server context. 7 | * @typedef {object} ServerContext 8 | * @prop {import("node:http").IncomingMessage} request 9 | * [Node.js HTTP server request instance](https://nodejs.org/api/http.html#http_class_http_incomingmessage). 10 | * @prop {import("node:http").ServerResponse} response 11 | * [Node.js HTTP server response instance](https://nodejs.org/api/http.html#http_class_http_serverresponse). 12 | */ 13 | 14 | /** 15 | * [React context](https://reactjs.org/docs/context.html) for a 16 | * {@linkcode ServerContext} object. Chances are you won’t have to interact with 17 | * this directly. 18 | */ 19 | const ServerContextContext = React.createContext( 20 | /** @type {ServerContext | undefined} */ (undefined), 21 | ); 22 | 23 | if (typeof process === "object" && process.env.NODE_ENV !== "production") 24 | /** 25 | * The display name. 26 | * @see [React display name conventions](https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging). 27 | */ 28 | ServerContextContext.displayName = "ServerContextContext"; 29 | 30 | export default ServerContextContext; 31 | -------------------------------------------------------------------------------- /ServerContextContext.test.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { strictEqual } from "node:assert"; 4 | import { describe, it } from "node:test"; 5 | 6 | import React from "react"; 7 | import ReactTestRenderer from "react-test-renderer"; 8 | 9 | import ServerContextContext from "./ServerContextContext.mjs"; 10 | import assertBundleSize from "./test/assertBundleSize.mjs"; 11 | 12 | describe("Function `ServerContextContext`.", { concurrency: true }, () => { 13 | it("Bundle size.", async () => { 14 | await assertBundleSize( 15 | new URL("./ServerContextContext.mjs", import.meta.url), 16 | 100, 17 | ); 18 | }); 19 | 20 | it("Used as a React context.", () => { 21 | let contextValue; 22 | 23 | function TestComponent() { 24 | contextValue = React.useContext(ServerContextContext); 25 | return null; 26 | } 27 | 28 | const value = 29 | /** @type {import("./ServerContextContext.mjs").ServerContext} */ ({}); 30 | 31 | ReactTestRenderer.act(() => { 32 | ReactTestRenderer.create( 33 | React.createElement( 34 | ServerContextContext.Provider, 35 | { value }, 36 | React.createElement(TestComponent), 37 | ), 38 | ); 39 | }); 40 | 41 | strictEqual(contextValue, value); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # next-server-context changelog 2 | 3 | ## 6.0.0 4 | 5 | ### Major 6 | 7 | - Updated Node.js support to `^18.17.0 || >=20.4.0`. 8 | - Updated dev dependencies, some of which require newer Node.js versions than previously supported. 9 | - Use the Node.js test runner API and remove the dev dependency [`test-director`](https://npm.im/test-director). 10 | - Refactored tests to use the standard `fetch` API available in modern Node.js and removed the dev dependency [`node-fetch`](https://npm.im/node-fetch). 11 | 12 | ### Patch 13 | 14 | - Updated the [`next`](https://npm.im/next) peer dependency to `12 - 14`. 15 | - Updated the `package.json` field `repository` to conform to new npm requirements. 16 | - Integrated the ESLint plugin [`eslint-plugin-optimal-modules`](https://npm.im/eslint-plugin-optimal-modules). 17 | - Updated GitHub Actions CI config: 18 | - The workflow still triggers on push, but no longer on pull request. 19 | - The workflow can now be manually triggered. 20 | - Run tests with Node.js v18, v20, v21. 21 | - Updated `actions/checkout` to v4. 22 | - Updated `actions/setup-node` to v4. 23 | - Improved the types for test fixture Next.js config. 24 | - Improved the test helper function `startNext`. 25 | - Added tests for the internal function `cjsDefaultImport`. 26 | - Updated link URLs in the readme. 27 | 28 | ## 5.0.2 29 | 30 | ### Patch 31 | 32 | - Updated the [`next`](https://npm.im/next) peer dependency to `12 - 13`. 33 | - Updated dev dependencies. 34 | - Use a new internal helper function `cjsDefaultImport` to normalize the default import value from the CJS module `next/app.js` that has a `default` property, preserving the type for the various ways TypeScript may be configured. 35 | - Simplified the test helper function `startNext`. 36 | 37 | ## 5.0.1 38 | 39 | ### Patch 40 | 41 | - Updated dev dependencies. 42 | - Use the `node:` URL scheme for Node.js builtin module imports in tests. 43 | - Replaced the test helper function `fsPathRemove` with the function `rm` from the Node.js builtin module `node:fs/promises`. 44 | - Revamped the readme: 45 | - Removed the badges. 46 | - Added information about TypeScript config and [optimal JavaScript module design](https://jaydenseric.com/blog/optimal-javascript-module-design). 47 | 48 | ## 5.0.0 49 | 50 | ### Major 51 | 52 | - Updated Node.js support to `^14.17.0 || ^16.0.0 || >= 18.0.0`. 53 | - Implemented TypeScript types via JSDoc comments, fixing [#1](https://github.com/jaydenseric/next-server-context/issues/1). 54 | 55 | ### Minor 56 | 57 | - Updated the [`react`](https://npm.im/react) peer dependency to `16.14 - 18`. 58 | 59 | ### Patch 60 | 61 | - Updated dev dependencies. 62 | - Simplified dev dependencies and config for ESLint. 63 | - Removed the [`jsdoc-md`](https://npm.im/jsdoc-md) dev dependency and the package `docs-update` and `docs-check` scripts, replacing the readme “API” section with a manually written “Exports” section. 64 | - Check TypeScript types via a new package `types` script. 65 | - Removed the [`@testing-library/react-hooks`](https://npm.im/@testing-library/react-hooks) dev dependency and rewrote React hook tests using [`react-test-renderer`](https://npm.im/react-test-renderer) and a custom React component `ReactHookTest`. 66 | - Disabled Next.js build ESLint checks for test fixture Next.js projects. 67 | - Upper-cased the first letter of the display name for the React component that the function `withServerContext` returns. 68 | - Updated GitHub Actions CI config: 69 | - Run tests with Node.js v14, v16, v18. 70 | - Updated `actions/checkout` to v3. 71 | - Updated `actions/setup-node` to v3. 72 | - Use the `.mjs` file extension for Next.js pages in test fixtures. 73 | - Removed the now redundant `not IE > 0` from the Browserslist query. 74 | - Added a `license.md` MIT License file. 75 | 76 | ## 4.0.0 77 | 78 | ### Major 79 | 80 | - Updated Node.js support to `^12.22.0 || ^14.17.0 || >= 16.0.0`. 81 | - Updated the [`next`](https://npm.im/next) peer dependency to `^12.0.0`. 82 | - Updated dev dependencies, some of which require newer Node.js versions than previously supported. 83 | - Public modules are now individually listed in the package `files` and `exports` fields. 84 | - Removed `./package` from the package `exports` field; the full `package.json` filename must be used in a `require` path. 85 | - Removed the package main index module; deep imports must be used. 86 | - Shortened public module deep import paths, removing the `/public/`. 87 | - The API is now ESM in `.mjs` files instead of CJS in `.js` files, [accessible via `import` but not `require`](https://nodejs.org/dist/latest/docs/api/esm.html#require). 88 | - Switched back to using `React.createElement` instead of the [the new React JSX runtime](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html). 89 | 90 | ### Patch 91 | 92 | - Also run GitHub Actions CI with Node.js v17. 93 | - Simplified package scripts. 94 | - Reorganized the test file structure. 95 | - Test the bundle sizes for public modules individually. 96 | - Use a new `assertBundleSize` function to assert module bundle size in tests: 97 | - Failure message contains details about the bundle size and how much the limit was exceeded. 98 | - Errors when the surplus is greater than 25% of the limit, suggesting the limit should be reduced. 99 | - Resolves the minified bundle and its gzipped size for debugging in tests. 100 | - Use Node.js `ok` assertions instead of `strictEqual` where possible. 101 | - Updated the ESLint config. 102 | - Removed a redundant prepare step that’s a no-op in current Next.js versions when programmatically starting Next.js in tests. 103 | - Fixed internal JSDoc types. 104 | - Configured Prettier option `singleQuote` to the default, `false`. 105 | - Documentation tweaks. 106 | 107 | ## 3.0.1 108 | 109 | ### Patch 110 | 111 | - Updated the [`next`](https://npm.im/next) peer dependency to `9.0.3 - 11`. 112 | - Updated dev dependencies. 113 | - Added the [`eslint-config-next`](https://npm.im/eslint-config-next) dev dependency for [`next`](https://npm.im/next) v11. 114 | - Renamed imports in the test index module. 115 | - Amended the changelog entries for v2.0.0 and v3.0.0. 116 | 117 | ## 3.0.0 118 | 119 | ### Major 120 | 121 | - Updated Node.js support to `^12.20 || >= 14.13`. 122 | - Updated browser support to the Browserslist query `> 0.5%, not OperaMini all, not IE > 0, not dead`. 123 | - Updated the [`react`](https://npm.im/react) peer dependency to `16.14 - 17`. 124 | - Updated dev dependencies, some of which require newer Node.js versions than were previously supported. 125 | - Replaced the the `package.json` `exports` field public [subpath folder mapping](https://nodejs.org/api/packages.html#packages_subpath_folder_mappings) (deprecated by Node.js) with a [subpath pattern](https://nodejs.org/api/packages.html#packages_subpath_patterns). Deep `require` paths within `next-server-context/public/` must now include the `.js` file extension. 126 | - Use [the new React JSX runtime](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html). 127 | - Removed Babel related dev dependencies, config, and scripts. Published modules now contain more modern ES syntax. 128 | - Published modules now contain JSDoc comments, which might affect TypeScript projects. 129 | - Added a package `test:api` script to test the API, using ESM in `.mjs` files and new dev dependencies: 130 | - [`@testing-library/react-hooks`](https://npm.im/@testing-library/react-hooks) 131 | - [`coverage-node`](https://npm.im/coverage-node) 132 | - [`node-fetch`](https://npm.im/node-fetch) 133 | - [`react-test-renderer`](https://npm.im/react-test-renderer) 134 | - [`test-director`](https://npm.im/test-director) 135 | 136 | ### Patch 137 | 138 | - Updated GitHub Actions CI config: 139 | - Run tests with Node.js v12, v14, v16. 140 | - Updated `actions/checkout` to v2. 141 | - Updated `actions/setup-node` to v2. 142 | - Don’t specify the `CI` environment variable as it’s set by default. 143 | - Simplified JSDoc related package scripts now that [`jsdoc-md`](https://npm.im/jsdoc-md) v10+ automatically generates a Prettier formatted readme. 144 | - Added a package `test:jsdoc` script that checks the readme API docs are up to date with the source JSDoc. 145 | - Test the bundle size manually using [`esbuild`](https://npm.im/esbuild) and [`gzip-size`](https://npm.im/gzip-size), removing [`size-limit`](https://npm.im/size-limit) related dev dependencies, config, and scripts. 146 | - Use the `.js` file extension in internal `require` paths. 147 | - Refactored an arrow function to a regular function within the function `withServerContext`. 148 | - Removed `npm-debug.log` from the `.gitignore` file as npm [v4.2.0](https://github.com/npm/npm/releases/tag/v4.2.0)+ doesn’t create it in the current working directory. 149 | - Improved documentation. 150 | - Updated a URL in the v2.0.0 changelog entry. 151 | - The file `changelog.md` is no longer published. 152 | 153 | ## 2.0.0 154 | 155 | ### Major 156 | 157 | - Updated the package `engines.node` field to `^10.17.0 || ^12.0.0 || >= 13.7.0`. 158 | - Updated dev dependencies, some of which require newer Node.js versions than previously supported. 159 | - Added a [package `exports` field](https://nodejs.org/api/packages.html#packages_exports) with [conditional exports](https://nodejs.org/api/packages.html#packages_conditional_exports) to support native ESM in Node.js and keep internal code private, whilst avoiding [the dual package hazard](https://nodejs.org/api/packages.html#packages_dual_package_hazard). Published files have been reorganized, so previously undocumented deep imports will need to be rewritten according to the newly documented paths. 160 | - Removed the package `module` field. 161 | 162 | ### Patch 163 | 164 | - Updated the [`next`](https://npm.im/next) peer dependency to `9.0.3 - 10`. 165 | - Updated the [`react`](https://npm.im/react) peer dependency to `16.8 - 17`. 166 | - Updated dependencies. 167 | - Removed the redundant [`object-assign`](https://npm.im/object-assign) dependency and related [`size-limit`](https://npm.im/size-limit) config. 168 | - Updated the package `keywords` field. 169 | - Ensure GitHub Actions run on pull request. 170 | - Also run GitHub Actions with Node.js v14 and v15, and not v13. 171 | - Simplified the GitHub Actions CI config with the [`npm install-test`](https://docs.npmjs.com/cli/v7/commands/npm-install-test) command. 172 | - Updated EditorConfig. 173 | - Simplified the Babel config and scripts: 174 | - Removed the [`babel-plugin-transform-require-extensions`](https://npm.im/babel-plugin-transform-require-extensions) dev dependency. 175 | - Replaced the `prepare:mjs` and `prepare:js` package scripts with a `prepare:babel` script. 176 | - Use `.json` instead of `.js` config. 177 | - Moved Browserslist config to the package `browserslist` field. 178 | - Ensure `.js` files are parsed as scripts. 179 | - Updated test scripts. 180 | - Configured Prettier option `semi` to the default, `true`. 181 | - Lint fixes. 182 | - Fixed typos in `useServerContext` examples. 183 | - Documented all the ways to `import` and `require` the public API. 184 | - Misc. readme improvements. 185 | - Allow `displayName` for React components to be eliminated in production builds. 186 | 187 | ## 1.0.0 188 | 189 | Initial release. 190 | -------------------------------------------------------------------------------- /cjsDefaultImport.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Normalize a CJS module default import value that may have a `default` 5 | * property, preserving the type for the various ways TypeScript may be 6 | * configured. 7 | * @template T Imported CJS module type. 8 | * @param {T} value Imported CJS module. 9 | * @returns {T extends { default: any } ? T["default"] : T} Normalized default 10 | * import value. 11 | */ 12 | export default function cjsDefaultImport(value) { 13 | return typeof value === "object" && value && "default" in value 14 | ? /** @type {{ default: any }} */ (value).default 15 | : value; 16 | } 17 | -------------------------------------------------------------------------------- /cjsDefaultImport.test.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { deepStrictEqual, strictEqual } from "node:assert"; 4 | import { describe, it } from "node:test"; 5 | 6 | import cjsDefaultImport from "./cjsDefaultImport.mjs"; 7 | import assertBundleSize from "./test/assertBundleSize.mjs"; 8 | 9 | describe("Function `cjsDefaultImport`.", { concurrency: true }, () => { 10 | it("Bundle size.", async () => { 11 | await assertBundleSize( 12 | new URL("./cjsDefaultImport.mjs", import.meta.url), 13 | 120, 14 | ); 15 | }); 16 | 17 | describe("Argument 1 `value`.", { concurrency: true }, () => { 18 | it("Non object.", () => { 19 | const value = false; 20 | 21 | strictEqual(cjsDefaultImport(value), value); 22 | }); 23 | 24 | describe("Object.", { concurrency: true }, () => { 25 | it("Property `default` absent.", () => { 26 | const value = Object.freeze({ a: 1 }); 27 | 28 | deepStrictEqual(cjsDefaultImport(value), value); 29 | }); 30 | 31 | it("Property `default` present.", () => { 32 | const value = Object.freeze({ 33 | default: Object.freeze({ a: 1 }), 34 | }); 35 | 36 | deepStrictEqual(cjsDefaultImport(value), value.default); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "maxNodeModuleJsDepth": 10, 4 | "module": "nodenext", 5 | "noEmit": true, 6 | "strict": true 7 | }, 8 | "typeAcquisition": { 9 | "enable": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright Jayden Seric 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-server-context", 3 | "version": "6.0.0", 4 | "description": "A Next.js App or page decorator, React context object, and React hook to access Node.js HTTP server context when rendering components.", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Jayden Seric", 8 | "email": "me@jaydenseric.com", 9 | "url": "https://jaydenseric.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jaydenseric/next-server-context.git" 14 | }, 15 | "homepage": "https://github.com/jaydenseric/next-server-context#readme", 16 | "bugs": "https://github.com/jaydenseric/next-server-context/issues", 17 | "funding": "https://github.com/sponsors/jaydenseric", 18 | "keywords": [ 19 | "next", 20 | "server", 21 | "context", 22 | "request", 23 | "response", 24 | "react", 25 | "hooks", 26 | "ssr", 27 | "esm", 28 | "mjs" 29 | ], 30 | "files": [ 31 | "cjsDefaultImport.mjs", 32 | "ServerContextContext.mjs", 33 | "useServerContext.mjs", 34 | "withServerContext.mjs" 35 | ], 36 | "sideEffects": false, 37 | "exports": { 38 | "./package.json": "./package.json", 39 | "./ServerContextContext.mjs": "./ServerContextContext.mjs", 40 | "./useServerContext.mjs": "./useServerContext.mjs", 41 | "./withServerContext.mjs": "./withServerContext.mjs" 42 | }, 43 | "engines": { 44 | "node": "^18.17.0 || >=20.4.0" 45 | }, 46 | "browserslist": "Node 18.17 - 19 and Node < 19, Node >= 20.4, > 0.5%, not OperaMini all, not dead", 47 | "peerDependencies": { 48 | "next": "12 - 14", 49 | "react": "16.14 - 18" 50 | }, 51 | "devDependencies": { 52 | "@types/node": "^20.9.4", 53 | "@types/react": "^18.2.38", 54 | "@types/react-dom": "^18.2.17", 55 | "@types/react-test-renderer": "^18.0.7", 56 | "coverage-node": "^8.0.0", 57 | "esbuild": "^0.19.7", 58 | "eslint": "^8.54.0", 59 | "eslint-plugin-optimal-modules": "^1.0.2", 60 | "eslint-plugin-react-hooks": "^4.6.0", 61 | "eslint-plugin-simple-import-sort": "^10.0.0", 62 | "gzip-size": "^7.0.0", 63 | "next": "^14.0.3", 64 | "prettier": "^3.1.0", 65 | "react": "^18.2.0", 66 | "react-dom": "^18.2.0", 67 | "react-test-renderer": "^18.2.0", 68 | "typescript": "^5.3.2" 69 | }, 70 | "scripts": { 71 | "eslint": "eslint .", 72 | "prettier": "prettier -c .", 73 | "types": "tsc -p jsconfig.json", 74 | "tests": "NEXT_TELEMETRY_DISABLED=1 coverage-node --test-reporter=spec --test *.test.mjs", 75 | "test": "npm run eslint && npm run prettier && npm run types && npm run tests", 76 | "prepublishOnly": "npm test" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # next-server-context 2 | 3 | A [Next.js](https://nextjs.org) [`App` or page decorator](./withServerContext.mjs), [React context object](./ServerContextContext.mjs), and [React hook](./useServerContext.mjs) to access [Node.js](https://nodejs.org) HTTP server context when rendering components. 4 | 5 | Some uses: 6 | 7 | - Setting the HTTP response status code for the server side rendered page according to [GraphQL](https://graphql.org) query results in components. 8 | - Isomorphically accessing cookies within components. 9 | 10 | ## Installation 11 | 12 | To install [`next-server-context`](https://npm.im/next-server-context) with [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), run: 13 | 14 | ```sh 15 | npm install next-server-context 16 | ``` 17 | 18 | Decorate either the entire [Next.js](https://nextjs.org) app or individual pages using the function [`withServerContext`](./withServerContext.mjs) to be able to use the [React](https://reactjs.org) hook [`useServerContext`](./useServerContext.mjs). 19 | 20 | ## Requirements 21 | 22 | Supported runtime environments: 23 | 24 | - [Node.js](https://nodejs.org) versions `^18.17.0 || >=20.4.0`. 25 | - Browsers matching the [Browserslist](https://browsersl.ist) query [`> 0.5%, not OperaMini all, not dead`](https://browsersl.ist/?q=%3E+0.5%25%2C+not+OperaMini+all%2C+not+dead). 26 | 27 | Projects must configure [TypeScript](https://www.typescriptlang.org) to use types from the ECMAScript modules that have a `// @ts-check` comment: 28 | 29 | - [`compilerOptions.allowJs`](https://www.typescriptlang.org/tsconfig#allowJs) should be `true`. 30 | - [`compilerOptions.maxNodeModuleJsDepth`](https://www.typescriptlang.org/tsconfig#maxNodeModuleJsDepth) should be reasonably large, e.g. `10`. 31 | - [`compilerOptions.module`](https://www.typescriptlang.org/tsconfig#module) should be `"node16"` or `"nodenext"`. 32 | 33 | ## Exports 34 | 35 | The [npm](https://npmjs.com) package [`next-server-context`](https://npm.im/next-server-context) features [optimal JavaScript module design](https://jaydenseric.com/blog/optimal-javascript-module-design). It doesn’t have a main index module, so use deep imports from the ECMAScript modules that are exported via the [`package.json`](./package.json) field [`exports`](https://nodejs.org/api/packages.html#exports): 36 | 37 | - [`ServerContextContext.mjs`](./ServerContextContext.mjs) 38 | - [`useServerContext.mjs`](./useServerContext.mjs) 39 | - [`withServerContext.mjs`](./withServerContext.mjs) 40 | -------------------------------------------------------------------------------- /test/ReactHookTest.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * React component for testing a React hook. 5 | * @param {object} props Props. 6 | * @param {() => unknown} props.hook React hook. 7 | * @param {Array} props.results Results of each render; the hook return 8 | * value or error. 9 | */ 10 | export default function ReactHookTest({ hook, results }) { 11 | let result; 12 | 13 | try { 14 | result = hook(); 15 | } catch (error) { 16 | result = error; 17 | } 18 | 19 | results.push(result); 20 | 21 | return null; 22 | } 23 | -------------------------------------------------------------------------------- /test/assertBundleSize.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { fail } from "node:assert"; 4 | import { fileURLToPath } from "node:url"; 5 | 6 | import esbuild from "esbuild"; 7 | import { gzipSize } from "gzip-size"; 8 | 9 | /** 10 | * Asserts the minified and gzipped bundle size of a module. 11 | * @param {URL} moduleUrl Module URL. 12 | * @param {number} limit Minified and gzipped bundle size limit (bytes). 13 | * @returns {Promise<{ bundle: string, gzippedSize: number }>} Resolves the 14 | * minified bundle and its gzipped size (bytes). 15 | */ 16 | export default async function assertBundleSize(moduleUrl, limit) { 17 | if (!(moduleUrl instanceof URL)) 18 | throw new TypeError("Argument 1 `moduleUrl` must be a `URL` instance."); 19 | 20 | if (typeof limit !== "number") 21 | throw new TypeError("Argument 2 `limit` must be a number."); 22 | 23 | const { 24 | outputFiles: [bundle], 25 | } = await esbuild.build({ 26 | entryPoints: [fileURLToPath(moduleUrl)], 27 | external: 28 | // Package peer dependencies. 29 | ["next", "react"], 30 | write: false, 31 | bundle: true, 32 | minify: true, 33 | legalComments: "none", 34 | format: "esm", 35 | }); 36 | 37 | const gzippedSize = await gzipSize(bundle.text); 38 | 39 | if (gzippedSize > limit) 40 | fail( 41 | `${gzippedSize} B minified and gzipped bundle exceeds the ${limit} B limit by ${ 42 | gzippedSize - limit 43 | } B; increase the limit or reduce the bundle size.`, 44 | ); 45 | 46 | const surplus = limit - gzippedSize; 47 | 48 | // Error if the surplus is greater than 25% of the limit. 49 | if (surplus > limit * 0.25) 50 | throw new Error( 51 | `${gzippedSize} B minified and gzipped bundle is under the ${limit} B limit by ${surplus} B; reduce the limit.`, 52 | ); 53 | 54 | // For debugging in tests. 55 | return { bundle: bundle.text, gzippedSize }; 56 | } 57 | -------------------------------------------------------------------------------- /test/execFilePromise.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { execFile } from "node:child_process"; 4 | import { promisify } from "node:util"; 5 | 6 | export default promisify(execFile); 7 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-app-getInitialProps-absent/next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @satisfies {import("next").NextConfig} */ 4 | const nextConfig = { 5 | pageExtensions: ["mjs"], 6 | eslint: { 7 | ignoreDuringBuilds: true, 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-app-getInitialProps-absent/pages/_app.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import NextApp from "next/app.js"; 4 | 5 | import withServerContext from "../../../../withServerContext.mjs"; 6 | 7 | export default withServerContext(NextApp.default); 8 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-app-getInitialProps-absent/pages/index.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from "react"; 4 | 5 | import useServerContext from "../../../../useServerContext.mjs"; 6 | 7 | export default function IndexPage() { 8 | const serverContext = useServerContext(); 9 | 10 | let requestCustomHeader = null; 11 | 12 | if (serverContext) { 13 | requestCustomHeader = serverContext.request.headers["custom-header"]; 14 | serverContext.response.statusCode = 418; 15 | } 16 | 17 | return typeof requestCustomHeader === "string" 18 | ? React.createElement("span", null, requestCustomHeader) 19 | : null; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-app-getInitialProps-present/next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @satisfies {import("next").NextConfig} */ 4 | const nextConfig = { 5 | pageExtensions: ["mjs"], 6 | eslint: { 7 | ignoreDuringBuilds: true, 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-app-getInitialProps-present/pages/_app.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import NextApp from "next/app.js"; 4 | import React from "react"; 5 | 6 | import withServerContext from "../../../../withServerContext.mjs"; 7 | 8 | /** 9 | * @type {import("next").NextComponentType< 10 | * import("next/app.js").AppContext, 11 | * import("next/app.js").AppInitialProps & { appCustomProp: string }, 12 | * import("next/app.js").AppProps & { appCustomProp: string } 13 | * >} 14 | */ 15 | const App = ({ Component, pageProps, appCustomProp }) => { 16 | return React.createElement( 17 | React.Fragment, 18 | null, 19 | appCustomProp, 20 | React.createElement(Component, pageProps), 21 | ); 22 | }; 23 | 24 | App.getInitialProps = async (context) => { 25 | const props = await NextApp.default.getInitialProps(context); 26 | 27 | return { 28 | ...props, 29 | // Add the value together so the test assertion that the HTML contains 30 | // `appCustomProp_value` will only be true if the value rendered correctly. 31 | appCustomProp: "appCustomProp" + "_value", 32 | }; 33 | }; 34 | 35 | export default withServerContext(App); 36 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-app-getInitialProps-present/pages/index.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from "react"; 4 | 5 | import useServerContext from "../../../../useServerContext.mjs"; 6 | 7 | /** @type {import("next").NextPage<{ pageCustomProp: string }>} */ 8 | const IndexPage = ({ pageCustomProp }) => { 9 | const serverContext = useServerContext(); 10 | 11 | let requestCustomHeader = null; 12 | 13 | if (serverContext) { 14 | requestCustomHeader = serverContext.request.headers["custom-header"]; 15 | serverContext.response.statusCode = 418; 16 | } 17 | 18 | return React.createElement( 19 | "span", 20 | null, 21 | `${requestCustomHeader} ${pageCustomProp}`, 22 | ); 23 | }; 24 | 25 | IndexPage.getInitialProps = async () => ({ 26 | // Add the value together so the test assertion that the HTML contains 27 | // `pageCustomProp_value` will only be true if the value rendered correctly. 28 | pageCustomProp: "pageCustomProp" + "_value", 29 | }); 30 | 31 | export default IndexPage; 32 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-page-getInitialProps-absent/next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @satisfies {import("next").NextConfig} */ 4 | const nextConfig = { 5 | pageExtensions: ["mjs"], 6 | eslint: { 7 | ignoreDuringBuilds: true, 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-page-getInitialProps-absent/pages/index.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from "react"; 4 | 5 | import useServerContext from "../../../../useServerContext.mjs"; 6 | import withServerContext from "../../../../withServerContext.mjs"; 7 | 8 | function IndexPage() { 9 | const serverContext = useServerContext(); 10 | 11 | let requestCustomHeader = null; 12 | 13 | if (serverContext) { 14 | requestCustomHeader = serverContext.request.headers["custom-header"]; 15 | serverContext.response.statusCode = 418; 16 | } 17 | 18 | return typeof requestCustomHeader === "string" 19 | ? React.createElement("span", null, requestCustomHeader) 20 | : null; 21 | } 22 | 23 | export default withServerContext(IndexPage); 24 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-page-getInitialProps-present/next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @satisfies {import("next").NextConfig} */ 4 | const nextConfig = { 5 | pageExtensions: ["mjs"], 6 | eslint: { 7 | ignoreDuringBuilds: true, 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /test/fixtures/withServerContext-page-getInitialProps-present/pages/index.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from "react"; 4 | 5 | import useServerContext from "../../../../useServerContext.mjs"; 6 | import withServerContext from "../../../../withServerContext.mjs"; 7 | 8 | /** @type {import("next").NextPage<{ pageCustomProp: string }>} */ 9 | const IndexPage = ({ pageCustomProp }) => { 10 | const serverContext = useServerContext(); 11 | 12 | let requestCustomHeader = null; 13 | 14 | if (serverContext) { 15 | requestCustomHeader = serverContext.request.headers["custom-header"]; 16 | serverContext.response.statusCode = 418; 17 | } 18 | 19 | return React.createElement( 20 | "span", 21 | null, 22 | `${requestCustomHeader} ${pageCustomProp}`, 23 | ); 24 | }; 25 | 26 | IndexPage.getInitialProps = async () => ({ 27 | // Add the value together so the test assertion that the HTML contains 28 | // `pageCustomProp_value` will only be true if the value rendered correctly. 29 | pageCustomProp: "pageCustomProp" + "_value", 30 | }); 31 | 32 | export default withServerContext(IndexPage); 33 | -------------------------------------------------------------------------------- /test/listen.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Starts a Node.js HTTP server. 5 | * @param {import("node:http").Server} server Node.js HTTP server. 6 | * @returns Resolves the port the server is listening on, and a server close 7 | * function. 8 | */ 9 | export default async function listen(server) { 10 | await new Promise((resolve) => { 11 | server.listen(resolve); 12 | }); 13 | 14 | return { 15 | port: /** @type {import("node:net").AddressInfo} */ (server.address()).port, 16 | close: () => server.close(), 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /test/startNext.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { createServer } from "node:http"; 4 | 5 | import next from "next"; 6 | 7 | import cjsDefaultImport from "../cjsDefaultImport.mjs"; 8 | import listen from "./listen.mjs"; 9 | 10 | // Workaround broken Next.js types. 11 | const nextCreateServer = cjsDefaultImport(next); 12 | 13 | /** 14 | * Starts Next.js. 15 | * @param {string} dir Next.js project directory path. 16 | * @returns Resolves the port the server is listening on, and a function to 17 | * close the server. 18 | */ 19 | export default async function startNext(dir) { 20 | const nextServer = nextCreateServer({ dir }); 21 | const nextRequestHandler = nextServer.getRequestHandler(); 22 | 23 | await nextServer.prepare(); 24 | 25 | const server = createServer(nextRequestHandler); 26 | 27 | return await listen(server); 28 | } 29 | -------------------------------------------------------------------------------- /useServerContext.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from "react"; 4 | 5 | import ServerContextContext from "./ServerContextContext.mjs"; 6 | 7 | /** 8 | * React hook to use the {@linkcode ServerContextContext}. 9 | * @example 10 | * A React component that sets the Next.js page’s HTTP status code when server 11 | * side rendered: 12 | * 13 | * ```jsx 14 | * import useServerContext from "next-server-context/useServerContext.mjs"; 15 | * 16 | * export default function ErrorMissing() { 17 | * const serverContext = useServerContext(); 18 | * if (serverContext) serverContext.response.statusCode = 404; 19 | * return ( 20 | *
21 | *

Error 404

22 | *

Something is missing.

23 | *
24 | * ); 25 | * } 26 | * ``` 27 | */ 28 | export default function useServerContext() { 29 | return React.useContext(ServerContextContext); 30 | } 31 | -------------------------------------------------------------------------------- /useServerContext.test.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { strictEqual } from "node:assert"; 4 | import { describe, it } from "node:test"; 5 | 6 | import React from "react"; 7 | import ReactTestRenderer from "react-test-renderer"; 8 | 9 | import ServerContextContext from "./ServerContextContext.mjs"; 10 | import assertBundleSize from "./test/assertBundleSize.mjs"; 11 | import ReactHookTest from "./test/ReactHookTest.mjs"; 12 | import useServerContext from "./useServerContext.mjs"; 13 | 14 | describe("Function `useServerContext`.", { concurrency: true }, () => { 15 | it("Bundle size.", async () => { 16 | await assertBundleSize( 17 | new URL("./useServerContext.mjs", import.meta.url), 18 | 150, 19 | ); 20 | }); 21 | 22 | it("Server context context missing.", () => { 23 | /** @type {Array} */ 24 | const results = []; 25 | 26 | ReactTestRenderer.act(() => { 27 | ReactTestRenderer.create( 28 | React.createElement(ReactHookTest, { hook: useServerContext, results }), 29 | ); 30 | }); 31 | 32 | strictEqual(results.length, 1); 33 | strictEqual(results[0], undefined); 34 | }); 35 | 36 | it("Getting the server context.", () => { 37 | /** @type {Array} */ 38 | const results = []; 39 | 40 | /** @type {import("react-test-renderer").ReactTestRenderer | undefined} */ 41 | let testRenderer; 42 | 43 | const serverContextA = 44 | /** @type {import("./ServerContextContext.mjs").ServerContext} */ ({}); 45 | 46 | ReactTestRenderer.act(() => { 47 | testRenderer = ReactTestRenderer.create( 48 | React.createElement( 49 | ServerContextContext.Provider, 50 | { value: serverContextA }, 51 | React.createElement(ReactHookTest, { 52 | hook: useServerContext, 53 | results, 54 | }), 55 | ), 56 | ); 57 | }); 58 | 59 | strictEqual(results.length, 1); 60 | strictEqual(results[0], serverContextA); 61 | 62 | const tr = 63 | /** @type {import("react-test-renderer").ReactTestRenderer} */ 64 | (testRenderer); 65 | 66 | // The server context shouldn’t change, but it’s good to test the normal 67 | // React context and hooks behavior anyway. 68 | 69 | const serverContextB = 70 | /** @type {import("./ServerContextContext.mjs").ServerContext} */ ({}); 71 | 72 | ReactTestRenderer.act(() => { 73 | tr.update( 74 | React.createElement( 75 | ServerContextContext.Provider, 76 | { value: serverContextB }, 77 | React.createElement(ReactHookTest, { 78 | hook: useServerContext, 79 | results, 80 | }), 81 | ), 82 | ); 83 | }); 84 | 85 | strictEqual(results.length, 2); 86 | strictEqual(results[1], serverContextB); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /withServerContext.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import nextApp from "next/app.js"; 4 | import React from "react"; 5 | 6 | import cjsDefaultImport from "./cjsDefaultImport.mjs"; 7 | import ServerContextContext from "./ServerContextContext.mjs"; 8 | 9 | const NextApp = cjsDefaultImport(nextApp); 10 | 11 | /** 12 | * Decorates a Next.js custom `App` or page React component, to provide 13 | * {@link ServerContextContext server context}. 14 | * @param {import("next").NextComponentType< 15 | * import("next/app.js").AppContext | import("next").NextPageContext, 16 | * any, 17 | * any 18 | * >} Component Next.js custom `App` or page React component. 19 | * @returns Next.js custom `App` or page higher-order React component. 20 | * @example 21 | * A Next.js custom `App` in `pages/_app.js`: 22 | * 23 | * ```jsx 24 | * import withServerContext from "next-server-context/withServerContext.mjs"; 25 | * import App from "next/app"; 26 | * 27 | * export default withServerContext(App); 28 | * ``` 29 | */ 30 | export default function withServerContext(Component) { 31 | /** 32 | * Next.js custom `App` or page higher-order React component. 33 | * @type {import("next").NextComponentType< 34 | * import("next/app.js").AppContext | import("next").NextPageContext, 35 | * { 36 | * serverContext?: import("./ServerContextContext.mjs").ServerContext, 37 | * [key: string]: unknown, 38 | * }, 39 | * { 40 | * serverContext?: import("./ServerContextContext.mjs").ServerContext, 41 | * [key: string]: unknown, 42 | * } 43 | * >} 44 | */ 45 | const WithServerContext = ({ serverContext, ...props }) => { 46 | return React.createElement( 47 | ServerContextContext.Provider, 48 | { value: serverContext }, 49 | React.createElement(Component, props), 50 | ); 51 | }; 52 | 53 | if (typeof process === "object" && process.env.NODE_ENV !== "production") 54 | /** 55 | * The display name. 56 | * @see [React display name conventions](https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging). 57 | */ 58 | WithServerContext.displayName = `WithServerContext(${ 59 | Component.displayName || Component.name || "Component" 60 | })`; 61 | 62 | WithServerContext.getInitialProps = async (context) => { 63 | const isApp = "ctx" in context; 64 | const { req, res } = isApp ? context.ctx : context; 65 | const props = Component.getInitialProps 66 | ? await Component.getInitialProps(context) 67 | : isApp 68 | ? await NextApp.getInitialProps(context) 69 | : {}; 70 | 71 | return req 72 | ? { 73 | ...props, 74 | serverContext: { 75 | request: req, 76 | response: res, 77 | 78 | // This prevents the server attempting to serialize the server 79 | // context object to JSON for hydration on the client. 80 | toJSON: () => undefined, 81 | }, 82 | } 83 | : props; 84 | }; 85 | 86 | return WithServerContext; 87 | } 88 | -------------------------------------------------------------------------------- /withServerContext.test.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { ok, strictEqual } from "node:assert"; 4 | import { rm } from "node:fs/promises"; 5 | import { describe, it } from "node:test"; 6 | import { fileURLToPath } from "node:url"; 7 | 8 | import assertBundleSize from "./test/assertBundleSize.mjs"; 9 | import execFilePromise from "./test/execFilePromise.mjs"; 10 | import startNext from "./test/startNext.mjs"; 11 | 12 | describe("Function `withServerContext`.", { concurrency: true }, () => { 13 | it("Bundle size.", async () => { 14 | await assertBundleSize( 15 | new URL("./withServerContext.mjs", import.meta.url), 16 | 350, 17 | ); 18 | }); 19 | 20 | describe("Decorating the app.", { concurrency: true }, () => { 21 | it("`getInitialProps` absent.", async () => { 22 | const nextProjectUrl = new URL( 23 | "./test/fixtures/withServerContext-app-getInitialProps-absent/", 24 | import.meta.url, 25 | ); 26 | const nextProjectPath = fileURLToPath(nextProjectUrl); 27 | const buildOutput = await execFilePromise("npx", ["next", "build"], { 28 | cwd: nextProjectPath, 29 | }); 30 | 31 | ok(buildOutput.stdout.includes("Compiled successfully")); 32 | 33 | try { 34 | const { port, close } = await startNext(nextProjectPath); 35 | 36 | try { 37 | const customHeaderValue = "custom-header_value"; 38 | const response = await fetch(`http://localhost:${port}`, { 39 | headers: { 40 | "custom-header": customHeaderValue, 41 | }, 42 | }); 43 | 44 | strictEqual(response.status, 418); 45 | 46 | const html = await response.text(); 47 | 48 | ok(html.includes(customHeaderValue)); 49 | } finally { 50 | close(); 51 | } 52 | } finally { 53 | await rm(new URL(".next", nextProjectUrl), { 54 | force: true, 55 | recursive: true, 56 | }); 57 | } 58 | }); 59 | 60 | it("`getInitialProps` present.", async () => { 61 | const nextProjectUrl = new URL( 62 | "./test/fixtures/withServerContext-app-getInitialProps-present/", 63 | import.meta.url, 64 | ); 65 | const nextProjectPath = fileURLToPath(nextProjectUrl); 66 | const buildOutput = await execFilePromise("npx", ["next", "build"], { 67 | cwd: nextProjectPath, 68 | }); 69 | 70 | ok(buildOutput.stdout.includes("Compiled successfully")); 71 | 72 | try { 73 | const { port, close } = await startNext(nextProjectPath); 74 | 75 | try { 76 | const customHeaderValue = "custom-header_value"; 77 | const response = await fetch(`http://localhost:${port}`, { 78 | headers: { 79 | "custom-header": customHeaderValue, 80 | }, 81 | }); 82 | 83 | strictEqual(response.status, 418); 84 | 85 | const html = await response.text(); 86 | 87 | ok(html.includes(customHeaderValue)); 88 | ok(html.includes("appCustomProp_value")); 89 | ok(html.includes("pageCustomProp_value")); 90 | } finally { 91 | close(); 92 | } 93 | } finally { 94 | await rm(new URL(".next", nextProjectUrl), { 95 | force: true, 96 | recursive: true, 97 | }); 98 | } 99 | }); 100 | }); 101 | 102 | describe("Decorating a page.", { concurrency: true }, () => { 103 | it("`getInitialProps` absent.", async () => { 104 | const nextProjectUrl = new URL( 105 | "./test/fixtures/withServerContext-page-getInitialProps-absent/", 106 | import.meta.url, 107 | ); 108 | const nextProjectPath = fileURLToPath(nextProjectUrl); 109 | const buildOutput = await execFilePromise("npx", ["next", "build"], { 110 | cwd: nextProjectPath, 111 | }); 112 | 113 | ok(buildOutput.stdout.includes("Compiled successfully")); 114 | 115 | try { 116 | const { port, close } = await startNext(nextProjectPath); 117 | 118 | try { 119 | const customHeaderValue = "custom-header_value"; 120 | const response = await fetch(`http://localhost:${port}`, { 121 | headers: { 122 | "custom-header": customHeaderValue, 123 | }, 124 | }); 125 | 126 | strictEqual(response.status, 418); 127 | 128 | const html = await response.text(); 129 | 130 | ok(html.includes(customHeaderValue)); 131 | } finally { 132 | close(); 133 | } 134 | } finally { 135 | await rm(new URL(".next", nextProjectUrl), { 136 | force: true, 137 | recursive: true, 138 | }); 139 | } 140 | }); 141 | 142 | it("`getInitialProps` present.", async () => { 143 | const nextProjectUrl = new URL( 144 | "./test/fixtures/withServerContext-page-getInitialProps-present/", 145 | import.meta.url, 146 | ); 147 | const nextProjectPath = fileURLToPath(nextProjectUrl); 148 | const buildOutput = await execFilePromise("npx", ["next", "build"], { 149 | cwd: nextProjectPath, 150 | }); 151 | 152 | ok(buildOutput.stdout.includes("Compiled successfully")); 153 | 154 | try { 155 | const { port, close } = await startNext(nextProjectPath); 156 | 157 | try { 158 | const customHeaderValue = "custom-header_value"; 159 | const response = await fetch(`http://localhost:${port}`, { 160 | headers: { 161 | "custom-header": customHeaderValue, 162 | }, 163 | }); 164 | 165 | strictEqual(response.status, 418); 166 | 167 | const html = await response.text(); 168 | 169 | ok(html.includes(customHeaderValue)); 170 | ok(html.includes("pageCustomProp_value")); 171 | } finally { 172 | close(); 173 | } 174 | } finally { 175 | await rm(new URL(".next", nextProjectUrl), { 176 | force: true, 177 | recursive: true, 178 | }); 179 | } 180 | }); 181 | }); 182 | }); 183 | --------------------------------------------------------------------------------