├── .babelrc
├── .eslintrc
├── .github
└── workflows
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── .releaserc.json
├── LICENSE
├── README.md
├── config.d.ts
├── config.js
├── examples
└── example-app
│ ├── README.md
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── hello.ts
│ └── index.tsx
│ ├── public
│ ├── favicon.ico
│ └── vercel.svg
│ ├── styles
│ ├── Home.module.css
│ └── globals.css
│ ├── tsconfig.json
│ └── utils
│ └── example.preval.ts
├── jest.config.js
├── loader.js
├── package-lock.json
├── package.json
├── renovate.json
├── scripts
├── build
├── build-example-app
└── download-from-next-server
├── setup-jest.js
├── src
├── __example-files__
│ ├── deps.preval.ts
│ ├── function-that-throws.ts
│ ├── invalid-json.preval.ts
│ ├── no-default-export.preval.ts
│ ├── simple.preval.ts
│ ├── test-module.ts
│ ├── throws-indirect.preval.ts
│ ├── throws.preval.ts
│ ├── tsconfig-paths.preval.ts
│ └── uses-fetch.preval.ts
├── create-next-plugin-preval.ts
├── index.d.ts
├── index.js
├── is-serializable.test.ts
├── is-serializable.ts
├── loader-utils.d.ts
├── loader.test.ts
└── loader.ts
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", { "targets": "node 12 and not IE 11" }],
4 | "@babel/preset-typescript"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - alpha
6 | - main
7 | jobs:
8 | release:
9 | name: Release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v2
14 | with:
15 | fetch-depth: 0
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: 18
20 | - name: Install dependencies
21 | run: npm ci
22 | - name: Run tests
23 | run: npm t -- --coverage
24 | - name: Upload coverage to Codecov
25 | uses: codecov/codecov-action@v2
26 | - name: Release
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
30 | run: npx semantic-release
31 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | - pull_request
4 |
5 | jobs:
6 | release:
7 | name: Test
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v2
12 | with:
13 | fetch-depth: 0
14 |
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v2
17 | with:
18 | node-version: 18
19 |
20 | - name: Install dependencies
21 | run: npm ci
22 |
23 | - name: Lint
24 | run: npm run lint
25 |
26 | - name: Run tests
27 | run: npm t -- --coverage
28 |
29 | - name: Build example app
30 | run: npm run build-example-app
31 |
32 | - name: Upload coverage to Codecov
33 | uses: codecov/codecov-action@v2
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | .DS_Store
5 | .env
6 | /examples/example-app/.next
7 | /examples/example-app/package.json
8 | /examples/example-app/package-lock.json
9 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github
2 | coverage
3 | node_modules
4 | scripts
5 | src
6 | .babelrc
7 | .env
8 | .gitignore
9 | .prettierrc
10 | .releaserc.json
11 | jest.config.js
12 | package-lock.json
13 | tsconfig.json
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "branches": ["main", { "name": "alpha", "prerelease": true }],
3 | "plugins": [
4 | "@semantic-release/commit-analyzer",
5 | "@semantic-release/release-notes-generator",
6 | "@semantic-release/npm",
7 | "@semantic-release/github"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Rico Kahler
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-plugin-preval · [](https://codecov.io/gh/ricokahler/next-plugin-preval) [](https://github.com/ricokahler/next-plugin-preval/actions) [](https://github.com/semantic-release/semantic-release)
2 |
3 | > Pre-evaluate async functions (for data fetches) at build time and import them like JSON
4 |
5 | ```js
6 | // data.preval.js (or data.preval.ts)
7 |
8 | // step 1: create a data.preval.js (or data.preval.ts) file
9 | import preval from 'next-plugin-preval';
10 |
11 | // step 2: write an async function that fetches your data
12 | async function getData() {
13 | const { title, body } = await /* your data fetching function */;
14 | return { title, body };
15 | }
16 |
17 | // step 3: export default and wrap with `preval()`
18 | export default preval(getData());
19 | ```
20 |
21 | ```js
22 | // Component.js (or Component.ts)
23 |
24 | // step 4: import the preval
25 | import data from './data.preval';
26 |
27 | // step 5: use the data. (it's there synchronously from the build step!)
28 | const { title, body } = data;
29 |
30 | function Component() {
31 | return (
32 | <>
33 |
{title}
34 |
{body}
35 | >
36 | );
37 | }
38 |
39 | export default Component;
40 | ```
41 |
42 | ## Why?
43 |
44 | The primary mechanism Next.js provides for static data is `getStaticProps` — which is a great feature and is the right tool for many use cases. However, there are other use cases for static data that are not covered by `getStaticProps`.
45 |
46 | - **Site-wide data**: if you have static data that's required across many different pages, `getStaticProps` is a somewhat awkward mechanism because for each new page, you'll have to re-fetch that same static data. For example, if you use `getStaticProps` to fetch content for your header, that data will be re-fetched on every page change.
47 | - **Static data for API routes**: It can be useful to pre-evaluate data fetches in API routes to speed up response times and offload work from your database. `getStaticProps` does not work for API routes while `next-plugin-preval` does.
48 | - **De-duped and code split data**: Since `next-plugin-preval` behaves like importing JSON, you can leverage the optimizations bundlers have for importing standard static assets. This includes standard code-splitting and de-duping.
49 | - **Zero runtime**: Preval files don't get sent to the browser, only their outputted JSON.
50 |
51 | See the [recipes](#recipes) for concrete examples.
52 |
53 | ## Installation
54 |
55 | ### Install
56 |
57 | ```
58 | yarn add next-plugin-preval
59 | ```
60 |
61 | or
62 |
63 | ```
64 | npm i next-plugin-preval
65 | ```
66 |
67 | ### Add to next.config.js
68 |
69 | ```js
70 | // next.config.js
71 | const createNextPluginPreval = require('next-plugin-preval/config');
72 | const withNextPluginPreval = createNextPluginPreval();
73 |
74 | module.exports = withNextPluginPreval(/* optionally add a next.js config */);
75 | ```
76 |
77 | ## Usage
78 |
79 | Create a file with the extension `.preval.ts` or `.preval.js` then export a promise wrapped in `preval()`.
80 |
81 | ```js
82 | // my-data.preval.js
83 | import preval from 'next-plugin-preval';
84 |
85 | async function getData() {
86 | return { hello: 'world'; }
87 | }
88 |
89 | export default preval(getData());
90 | ```
91 |
92 | Then import that file anywhere. The result of the promise is returned.
93 |
94 | ```js
95 | // component.js (or any file)
96 | import myData from './my-data.preval'; // 👈 this is effectively like importing JSON
97 |
98 | function Component() {
99 | return (
100 |
101 |
{JSON.stringify(myData, null, 2)}
102 |
103 | );
104 | }
105 |
106 | export default Component;
107 | ```
108 |
109 | When you import a `.preval` file, it's like you're importing JSON. `next-plugin-preval` will run your function during the build and inline a JSON blob as a module.
110 |
111 | ## ⚠️ Important notes
112 |
113 | This works via a webpack loader that takes your code, compiles it, and runs it inside of Node.js.
114 |
115 | - Since this is an optimization at the bundler level, it will not update with Next.js [preview mode](https://nextjs.org/docs/advanced-features/preview-mode), during dynamic SSR, or even [ISR](https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration). Once this data is generated during the initial build, it can't change. It's like importing JSON. See [this pattern](#supporting-preview-mode) for a work around.
116 | - Because this plugin runs code directly in Node.js, code is not executed in the typical Next.js server context. This means certain injections Next.js does at the bundler level will not be available. We try our best to mock this context via [`require('next')`](https://github.com/ricokahler/next-plugin-preval/issues/12). For most data queries this should be sufficient, however please [open an issue](https://github.com/ricokahler/next-plugin-preval/issues/new) if something seems off.
117 |
118 | ## Recipes
119 |
120 | ### Site-wide data: Shared header
121 |
122 | ```js
123 | // header-data.preval.js
124 | import preval from 'next-plugin-preval';
125 |
126 | async function getHeaderData() {
127 | const headerData = await /* your data fetching function */;
128 |
129 | return headerData;
130 | }
131 |
132 | export default preval(getHeaderData());
133 | ```
134 |
135 | ```js
136 | // header.js
137 | import headerData from './header-data.preval';
138 | const { title } = headerData;
139 |
140 | function Header() {
141 | return {title};
142 | }
143 |
144 | export default Header;
145 | ```
146 |
147 | ### Static data for API routes: Pre-evaluated listings
148 |
149 | ```js
150 | // products.preval.js
151 | import preval from 'next-plugin-preval';
152 |
153 | async function getProducts() {
154 | const products = await /* your data fetching function */;
155 |
156 | // create a hash-map for O(1) lookups
157 | return products.reduce((productsById, product) => {
158 | productsById[product.id] = product;
159 | return productsById;
160 | }, {});
161 | }
162 |
163 | export default preval(getProducts());
164 | ```
165 |
166 | ```js
167 | // /pages/api/products/[id].js
168 | import productsById from '../products.preval.js';
169 |
170 | const handler = (req, res) => {
171 | const { id } = req.params;
172 |
173 | const product = productsById[id];
174 |
175 | if (!product) {
176 | res.status(404).end();
177 | return;
178 | }
179 |
180 | res.json(product);
181 | };
182 |
183 | export default handler;
184 | ```
185 |
186 | ### Code-split static data: Loading non-critical data
187 |
188 | ```js
189 | // states.preval.js
190 | import preval from 'next-plugin-preval';
191 |
192 | async function getAvailableStates() {
193 | const states = await /* your data fetching function */;
194 | return states;
195 | }
196 |
197 | export default preval(getAvailableStates());
198 | ```
199 |
200 | ```js
201 | // state-picker.js
202 | import { useState, useEffect } from 'react';
203 |
204 | function StatePicker({ value, onChange }) {
205 | const [states, setStates] = useState([]);
206 |
207 | useEffect(() => {
208 | // ES6 dynamic import
209 | import('./states.preval').then((response) => setStates(response.default));
210 | }, []);
211 |
212 | if (!states.length) {
213 | return
Loading…
;
214 | }
215 |
216 | return (
217 |
224 | );
225 | }
226 | ```
227 |
228 | ### Supporting preview mode
229 |
230 | As stated in the [notes](#%EF%B8%8F-important-notes), the result of next-plugin-preval won't change after it leaves the build. However, you can still make preview mode work if you extract your data fetching function and conditionally call it based on preview mode (via [`context.preview`](https://nextjs.org/docs/advanced-features/preview-mode#step-2-update-getstaticprops). If preview mode is not active, you can default to the preval file.
231 |
232 | ```js
233 | // get-data.js
234 |
235 | // 1. extract a data fetching function
236 | async function getData() {
237 | const data = await /* your data fetching function */;
238 | return data
239 | }
240 | ```
241 |
242 | ```js
243 | // data.preval.js
244 | import preval from 'next-plugin-preval';
245 | import getData from './getData';
246 |
247 | // 2. use that data fetching function in the preval
248 | export default preval(getData());
249 | ```
250 |
251 | ```js
252 | // /pages/some-page.js
253 | import data from './data.preval';
254 | import getData from './get-data';
255 |
256 | export async function getStaticProps(context) {
257 | // 3. conditionally call the data fetching function defaulting to the prevalled version
258 | const data = context.preview ? await getData() : data;
259 |
260 | return { props: { data } };
261 | }
262 | ```
263 |
264 | ## Related Projects
265 |
266 | - [`next-data-hooks`](https://github.com/ricokahler/next-data-hooks) — creates a pattern to use `getStaticProps` as React hooks. Great for the site-wide data case when preview mode or ISR is needed.
267 |
--------------------------------------------------------------------------------
/config.d.ts:
--------------------------------------------------------------------------------
1 | import createNextPluginPreval from './dist/create-next-plugin-preval';
2 |
3 | declare const _: typeof createNextPluginPreval;
4 | export = _;
5 | export as namespace _;
6 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./dist/create-next-plugin-preval').default;
2 |
--------------------------------------------------------------------------------
/examples/example-app/README.md:
--------------------------------------------------------------------------------
1 | The following is an example app used primarily for testing next-plugin-preval in CI
2 |
--------------------------------------------------------------------------------
/examples/example-app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 |
--------------------------------------------------------------------------------
/examples/example-app/next.config.js:
--------------------------------------------------------------------------------
1 | const createNextPluginPreval = require('next-plugin-preval/config');
2 | const withNextPluginPreval = createNextPluginPreval();
3 |
4 | module.exports = withNextPluginPreval({
5 | eslint: {
6 | ignoreDuringBuilds: true,
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/examples/example-app/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from 'next/app';
2 | import '../styles/globals.css';
3 |
4 | function MyApp({ Component, pageProps }: AppProps) {
5 | return ;
6 | }
7 |
8 | export default MyApp;
9 |
--------------------------------------------------------------------------------
/examples/example-app/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | import { NextApiHandler } from 'next';
2 | import exampleData from '@/utils/example.preval';
3 |
4 | const handler: NextApiHandler = (_req, res) => {
5 | res.status(200).json(exampleData);
6 | };
7 |
8 | export default handler;
9 |
--------------------------------------------------------------------------------
/examples/example-app/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import Image from 'next/image';
3 | import exampleData from '@/utils/example.preval';
4 | import styles from '../styles/Home.module.css';
5 |
6 | export default function Home() {
7 | return (
8 |