├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── package.json ├── packages ├── demo │ ├── lazy.js │ ├── next.config.js │ ├── package.json │ └── pages │ │ ├── _app.css │ │ ├── _app.js │ │ ├── _document.js │ │ ├── error-in-effect.js │ │ ├── error.js │ │ └── index.js └── next-plugin-preact │ ├── esc.js │ ├── index.js │ ├── package.json │ ├── patches │ └── rts-invoke-diff-hook.js │ └── postinstall.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_size = 2 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | timeout-minutes: 5 8 | runs-on: ubuntu-20.04 9 | strategy: 10 | matrix: 11 | node: ['12', '13', '14'] 12 | name: Node ${{ matrix.node }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2-beta 16 | with: 17 | node-version: ${{ matrix.node }} 18 | - run: yarn 19 | - run: yarn --cwd packages/demo next build 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | dist/ 4 | packages/next-plugin-preact/README.md 5 | packages/next-plugin-preact/next-plugin-preact*.tgz 6 | packages/next-plugin-preact/package 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js plugin for preact X 2 | 3 | ## Installation 4 | 5 | ```sh 6 | npm install --save next next-plugin-preact preact react@npm:@preact/compat react-dom@npm:@preact/compat react-ssr-prepass@npm:preact-ssr-prepass preact-render-to-string 7 | ``` 8 | 9 | or 10 | 11 | ```sh 12 | yarn add next next-plugin-preact preact react@npm:@preact/compat react-dom@npm:@preact/compat react-ssr-prepass@npm:preact-ssr-prepass preact-render-to-string 13 | ``` 14 | 15 | ## Usage 16 | 17 | Create a next.config.js in your project and apply the plugin. 18 | 19 | ```js 20 | // next.config.js 21 | const withPreact = require('next-plugin-preact'); 22 | 23 | module.exports = withPreact({ 24 | /* regular next.js config options here */ 25 | }); 26 | ``` 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-plugin-preact-workspace", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/sventschui/next-plugin-preact", 6 | "author": "Sven Tschui ", 7 | "license": "MIT", 8 | "private": true, 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "devDependencies": { 13 | "eslint": "^7.6.0", 14 | "eslint-config-developit": "^1.2.0", 15 | "eslint-config-prettier": "^6.11.0", 16 | "husky": "^4.2.5", 17 | "prettier": "^2.0.5", 18 | "pretty-quick": "^3.0.2" 19 | }, 20 | "resolutions": { 21 | "node-fetch": "2.6.1" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "pretty-quick --staged packages/next-plugin-preact/*.js packages/demo/*.js packages/demo/pages/*.js && eslint packages/**/*.js" 26 | } 27 | }, 28 | "prettier": { 29 | "singleQuote": true, 30 | "trailingComma": "none", 31 | "arrowParens": "avoid" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "developit", 36 | "prettier" 37 | ], 38 | "settings": { 39 | "react": { 40 | "pragma": "React", 41 | "version": "16.13" 42 | } 43 | }, 44 | "rules": { 45 | "indent": [ 46 | "warn", 47 | 2 48 | ] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/demo/lazy.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function LazyComp() { 4 | return
Hi Lazy!
; 5 | } 6 | -------------------------------------------------------------------------------- /packages/demo/next.config.js: -------------------------------------------------------------------------------- 1 | // next.config.js 2 | const withPreact = require('next-plugin-preact'); 3 | const withCSS = require('@zeit/next-css'); 4 | 5 | module.exports = withCSS( 6 | withPreact({ 7 | /* config options here */ 8 | }) 9 | ); 10 | -------------------------------------------------------------------------------- /packages/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-plugin-preact-demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "next", 7 | "prod": "next build && next start" 8 | }, 9 | "dependencies": { 10 | "@zeit/next-css": "^1.0.1", 11 | "next": "^9.5.4", 12 | "next-plugin-preact": "^3.0.2", 13 | "preact": "^10.4.6", 14 | "preact-render-to-string": "^5.1.10", 15 | "react": "npm:@preact/compat", 16 | "react-dom": "npm:@preact/compat" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/demo/pages/_app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: lightcoral; 3 | } 4 | -------------------------------------------------------------------------------- /packages/demo/pages/_app.js: -------------------------------------------------------------------------------- 1 | import './_app.css'; 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /packages/demo/pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | 4 | export default class MyDocument extends Document { 5 | render() { 6 | return ( 7 | 8 | 9 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | ); 20 | } 21 | } 22 | 23 | // `getInitialProps` belongs to `_document` (instead of `_app`), 24 | // it's compatible with server-side generation (SSG). 25 | MyDocument.getInitialProps = async ctx => { 26 | const initialProps = await Document.getInitialProps(ctx); 27 | return { 28 | ...initialProps, 29 | // Styles fragment is rendered after the app and page rendering finish. 30 | styles: [...React.Children.toArray(initialProps.styles)] 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/demo/pages/error-in-effect.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export default function ErrorPage() { 4 | if (!process.browser) { 5 | return
SSR is ok, client will throw inside useEffect...
; 6 | } 7 | 8 | useEffect(() => { 9 | throw new Error('test'); 10 | }, []); 11 | 12 | return
; 13 | } 14 | -------------------------------------------------------------------------------- /packages/demo/pages/error.js: -------------------------------------------------------------------------------- 1 | export default function ErrorPage() { 2 | if (!process.browser) { 3 | return
SSR is ok, client will throw...
; 4 | } 5 | 6 | throw new Error('test'); 7 | } 8 | -------------------------------------------------------------------------------- /packages/demo/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | import Head from 'next/head'; 4 | 5 | const Lazy = dynamic(() => import('../lazy')); 6 | 7 | export default function IndexPage() { 8 | return ( 9 |
10 | Hello world! 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/next-plugin-preact/esc.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | process.stdout.hasColors && process.stdout.hasColors() 3 | ? n => `\x1b[${n}` 4 | : () => ''; 5 | -------------------------------------------------------------------------------- /packages/next-plugin-preact/index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const moduleAlias = require('module-alias'); 3 | const esc = require('./esc'); 4 | 5 | moduleAlias.addAliases({ 6 | react: 'preact/compat', 7 | 'react-dom': 'preact/compat', 8 | 'react-ssr-prepass': 'preact-ssr-prepass', 9 | webpack: 'webpack' 10 | }); 11 | 12 | // this has to come after the webpack alias is set up: 13 | const withPrefresh = require('@prefresh/next'); 14 | 15 | validateDependencies(); 16 | 17 | module.exports = function withPreact(nextConfig = {}) { 18 | return withPrefresh( 19 | Object.assign({}, nextConfig, { 20 | webpack(config, options) { 21 | const { dev, isServer, nextRuntime, defaultLoaders } = options; 22 | 23 | // Disable package exports field resolution in webpack. It can lead 24 | // to dual package hazards where packages are imported twice: One 25 | // commonjs version and one ESM version. This breaks hooks which have 26 | // to rely on a singleton by design (nothing we can do about that). 27 | // See #25 and https://nodejs.org/dist/latest-v14.x/docs/api/esm.html#esm_dual_package_hazard 28 | // for more information. 29 | const webpackVersion = options.webpack.version; 30 | if (isServer && +webpackVersion.split('.')[0] >= 5) { 31 | config.resolve.exportsFields = []; 32 | } 33 | 34 | if (!defaultLoaders) { 35 | throw new Error( 36 | 'This plugin is not compatible with Next.js versions below 5.0.0 https://err.sh/next-plugins/upgrade' 37 | ); 38 | } 39 | 40 | // Move Preact into the framework chunk instead of duplicating in routes: 41 | const splitChunks = 42 | config.optimization && config.optimization.splitChunks; 43 | if (splitChunks && splitChunks.cacheGroups) { 44 | const cacheGroups = splitChunks.cacheGroups; 45 | const test = /[\\/]node_modules[\\/](preact|preact-render-to-string|preact-context-provider)[\\/]/; 46 | if (cacheGroups.framework) { 47 | cacheGroups.preact = Object.assign({}, cacheGroups.framework, { 48 | test 49 | }); 50 | // if you want to merge the 2 small commons+framework chunks: 51 | // cacheGroups.commons.name = 'framework'; 52 | } 53 | } 54 | 55 | // Install webpack aliases: 56 | const aliases = config.resolve.alias || (config.resolve.alias = {}); 57 | aliases.react = aliases['react-dom'] = 'preact/compat'; 58 | aliases['react-ssr-prepass'] = 'preact-ssr-prepass'; 59 | 60 | // Automatically inject Preact DevTools 61 | if (dev && nextRuntime !== 'edge') { 62 | const prependToEntry = isServer ? 'pages/_document' : 'main.js'; 63 | 64 | const rtsVersion = require('preact-render-to-string/package.json') 65 | .version.split('.') 66 | .map(Number); 67 | // render to string <= 5.1.10 requires a monkey-patch to invoke preact.options._diff 68 | const requiresRTSDiffHookPatch = 69 | rtsVersion[0] < 5 || 70 | (rtsVersion[0] === 5 && 71 | (rtsVersion[1] < 1 || 72 | (rtsVersion[1] === 1 && rtsVersion[2] <= 10))); 73 | 74 | const itemsToPrepend = 75 | isServer && requiresRTSDiffHookPatch 76 | ? [ 77 | 'preact/debug', 78 | 'next-plugin-preact/patches/rts-invoke-diff-hook.js' 79 | ] 80 | : ['preact/debug']; 81 | 82 | if (isServer && requiresRTSDiffHookPatch) { 83 | process.stdout.write( 84 | `${esc('31;1m')}next-plugin-preact:${esc( 85 | '0m' 86 | )} The preact-render-to-string in use requires a monkey-patch for options._diff. Upgrade to preact-render-to-string > 5.1.10 once available!\n` 87 | ); 88 | } 89 | 90 | const entry = config.entry; 91 | config.entry = function () { 92 | return entry().then(function (entries) { 93 | entries[prependToEntry] = itemsToPrepend.concat( 94 | entries[prependToEntry] || [] 95 | ); 96 | return entries; 97 | }); 98 | }; 99 | } 100 | 101 | if (typeof nextConfig.webpack === 'function') { 102 | config = nextConfig.webpack(config, options); 103 | } 104 | 105 | return config; 106 | } 107 | }) 108 | ); 109 | }; 110 | 111 | function validateDependencies() { 112 | const toInstall = []; 113 | 114 | for (const dep of ['preact', 'preact-render-to-string']) { 115 | try { 116 | require.resolve(dep); 117 | } catch (e) { 118 | toInstall.push(dep); 119 | } 120 | } 121 | 122 | const NON_ALIAS_VERSION_REGEX = /^[\^~<>=\d]/; 123 | const pkg = require(join(process.cwd(), 'package.json')); 124 | const deps = pkg.dependencies; 125 | if (!deps || !deps.react || NON_ALIAS_VERSION_REGEX.test(deps.react)) { 126 | toInstall.push('react@npm:@preact/compat'); 127 | } 128 | if ( 129 | !deps || 130 | !deps['react-dom'] || 131 | NON_ALIAS_VERSION_REGEX.test(deps['react-dom']) 132 | ) { 133 | toInstall.push('react-dom@npm:@preact/compat'); 134 | } 135 | 136 | if (toInstall.length) { 137 | const lines = '-'.repeat(Math.max(process.stdout.columns - 1, 10)); 138 | console.error(`${lines}${esc('31;0m')} 139 | [preact] Missing/incorrect dependencies. 140 | Please run: 141 | npm i ${toInstall.join(' ')} 142 | 143 | or: 144 | 145 | yarn add ${toInstall.join(' ')} 146 | ${lines}${esc('0m')}`); 147 | process.exit(-1); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /packages/next-plugin-preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-plugin-preact", 3 | "version": "3.0.7", 4 | "description": "Preact plugin for Next.js", 5 | "main": "index.js", 6 | "repository": "preactjs/next-plugin-preact", 7 | "authors": [ 8 | "Sven Tschui ", 9 | "The Preact Authors (https://github.com/preactjs/preact/contributors)" 10 | ], 11 | "license": "MIT", 12 | "nextjs": { 13 | "name": "preact", 14 | "required-env": [] 15 | }, 16 | "scripts": { 17 | "postinstall": "node ./postinstall.js", 18 | "prepare": "cp ../../README.md ." 19 | }, 20 | "files": [ 21 | "esc.js", 22 | "index.js", 23 | "postinstall.js", 24 | "patches" 25 | ], 26 | "devDependencies": {}, 27 | "dependencies": { 28 | "@prefresh/next": "^1.4.6", 29 | "@prefresh/webpack": "^3.3.0", 30 | "module-alias": "^2.0.0" 31 | }, 32 | "peerDependencies": { 33 | "preact": ">=10", 34 | "preact-render-to-string": ">=5", 35 | "preact-ssr-prepass": ">=1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/next-plugin-preact/patches/rts-invoke-diff-hook.js: -------------------------------------------------------------------------------- 1 | const { options } = require('preact'); 2 | const oldRender = options.__r; 3 | 4 | options.__r = function (vnode) { 5 | options.__b(vnode); 6 | return oldRender(vnode); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/next-plugin-preact/postinstall.js: -------------------------------------------------------------------------------- 1 | const esc = require('./esc'); 2 | const lines = `\n${'-'.repeat(Math.max(process.stdout.columns - 1, 10))}\n`; 3 | 4 | process.stdout.write( 5 | `${esc('1A')}${esc('K')}${lines} 6 | ${esc('35;1m')}[PREACT] Required manual step!${esc('0m')} 7 | 8 | ${esc('36;1m')}Install the alias packages: 9 | ${esc('0m')} 10 | 11 | npm i --save react@npm:@preact/compat react-dom@npm:@preact/compat 12 | 13 | or: 14 | 15 | yarn add react@npm:@preact/compat react-dom@npm:@preact/compat 16 | ${lines} 17 | ` 18 | ); 19 | --------------------------------------------------------------------------------