├── .gitignore
├── index.d.ts
├── .github
└── workflows
│ └── publish.yml
├── LICENSE.md
├── package.json
├── index.js
├── README.md
└── rsc.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import * as React from "react";
3 | export = React;
4 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package to npmjs
2 | on:
3 | release:
4 | types: [published]
5 | workflow_dispatch:
6 | inputs:
7 | version:
8 | description: "The version to publish"
9 | required: true
10 | tag:
11 | description: "Tag"
12 | required: true
13 | default: "latest"
14 | type: choice
15 | options:
16 | - latest
17 | - snapshot
18 | - next
19 | jobs:
20 | publish:
21 | runs-on: ubuntu-latest
22 | permissions:
23 | id-token: write
24 | contents: read
25 | steps:
26 | - uses: actions/checkout@v3
27 | - uses: actions/setup-node@v3
28 | with:
29 | node-version: "18.x"
30 | registry-url: "https://registry.npmjs.org"
31 | - run: npm pkg set "version=${{ inputs.version }}"
32 | - run: npm publish --provenance --tag ${{ inputs.tag }} --access public
33 | env:
34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
35 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Lenz Weber-Tronic
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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rehackt",
3 | "version": "0.1.0",
4 | "description": "A wrapper around React that will hide hooks from the React Server Component compiler",
5 | "author": "Lenz Weber-Tronic",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/phryneas/rehackt.git"
9 | },
10 | "homepage": "https://github.com/phryneas/rehackt",
11 | "license": "MIT",
12 | "main": "index.js",
13 | "exports": {
14 | ".": {
15 | "types": "./index.d.ts",
16 | "react-server": "./rsc.js",
17 | "default": "./index.js"
18 | },
19 | "./package.json": "./package.json"
20 | },
21 | "files": [
22 | "index.js",
23 | "index.d.ts",
24 | "rsc.js",
25 | "package.json",
26 | "README.md",
27 | "LICENSE.md"
28 | ],
29 | "peerDependencies": {
30 | "@types/react": "*",
31 | "react": "*"
32 | },
33 | "peerDependenciesMeta": {
34 | "react": {
35 | "optional": true
36 | },
37 | "@types/react": {
38 | "optional": true
39 | }
40 | },
41 | "devDependencies": {
42 | "@types/node": "^20.5.7",
43 | "react": "^19.0.0-canary-33a32441e9-20240418"
44 | },
45 | "prettier": {
46 | "printWidth": 120
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | if (0) {
3 | // Trick cjs-module-lexer into adding named exports for all React exports.
4 | // (if imported with `import()`, they will appear in `.default` as well.)
5 | // This way, cjs-module-lexer will let all of react's (named) exports through unchanged.
6 | module.exports = require("react");
7 | }
8 | // We don't want bundlers to error when they encounter usage of any of these exports.
9 | // It's up to the package author to ensure that if they access React internals,
10 | // they do so in a safe way that won't break if React changes how they use these internals.
11 | // (e.g. only access them in development, and only in an optional way that won't
12 | // break if internals are not there or do not have the expected structure)
13 | // @ts-ignore
14 | module.exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = undefined;
15 | // @ts-ignore
16 | module.exports.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = undefined;
17 | // @ts-ignore
18 | module.exports.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = undefined;
19 | // Here we actually pull in the React library and add everything
20 | // it exports to our own `module.exports`.
21 | // If React suddenly were to add one of the above "polyfilled" exports,
22 | // the React version would overwrite our version, so this should be
23 | // future-proof.
24 | Object.assign(module.exports, require("react"));
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rehackt
2 |
3 | > This package is fairly advanced and is only intended for library developers that want to maintain high interop with Next.js server actions.
4 |
5 | Rehackt invisibly wraps `react` so that you're able to use shared imports with `react` in server-side Next.js code without throwing an error to your users.
6 |
7 | ## Explainer
8 |
9 | Assume you have the following code in a Next.js codebase:
10 |
11 | ```tsx
12 | "use client"
13 |
14 | import { useFormState } from "react-dom"
15 | import someAction from "./action";
16 |
17 | export const ClientComp = () => {
18 | const [data, action] = useFormState(someAction, "Hello client");
19 |
20 | return
24 | }
25 | ```
26 |
27 | ```tsx
28 | "use server"
29 | // action.ts
30 |
31 | import {data} from "./shared-code";
32 |
33 | export default async function someAction() {
34 | return "Hello " + data.name;
35 | }
36 | ```
37 |
38 | ```tsx
39 | // shared-code.ts
40 | import {useState} from "react";
41 |
42 | export const data = {
43 | useForm: (val: T) => {
44 | useState(val)
45 | },
46 | name: "server"
47 | }
48 | ```
49 |
50 | While you're not intending to use `data.useForm` in your `action.ts` server-only file, you'll still receive the following error from Next.js' build process when trying to use this code:
51 |
52 | ```shell
53 | ./src/app/shared-code.ts
54 | ReactServerComponentsError:
55 |
56 | You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
57 | Learn more: https://nextjs.org/docs/getting-started/react-essentials
58 |
59 | ╭─[/src/app/shared-code.ts:1:1]
60 | 1 │ import {useState} from "react";
61 | · ────────
62 | 2 │
63 | 3 │ export const data = {
64 | 3 │ useForm: (val: T) => {
65 | ╰────
66 |
67 | Maybe one of these should be marked as a client entry with "use client":
68 | ./src/app/shared-code.ts
69 | ./src/app/action.ts
70 | ```
71 |
72 | This is because Next.js statically analyzes usage of `useState` to ensure it's not being utilized in server-only code.
73 |
74 | By replacing the import from `react` to `rehackt`:
75 |
76 | ```tsx
77 | // shared-code.ts
78 | import {useState} from "rehackt";
79 |
80 | export const data = {
81 | useForm: (val: T) => {
82 | useState(val)
83 | },
84 | name: "server"
85 | }
86 | ```
87 |
88 | You'll no longer see this error.
89 |
90 | > Keep in mind, this does not enable usage of `useState` in server-only code, this just removes the error described above.
91 |
92 | ## Further Reading
93 |
94 | The following is a list of reading resources that pertain to this package:
95 |
96 | - [My take on the current React & Server Components controversy - Lenz Weber-Tronic](https://phryneas.de/react-server-components-controversy)
97 |
98 | - [apollographql/apollo-client#10974](https://github.com/apollographql/apollo-client/issues/10974)
99 |
100 | - [TanStack/form#480](https://github.com/TanStack/form/issues/480#issuecomment-1793576645)
101 |
--------------------------------------------------------------------------------
/rsc.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | if (0) {
4 | // Trick cjs-module-lexer into adding named exports for all React exports.
5 | // (if imported with `import()`, they will appear in `.default` as well.)
6 | // This way, cjs-module-lexer will let all of react's (named) exports through unchanged.
7 | module.exports = require("react");
8 | }
9 |
10 | // missing functions
11 | module.exports.createContext = polyfillMissingFn("createContext");
12 | // @ts-ignore
13 | module.exports.createFactory = polyfillMissingFn("createFactory");
14 | module.exports.act = polyfillMissingFn("act");
15 | // @ts-ignore
16 | module.exports.unstable_act = polyfillMissingFn("unstable_act");
17 | module.exports.unstable_useCacheRefresh = polyfillMissingFn("unstable_useCacheRefresh");
18 | module.exports.useContext = polyfillMissingFn("useContext");
19 | module.exports.useDeferredValue = polyfillMissingFn("useDeferredValue");
20 | module.exports.useEffect = polyfillMissingFn("useEffect");
21 | module.exports.useImperativeHandle = polyfillMissingFn("useImperativeHandle");
22 | module.exports.useInsertionEffect = polyfillMissingFn("useInsertionEffect");
23 | module.exports.useLayoutEffect = polyfillMissingFn("useLayoutEffect");
24 | module.exports.useReducer = polyfillMissingFn("useReducer");
25 | module.exports.useRef = polyfillMissingFn("useRef");
26 | module.exports.useState = polyfillMissingFn("useState");
27 | module.exports.useSyncExternalStore = polyfillMissingFn("useSyncExternalStore");
28 | module.exports.useTransition = polyfillMissingFn("useTransition");
29 | module.exports.useOptimistic = polyfillMissingFn("useOptimistic");
30 | // We don't want bundlers to error when they encounter usage of any of these exports.
31 | // It's up to the package author to ensure that if they access React internals,
32 | // they do so in a safe way that won't break if React changes how they use these internals.
33 | // (e.g. only access them in development, and only in an optional way that won't
34 | // break if internals are not there or do not have the expected structure)
35 | // @ts-ignore
36 | module.exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = undefined;
37 | // @ts-ignore
38 | module.exports.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = undefined;
39 | // @ts-ignore
40 | module.exports.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = undefined;
41 |
42 | // missing classes
43 | module.exports.Component = polyfillMissingClass("Component");
44 | module.exports.PureComponent = polyfillMissingClass("PureComponent");
45 |
46 | module.exports.createContext = function unsupportedCreateContext() {
47 | return {
48 | Provider: function throwNoContext() {
49 | throw new Error("Context is not available in this environment.");
50 | },
51 | Consumer: function throwNoContext() {
52 | throw new Error("Context is not available in this environment.");
53 | },
54 | };
55 | };
56 | // @ts-ignore
57 | module.exports.createFactory = function unsupportedCreateFactory() {
58 | return function throwNoCreateFactory() {
59 | throw new Error("createFactory is not available in this environment.");
60 | };
61 | };
62 |
63 | // Here we actually pull in the React library and add everything
64 | // it exports to our own `module.exports`.
65 | // If React suddenly were to add one of the above "polyfilled" exports,
66 | // the React version would overwrite our version, so this should be
67 | // future-proof.
68 | Object.assign(module.exports, require("react"));
69 |
70 | function polyfillMissingFn(exportName) {
71 | const name = "nonExistingExport__" + exportName;
72 | return /** @type {any} */ (
73 | {
74 | [name]() {
75 | throw new Error(`React functionality '${exportName}' is not available in this environment.`);
76 | },
77 | }[name]
78 | );
79 | }
80 |
81 | function polyfillMissingClass(exportName) {
82 | return /** @type {any} */ (
83 | class NonExistentClass {
84 | constructor() {
85 | throw new Error(`React class '${exportName}' is not available in this environment.`);
86 | }
87 | }
88 | );
89 | }
90 |
--------------------------------------------------------------------------------