├── .gitattributes ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── context.d.ts ├── context.js ├── examples ├── with-react-query │ ├── README.md │ ├── next.config.js │ ├── package.json │ └── pages │ │ ├── _app.js │ │ ├── api │ │ └── movies.js │ │ └── index.js ├── with-swr │ ├── README.md │ ├── next.config.js │ ├── package.json │ └── pages │ │ ├── api │ │ └── movies.js │ │ └── index.js └── with-typescript │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── api │ │ └── host.ts │ └── index.tsx │ └── tsconfig.json ├── index.d.ts ├── index.js ├── jest.config.js ├── json-rpc-chrome-viewer.jpeg ├── lib ├── astUtils.ts ├── babelTransformContext.ts ├── babelTransformRpc.ts ├── browser.ts ├── context-internal.ts ├── context.ts ├── index.ts ├── jsonRpc.ts └── server.ts ├── package.json ├── test ├── __fixtures__ │ ├── basePath │ │ ├── jsconfig.json │ │ ├── next.config.js │ │ └── pages │ │ │ ├── api │ │ │ └── rpc-route.js │ │ │ └── index.js │ ├── basic-app │ │ ├── jsconfig.json │ │ ├── next.config.js │ │ └── pages │ │ │ ├── api │ │ │ ├── disallowed-syntax.js │ │ │ ├── non-rpc-route.js │ │ │ ├── rpc-route.js │ │ │ ├── rpc-syntax.js │ │ │ ├── wrapMethod1.js │ │ │ ├── wrapMethod2.js │ │ │ └── wrapMethod3.js │ │ │ ├── index.js │ │ │ ├── syntax.js │ │ │ ├── throws-non-error.js │ │ │ ├── throws.js │ │ │ ├── wrapped1.js │ │ │ ├── wrapped2.js │ │ │ └── wrapped3.js │ ├── context │ │ ├── jsconfig.json │ │ ├── middleware.js │ │ ├── next.config.js │ │ └── pages │ │ │ ├── _app.js │ │ │ ├── api │ │ │ ├── classicApi.js │ │ │ └── withContext.js │ │ │ ├── callRpc.js │ │ │ ├── cookies.js │ │ │ ├── getInitialProps1.js │ │ │ ├── getInitialProps2.js │ │ │ ├── getInitialProps3.js │ │ │ ├── getInitialProps4.js │ │ │ ├── getServerSideProps.js │ │ │ ├── getServerSideProps2.js │ │ │ ├── getServerSideProps3.js │ │ │ ├── getServerSideProps4.js │ │ │ └── index.js │ ├── typescript-context │ │ ├── next-env.d.ts │ │ ├── next.config.js │ │ ├── pages │ │ │ ├── api │ │ │ │ └── withContext.ts │ │ │ └── getServerSideProps.tsx │ │ └── tsconfig.json │ └── typescript │ │ ├── next-env.d.ts │ │ ├── next.config.js │ │ ├── pages │ │ ├── api │ │ │ ├── host.ts │ │ │ └── wrapped.ts │ │ └── index.tsx │ │ └── tsconfig.json ├── basePath.spec.ts ├── context.spec.ts ├── rpc.spec.ts ├── typescript.spec.ts ├── typescriptContext.spec.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x, 16.x, 18.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: yarn install --frozen-lockfile 27 | - run: yarn build 28 | - run: yarn test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | examples/**/yarn.lock 107 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | dist 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | singleQuote: true, 4 | }; 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.7.0 - 2023-01-21 4 | 5 | - Remove `findPagesDir()` method from public API. It shouldn't have been there in the first place. 6 | - Adhere underlying messages to the [JSON-RPC](https://www.jsonrpc.org/specification) protocol. 7 | 8 | ## v3.6.0 - 2023-01-21 9 | 10 | _Accidental republish of 3.5.3._ 11 | 12 | ## v3.5.3 - 2023-01-07 13 | 14 | - Fix context when `getServerSideProps` is a function expression. 15 | 16 | ## v3.5.1 - 2022-05-28 17 | 18 | _Beginning of this changelog._ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jan Potoms 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 | # next-rpc 2 | 3 | `next-rpc` makes exported functions from API routes accessible in the browser. Just import your API function and call it anywhere you want. 4 | 5 | ## Example 6 | 7 | Define your rpc route as follows: 8 | 9 | ```js 10 | // /pages/api/countries.js 11 | export const config = { rpc: true }; // enable rpc on this API route 12 | 13 | // export a function that needs to be called from the server and the browser 14 | export async function getName(code) { 15 | return db.query(`SELECT name FROM country WHERE code = ?`, code); 16 | } 17 | ``` 18 | 19 | Now in your components you can just import `getName` and call it anywhere you want: 20 | 21 | ```jsx 22 | // /pages/index.js 23 | import { getName } from './api/countries'; 24 | 25 | export default function MyPage({ initialData }) { 26 | const [countryName, setCountryName] = React.useState(initialData); 27 | 28 | return ( 29 | 32 | ); 33 | } 34 | ``` 35 | 36 | ## Installation 37 | 38 | Install the `next-rpc` module 39 | 40 | ``` 41 | npm install -S next-rpc 42 | ``` 43 | 44 | configure Next.js to use the module 45 | 46 | ```tsx 47 | // ./next.config.js 48 | const withRpc = require('next-rpc')(); 49 | module.exports = withRpc({ 50 | // your next.js config goes here 51 | }); 52 | ``` 53 | 54 | ## Why this library is needed 55 | 56 | Next.js 9.3 introduced [`getServerSideProps` and `getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching). New ways of calling serverside code and transfer the data to the browser. a pattern emerged for sharing API routes serverside and browserside. In short the idea is to abstract the logic into an exported function for serverside code and expose the function to the browser through an API handler. 57 | 58 | ```js 59 | // /pages/api/myApi.js 60 | export async function getName(code) { 61 | return db.query(`SELECT name FROM country WHERE code = ?`, code); 62 | } 63 | 64 | export default async (req, res) => { 65 | res.send(await getName(req.query.code)); 66 | }; 67 | ``` 68 | 69 | This pattern is great as it avoids hitting the network when used serverside. Unfortunately, to use it client side it still involves a lot of ceremony. i.e. a http request handler needs to be set up, `fetch` needs to be used in the browser, the input and output needs to be correctly encoded and decoded. Error handling needs to be set up to deal with network related errors. If you use typescript you need to find a way to propagate the types from API to fetch result. etc... 70 | 71 | Wouldn't it be nice if all of that was automatically handled and all you'd need to do is import `getName` on the browserside, just like you do serverside? That's where `next-rpc` comes in. With a `next-rpc` enabled API route, all its exported functions automatically become available to the browser as well. 72 | 73 | > **Note:** `next-rpc` is not meant as a full replacement for Next.js API routes. Some use cases are still better solved with classic API routes. For instance when you want to rely on the existing browser caching mechanisms. 74 | 75 | ## Rules and limitations 76 | 77 | 1. Rpc routes are **only allowed to export async functions**. They also need to be statically analyzable as such. Therefore only the following is allowed, either: 78 | 79 | ```js 80 | export async function fn1() {} 81 | 82 | export const fn2 = async () => {}; 83 | ``` 84 | 85 | 2. All inputs and outputs must be simple **JSON serializable values**. 86 | 3. a **default export is not allowed**. `next-rpc` will generate one. 87 | 4. **You must enable rpc routes** through the `config` export. It must be an exported object that has the `rpc: true` property. 88 | 89 | ## typescript 90 | 91 | > Try [the example](https://github.com/Janpot/next-rpc/tree/master/examples/with-typescript) on [codesandbox](https://codesandbox.io/s/github/Janpot/next-rpc/tree/master/examples/with-typescript) 92 | 93 | `next-rpc` works really nicely with typescript. There is no serialization layer so functions just retain their type sigantures both on server and client. 94 | 95 | ## swr 96 | 97 | > Try [the example](https://github.com/Janpot/next-rpc/tree/master/examples/with-swr) on [codesandbox](https://codesandbox.io/s/github/Janpot/next-rpc/tree/master/examples/with-swr) 98 | 99 | `next-rpc` can work seamlessly with [`swr`](https://swr.vercel.app/). 100 | 101 | ```ts 102 | // ./pages/api/projects.js 103 | export const config = { rpc: true }; 104 | 105 | export async function getMovies(genre) { 106 | return db.query(`...`); 107 | } 108 | 109 | // ./pages/index.jsx 110 | import useSwr from 'swr'; 111 | import { getMovies } from './api/movies'; 112 | import MoviesList from '../components/MoviesList'; 113 | 114 | const callFn = (method, ...params) => method(...params); 115 | 116 | export default function Comedies() { 117 | const { data, error } = useSwr([getMovies, 'comedy'], callFn); 118 | if (error) return
Title | 44 |Year | 45 |
---|---|
{movie.title} | 52 |{movie.year} | 53 |
Title | 44 |Year | 45 |
---|---|
{movie.title} | 52 |{movie.year} | 53 |
(Page: NextPage
): NextPage
{ 49 | if (typeof Page.getInitialProps === 'function') { 50 | Page.getInitialProps = wrapGetInitialProps(Page.getInitialProps); 51 | } 52 | return new Proxy(Page, { 53 | set(target, property, value) { 54 | if (property === 'getInitialProps' && typeof value === 'function') { 55 | return Reflect.set(target, property, wrapGetInitialProps(value)); 56 | } 57 | return Reflect.set(target, property, value); 58 | }, 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /lib/context.ts: -------------------------------------------------------------------------------- 1 | export { getContext } from './context-internal'; 2 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import * as webpack from 'webpack'; 4 | import { NextConfig } from 'next'; 5 | import { PluginOptions as RpcPluginOptions } from './babelTransformRpc'; 6 | import { PluginOptions as ContextPluginOptions } from './babelTransformContext'; 7 | import { WrapMethod } from './server'; 8 | 9 | export interface NextRpcConfig { 10 | rpc: true; 11 | wrapMethod?: WrapMethod; 12 | } 13 | 14 | export interface WithRpcConfig { 15 | experimentalContext?: boolean; 16 | } 17 | 18 | export { WrapMethod }; 19 | 20 | export default function init(withRpcConfig: WithRpcConfig = {}) { 21 | return (nextConfig: NextConfig = {}): NextConfig => { 22 | return { 23 | ...nextConfig, 24 | 25 | webpack(config: webpack.Configuration, options) { 26 | const { experimentalContext = false } = withRpcConfig; 27 | const { isServer, dev, dir } = options; 28 | const pagesDir = findPagesDir(dir); 29 | const apiDir = path.resolve(pagesDir, './api'); 30 | 31 | const rpcPluginOptions: RpcPluginOptions = { 32 | isServer, 33 | pagesDir, 34 | dev, 35 | apiDir, 36 | basePath: nextConfig.basePath || '/', 37 | }; 38 | 39 | const contextPluginOptions: ContextPluginOptions = { apiDir, isServer }; 40 | 41 | config.module = config.module || {}; 42 | config.module.rules = config.module.rules || []; 43 | config.module.rules.push({ 44 | test: /\.(tsx|ts|js|mjs|jsx)$/, 45 | include: [pagesDir], 46 | use: [ 47 | options.defaultLoaders.babel, 48 | { 49 | loader: 'babel-loader', 50 | options: { 51 | sourceMaps: dev, 52 | plugins: [ 53 | [ 54 | require.resolve('../dist/babelTransformRpc'), 55 | rpcPluginOptions, 56 | ], 57 | ...(experimentalContext 58 | ? [ 59 | [ 60 | require.resolve('../dist/babelTransformContext'), 61 | contextPluginOptions, 62 | ], 63 | ] 64 | : []), 65 | require.resolve('@babel/plugin-syntax-jsx'), 66 | [ 67 | require.resolve('@babel/plugin-syntax-typescript'), 68 | { isTSX: true }, 69 | ], 70 | ], 71 | }, 72 | }, 73 | ], 74 | }); 75 | 76 | if (typeof nextConfig.webpack === 'function') { 77 | return nextConfig.webpack(config, options); 78 | } else { 79 | return config; 80 | } 81 | }, 82 | }; 83 | }; 84 | } 85 | 86 | // taken from https://github.com/vercel/next.js/blob/v12.1.5/packages/next/lib/find-pages-dir.ts 87 | function findPagesDir(dir: string): string { 88 | // prioritize ./pages over ./src/pages 89 | let curDir = path.join(dir, 'pages'); 90 | if (fs.existsSync(curDir)) return curDir; 91 | 92 | curDir = path.join(dir, 'src/pages'); 93 | if (fs.existsSync(curDir)) return curDir; 94 | 95 | // Check one level up the tree to see if the pages directory might be there 96 | if (fs.existsSync(path.join(dir, '..', 'pages'))) { 97 | throw new Error( 98 | 'No `pages` directory found. Did you mean to run `next` in the parent (`../`) directory?' 99 | ); 100 | } 101 | 102 | throw new Error( 103 | "Couldn't find a `pages` directory. Please create one under the project root" 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /lib/jsonRpc.ts: -------------------------------------------------------------------------------- 1 | // https://www.jsonrpc.org/specification 2 | 3 | export type JsonRpcRequestId = string | number | null; 4 | 5 | export type JsonValue = 6 | | string 7 | | number 8 | | boolean 9 | | null 10 | | JsonValue[] 11 | | { [key: string]: JsonValue }; 12 | 13 | export interface JsonRpcRequest { 14 | jsonrpc: '2.0'; 15 | method: string; 16 | params: JsonValue[]; 17 | id: JsonRpcRequestId; 18 | } 19 | 20 | export interface JsonRpcError { 21 | code: number; 22 | message: string; 23 | data?: JsonValue; 24 | } 25 | 26 | export interface JsonRpcSuccessResponse { 27 | jsonrpc: '2.0'; 28 | result: JsonValue; 29 | error?: undefined; 30 | id: JsonRpcRequestId; 31 | } 32 | 33 | export interface JsonRpcErrorResponse { 34 | jsonrpc: '2.0'; 35 | result?: undefined; 36 | error: JsonRpcError; 37 | id: JsonRpcRequestId; 38 | } 39 | 40 | export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse; 41 | -------------------------------------------------------------------------------- /lib/server.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from 'next'; 2 | import { JsonRpcResponse } from './jsonRpc'; 3 | 4 | export type Method
= (...params: P) => Promise (
12 | method: Method ,
13 | meta: WrapMethodMeta
14 | ): Method ;
15 | }
16 |
17 | export function createRpcMethod (
18 | method: Method ,
19 | meta: WrapMethodMeta,
20 | customWrapRpcMethod: unknown
21 | ): Method {
22 | let wrapped = method;
23 | if (typeof customWrapRpcMethod === 'function') {
24 | wrapped = customWrapRpcMethod(method, meta);
25 | if (typeof wrapped !== 'function') {
26 | throw new Error(
27 | `wrapMethod didn't return a function, got "${typeof wrapped}"`
28 | );
29 | }
30 | } else if (
31 | customWrapRpcMethod !== undefined &&
32 | customWrapRpcMethod !== null
33 | ) {
34 | throw new Error(
35 | `Invalid wrapMethod type, expected "function", got "${typeof customWrapRpcMethod}"`
36 | );
37 | }
38 | return async (...args) => wrapped(...args);
39 | }
40 |
41 | export function createRpcHandler(
42 | methodsInit: [string, (...params: any[]) => Promise