├── .husky └── pre-commit ├── Readme.md ├── projects ├── rspack │ ├── src │ │ ├── index.jsx │ │ ├── CompatCounter.jsx │ │ ├── App.css │ │ ├── App.jsx │ │ ├── index.css │ │ └── assets │ │ │ └── preact.svg │ ├── index.html │ ├── .gitignore │ ├── package.json │ └── rspack.config.js ├── vite │ ├── vite.config.js │ ├── jsconfig.json │ ├── .gitignore │ ├── src │ │ ├── Counter.jsx │ │ ├── CompatCounter.jsx │ │ ├── index.jsx │ │ ├── assets │ │ │ └── preact.svg │ │ └── style.css │ ├── package.json │ ├── index.html │ └── public │ │ └── vite.svg ├── parcel │ ├── index.html │ ├── src │ │ ├── compat.js │ │ └── index.js │ └── package.json ├── rollup │ ├── babel.config.js │ ├── src │ │ ├── compat.js │ │ └── index.js │ ├── package.json │ └── rollup.config.js ├── esbuild │ ├── dist │ │ └── index.html │ ├── src │ │ ├── compat.jsx │ │ └── index.jsx │ ├── package.json │ └── build.js └── webpack │ ├── src │ ├── compat.js │ └── index.js │ ├── package.json │ └── webpack.config.js ├── jsconfig.json ├── .editorconfig ├── tests ├── package.json └── index.test.js ├── .github └── workflows │ └── main.yml ├── LICENSE ├── package.json └── .gitignore /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Preact bundling examples 2 | 3 | A repo to show how to bundle preact apps with different bundlers. 4 | -------------------------------------------------------------------------------- /projects/rspack/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | import App from "./App"; 3 | import "./index.css"; 4 | 5 | render(, document.getElementById("root")); 6 | -------------------------------------------------------------------------------- /projects/vite/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import preact from '@preact/preset-vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [preact()], 7 | }); 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "checkJs": true, 6 | "moduleResolution": "node", 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "preact" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/parcel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My First Parcel App 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /projects/rollup/babel.config.js: -------------------------------------------------------------------------------- 1 | export default (api) => { 2 | api.cache(true); 3 | 4 | return { 5 | presets: [ 6 | [ 7 | "@babel/preset-react", 8 | { 9 | runtime: "automatic", 10 | importSource: "preact", 11 | }, 12 | ], 13 | ], 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [{*.json,.*rc,*.yml,*.yaml}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /projects/rspack/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rspack + Preact 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /projects/vite/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "noEmit": true, 7 | "allowJs": true, 8 | "checkJs": true, 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "preact" 11 | }, 12 | "include": ["node_modules/vite/client.d.ts", "**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /projects/esbuild/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | esbuild + Preact 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-bundling-tests", 3 | "type": "module", 4 | "private": true, 5 | "version": "1.0.0", 6 | "main": "index.test.js", 7 | "description": "Tests for the preact-bundling projects", 8 | "scripts": { 9 | "test": "node --test" 10 | }, 11 | "devDependencies": { 12 | "puppeteer": "^21.3.6" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /projects/vite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /projects/rspack/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /projects/rspack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rspack", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.1", 6 | "scripts": { 7 | "start": "rspack serve", 8 | "build": "rspack build", 9 | "serve": "sirv dist" 10 | }, 11 | "dependencies": { 12 | "preact": "^10.18.1" 13 | }, 14 | "devDependencies": { 15 | "@rspack/cli": "latest" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/vite/src/Counter.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "preact/hooks"; 2 | 3 | export default function Counter() { 4 | const [count, setCount] = useState(0); 5 | 6 | return ( 7 |
8 |

9 | Vite Count: {count} 10 |

11 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /projects/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite", 3 | "type": "module", 4 | "private": true, 5 | "description": "A Preact app built with Vite, using `npm create preact`", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "vite build", 9 | "serve": "vite preview" 10 | }, 11 | "dependencies": { 12 | "preact": "^10.18.1" 13 | }, 14 | "devDependencies": { 15 | "@preact/preset-vite": "^2.5.0", 16 | "vite": "^4.3.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /projects/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + Preact 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/parcel/src/compat.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function CompatCounter() { 4 | const [count, setCount] = useState(0); 5 | 6 | return ( 7 |
8 |

9 | compat-Parcel Count:{" "} 10 | {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /projects/rollup/src/compat.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function CompatCounter() { 4 | const [count, setCount] = useState(0); 5 | 6 | return ( 7 |
8 |

9 | compat-Rollup Count:{" "} 10 | {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /projects/esbuild/src/compat.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function CompatCounter() { 4 | const [count, setCount] = useState(0); 5 | 6 | return ( 7 |
8 |

9 | compat-esbuild Count:{" "} 10 | {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /projects/vite/src/CompatCounter.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function CompatCounter() { 4 | const [count, setCount] = useState(0); 5 | 6 | return ( 7 |
8 |

9 | compat-Vite Count:{" "} 10 | {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /projects/webpack/src/compat.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function CompatCounter() { 4 | const [count, setCount] = useState(0); 5 | 6 | return ( 7 |
8 |

9 | compat-Webpack Count:{" "} 10 | {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /projects/rspack/src/CompatCounter.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function CompatCounter() { 4 | const [count, setCount] = useState(0); 5 | 6 | return ( 7 |
8 |

9 | compat-Rspack Count:{" "} 10 | {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "**" 7 | workflow_call: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | cache: "npm" 19 | node-version-file: package.json 20 | - run: npm ci 21 | - run: npm run lint 22 | - run: npm run build 23 | - run: npm run test 24 | -------------------------------------------------------------------------------- /projects/esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esbuild", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.1", 6 | "description": "A Preact app built with ESBuild", 7 | "main": "src/index.jsx", 8 | "scripts": { 9 | "start": "concurrently \"sirv dist --dev\" \"node build.js --watch\"", 10 | "build": "node build.js", 11 | "serve": "sirv dist" 12 | }, 13 | "devDependencies": { 14 | "concurrently": "^8.2.1", 15 | "esbuild": "^0.19.4", 16 | "rollup": "^3.29.4", 17 | "sirv-cli": "^2.0.2" 18 | }, 19 | "dependencies": { 20 | "preact": "^10.18.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /projects/esbuild/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import CompatCounter from "./compat.jsx"; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 |
9 |

10 | esbuild Count: {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | 19 | function App() { 20 | return ( 21 | <> 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | render(, document.getElementById("root")); 29 | -------------------------------------------------------------------------------- /projects/parcel/src/index.js: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import CompatCounter from "./compat.js"; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 |
9 |

10 | Parcel Count: {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | 19 | function App() { 20 | return ( 21 | <> 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | render(, document.getElementById("root")); 29 | -------------------------------------------------------------------------------- /projects/rollup/src/index.js: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import CompatCounter from "./compat.js"; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 |
9 |

10 | Rollup Count: {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | 19 | function App() { 20 | return ( 21 | <> 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | render(, document.getElementById("root")); 29 | -------------------------------------------------------------------------------- /projects/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.1", 6 | "description": "Preact app built with Webpack", 7 | "scripts": { 8 | "start": "webpack serve --mode development", 9 | "build": "webpack --mode production", 10 | "serve": "sirv dist" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.23.0", 14 | "@babel/preset-react": "^7.22.15", 15 | "babel-loader": "^9.1.3", 16 | "html-webpack-plugin": "^5.5.3", 17 | "webpack": "^5.88.2", 18 | "webpack-cli": "^5.1.4", 19 | "webpack-dev-server": "^4.15.1" 20 | }, 21 | "dependencies": { 22 | "preact": "^10.18.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/parcel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.1", 6 | "description": "Preact app built with Parcel", 7 | "browserslist": "last 2 Chrome versions, last 2 Safari versions, last 2 Firefox versions", 8 | "@parcel/resolver-default": { 9 | "packageExports": true 10 | }, 11 | "alias": { 12 | "react": "preact/compat", 13 | "react-dom": "preact/compat" 14 | }, 15 | "scripts": { 16 | "start": "parcel index.html", 17 | "build": "parcel build index.html", 18 | "serve": "sirv dist" 19 | }, 20 | "devDependencies": { 21 | "parcel": "^2.9.3" 22 | }, 23 | "dependencies": { 24 | "preact": "^10.18.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/rspack/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /projects/webpack/src/index.js: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import CompatCounter from "./compat.js"; 4 | 5 | function Counter() { 6 | const [count, setCount] = useState(0); 7 | return ( 8 |
9 |

10 | Webpack Count: {count} 11 |

12 | 15 |
16 | ); 17 | } 18 | 19 | function App() { 20 | return ( 21 | <> 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | const rootElement = document.createElement("div"); 29 | document.body.appendChild(rootElement); 30 | render(, rootElement); 31 | -------------------------------------------------------------------------------- /projects/rspack/rspack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { fileURLToPath } from "url"; 3 | 4 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 5 | 6 | /** 7 | * @type {import('@rspack/cli').Configuration} 8 | */ 9 | export default { 10 | context: __dirname, 11 | entry: { 12 | main: "./src/index.jsx", 13 | }, 14 | builtins: { 15 | react: { 16 | runtime: "automatic", 17 | importSource: "preact", 18 | refresh: false, 19 | }, 20 | html: [ 21 | { 22 | template: "./index.html", 23 | }, 24 | ], 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.svg$/, 30 | type: "asset", 31 | }, 32 | ], 33 | }, 34 | resolve: { 35 | alias: { 36 | react: "preact/compat", 37 | "react-dom": "preact/compat", 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /projects/rollup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rollup", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.1", 6 | "description": "A Preact app built with Rollup", 7 | "main": "index.js", 8 | "scripts": { 9 | "start": "concurrently \"sirv dist --dev\" \"rollup -c -w\"", 10 | "build": "rollup -c", 11 | "serve": "sirv dist" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.23.0", 15 | "@babel/preset-react": "^7.22.15", 16 | "@rollup/plugin-alias": "^5.0.0", 17 | "@rollup/plugin-babel": "^6.0.3", 18 | "@rollup/plugin-html": "^1.0.2", 19 | "@rollup/plugin-node-resolve": "^15.2.1", 20 | "@rollup/plugin-terser": "^0.4.3", 21 | "concurrently": "^8.2.1", 22 | "rollup": "^3.29.4", 23 | "sirv-cli": "^2.0.2" 24 | }, 25 | "dependencies": { 26 | "preact": "^10.18.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/rspack/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "preact/hooks"; 2 | import reactLogo from "./assets/preact.svg"; 3 | import "./App.css"; 4 | import CompatCounter from "./CompatCounter.jsx"; 5 | 6 | function App() { 7 | const [count, setCount] = useState(0); 8 | 9 | return ( 10 |
11 |
12 | 13 | React logo 14 | 15 |
16 |

Rspack + React

17 |
18 | 22 | 23 |

24 | Edit src/App.jsx and save to test HMR 25 |

26 |
27 |

28 | Click on the Rspack and React logos to learn more 29 |

30 |
31 | ); 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /projects/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from "html-webpack-plugin"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | const p = (...args) => path.join(__dirname, ...args); 7 | 8 | export default { 9 | entry: "./src/index.js", 10 | output: { 11 | filename: "main.js", 12 | path: p("dist"), 13 | clean: true, 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(?:js|mjs|cjs)$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: "babel-loader", 22 | options: { 23 | presets: [ 24 | [ 25 | "@babel/preset-react", 26 | { runtime: "automatic", importSource: "preact" }, 27 | ], 28 | ], 29 | }, 30 | }, 31 | }, 32 | ], 33 | }, 34 | resolve: { 35 | alias: { 36 | react: "preact/compat", 37 | "react-dom": "preact/compat", 38 | }, 39 | }, 40 | plugins: [ 41 | new HtmlWebpackPlugin({ 42 | title: "Preact built with Webpack", 43 | }), 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /projects/esbuild/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | 3 | // Replace with import.meta.resolve when it's available 4 | import { createRequire } from "module"; 5 | const require = createRequire(import.meta.url); 6 | 7 | // Sample cmd line: esbuild --bundle src/index.jsx --outdir=dist --sourcemap 8 | /** @type {import('esbuild').BuildOptions} */ 9 | const buildOptions = { 10 | entryPoints: ["src/index.jsx"], 11 | bundle: true, 12 | outdir: "dist", 13 | sourcemap: true, 14 | jsxImportSource: "preact", 15 | plugins: [ 16 | { 17 | name: "preact-compat", 18 | setup(build) { 19 | build.onResolve({ filter: /^react(-dom)?$/ }, (args) => ({ 20 | path: require.resolve("preact/compat"), 21 | })); 22 | 23 | build.onResolve({ filter: /^react(?:-dom)?\/(.*)$/ }, (args) => { 24 | return { 25 | path: require.resolve(`preact/compat/${args.path[1]}`), 26 | }; 27 | }); 28 | }, 29 | }, 30 | ], 31 | }; 32 | 33 | if (process.argv.includes("--watch")) { 34 | const ctx = await esbuild.context(buildOptions); 35 | await ctx.watch(); 36 | } else { 37 | await esbuild.build(buildOptions); 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-present Preact Team 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 | -------------------------------------------------------------------------------- /projects/vite/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/rspack/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /projects/vite/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | import preactLogo from "./assets/preact.svg"; 3 | import CompatCounter from "./CompatCounter.jsx"; 4 | import Counter from "./Counter.jsx"; 5 | import "./style.css"; 6 | 7 | export function App() { 8 | return ( 9 |
10 | 11 | Preact logo 12 | 13 | 14 | 15 |

Get Started building Vite-powered Preact Apps

16 |
17 | 22 | 27 | 32 |
33 |
34 | ); 35 | } 36 | 37 | function Resource(props) { 38 | return ( 39 | 40 |

{props.title}

41 |

{props.description}

42 |
43 | ); 44 | } 45 | 46 | render(, document.getElementById("app")); 47 | -------------------------------------------------------------------------------- /projects/rspack/src/assets/preact.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/vite/src/assets/preact.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/vite/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color: #222; 7 | background-color: #ffffff; 8 | 9 | font-synthesis: none; 10 | text-rendering: optimizeLegibility; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | -webkit-text-size-adjust: 100%; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | display: flex; 19 | align-items: center; 20 | min-height: 100vh; 21 | } 22 | 23 | #app { 24 | max-width: 1280px; 25 | margin: 0 auto; 26 | text-align: center; 27 | } 28 | 29 | img { 30 | margin-bottom: 1.5rem; 31 | } 32 | 33 | img:hover { 34 | filter: drop-shadow(0 0 2em #673ab8aa); 35 | } 36 | 37 | section { 38 | margin-top: 5rem; 39 | display: grid; 40 | grid-template-columns: repeat(3, 1fr); 41 | column-gap: 1.5rem; 42 | } 43 | 44 | .resource { 45 | padding: 0.75rem 1.5rem; 46 | border-radius: 0.5rem; 47 | text-align: left; 48 | text-decoration: none; 49 | color: #222; 50 | background-color: #f1f1f1; 51 | border: 1px solid transparent; 52 | } 53 | 54 | .resource:hover { 55 | border: 1px solid #000; 56 | box-shadow: 0 25px 50px -12px #673ab888; 57 | } 58 | 59 | @media (max-width: 639px) { 60 | #app { 61 | margin: 2rem; 62 | } 63 | section { 64 | margin-top: 5rem; 65 | grid-template-columns: 1fr; 66 | row-gap: 1rem; 67 | } 68 | } 69 | 70 | @media (prefers-color-scheme: dark) { 71 | :root { 72 | color: #ccc; 73 | background-color: #1a1a1a; 74 | } 75 | .resource { 76 | color: #ccc; 77 | background-color: #161616; 78 | } 79 | .resource:hover { 80 | border: 1px solid #bbb; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-bundling", 3 | "version": "0.0.1", 4 | "description": "A repo containing samples of bundling Preact in using various bundlers", 5 | "scripts": { 6 | "build": "npm run build -ws --if-present", 7 | "test": "node --test tests", 8 | "test:debug": "cross-env DEBUG=1 node --test tests", 9 | "lint": "prettier --check --no-error-on-unmatched-pattern **/*.{js,jsx,ts,tsx,css,md,html,yml,yaml,json}", 10 | "lint:fix": "prettier --write --no-error-on-unmatched-pattern **/*.{js,jsx,ts,tsx,css,md,html,yml,yaml,json}", 11 | "lint-staged": "lint-staged", 12 | "prepare": "husky install" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/andrewiggins/preact-bundling.git" 17 | }, 18 | "keywords": [ 19 | "preact", 20 | "rollup", 21 | "webpack", 22 | "parcel", 23 | "vite", 24 | "rspack" 25 | ], 26 | "authors": [ 27 | "The Preact Authors (https://github.com/preactjs/signals/contributors)" 28 | ], 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/andrewiggins/preact-bundling/issues" 32 | }, 33 | "homepage": "https://github.com/andrewiggins/preact-bundling#readme", 34 | "workspaces": [ 35 | "projects/*", 36 | "tests" 37 | ], 38 | "devDependencies": { 39 | "cross-env": "^7.0.3", 40 | "husky": "^8.0.3", 41 | "lint-staged": "^14.0.1", 42 | "prettier": "^3.0.3", 43 | "strip-ansi": "^7.1.0" 44 | }, 45 | "lint-staged": { 46 | "**/*.{js,jsx,ts,tsx,css,md,html,yml,yaml,json}": [ 47 | "prettier --write" 48 | ] 49 | }, 50 | "volta": { 51 | "node": "18.18.0" 52 | }, 53 | "@parcel/resolver-default": { 54 | "packageExports": true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /projects/rollup/rollup.config.js: -------------------------------------------------------------------------------- 1 | import alias from "@rollup/plugin-alias"; 2 | import babel from "@rollup/plugin-babel"; 3 | import html, { makeHtmlAttributes } from "@rollup/plugin-html"; 4 | import nodeResolve from "@rollup/plugin-node-resolve"; 5 | import terser from "@rollup/plugin-terser"; 6 | 7 | function htmlTemplate({ attributes, files, meta, publicPath, title }) { 8 | const scripts = (files.js || []) 9 | .map(({ fileName }) => { 10 | const attrs = makeHtmlAttributes(attributes.script); 11 | return ``; 12 | }) 13 | .join("\n"); 14 | 15 | const links = (files.css || []) 16 | .map(({ fileName }) => { 17 | const attrs = makeHtmlAttributes(attributes.link); 18 | return ``; 19 | }) 20 | .join("\n"); 21 | 22 | const metas = meta 23 | .map((input) => { 24 | const attrs = makeHtmlAttributes(input); 25 | return ``; 26 | }) 27 | .join("\n"); 28 | 29 | return ` 30 | 31 | 32 | ${metas} 33 | ${title} 34 | ${links} 35 | 36 | 37 |
38 | ${scripts} 39 | 40 | `; 41 | } 42 | 43 | export default { 44 | input: "src/index.js", 45 | output: { 46 | file: "dist/bundle.js", 47 | format: "iife", 48 | plugins: [terser()], 49 | }, 50 | plugins: [ 51 | alias({ 52 | entries: [ 53 | { 54 | find: "react", 55 | replacement: "preact/compat", 56 | }, 57 | { 58 | find: "react-dom", 59 | replacement: "preact/compat", 60 | }, 61 | ], 62 | }), 63 | nodeResolve(), 64 | babel({ babelHelpers: "bundled" }), 65 | html({ 66 | template: htmlTemplate, 67 | }), 68 | ], 69 | }; 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | import { spawn } from "node:child_process"; 3 | import path from "node:path"; 4 | import { describe, it, before, beforeEach, afterEach, after } from "node:test"; 5 | import { fileURLToPath, pathToFileURL } from "node:url"; 6 | import puppeteer from "puppeteer"; 7 | import stripAnsi from "strip-ansi"; 8 | 9 | const DEBUG = process.env.DEBUG === "true" || process.env.DEBUG === "1"; 10 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 11 | const p = (...args) => path.join(__dirname, "..", ...args); 12 | const urlRegex = /http:\/\/localhost:\d+\/?/; 13 | 14 | describe("bundling projects", () => { 15 | /** @type {import('puppeteer').Browser} */ 16 | let browser; 17 | 18 | /** @type {import('puppeteer').Page} */ 19 | let page; 20 | 21 | /** @type {import('node:child_process').ChildProcessWithoutNullStreams} */ 22 | let server; 23 | 24 | /** @type {Promise} */ 25 | let serverExit; 26 | 27 | /** 28 | * @param {import('child_process').ChildProcess} childProcess 29 | * @returns {Promise} 30 | */ 31 | async function waitForExit(childProcess) { 32 | return new Promise((resolve, reject) => { 33 | childProcess.once("exit", (code, signal) => { 34 | if (code === 0 || signal == "SIGINT") { 35 | resolve(); 36 | } else { 37 | reject(new Error("Exit with error code: " + code)); 38 | } 39 | }); 40 | 41 | childProcess.once("error", (err) => { 42 | reject(err); 43 | }); 44 | }); 45 | } 46 | 47 | /** 48 | * @param {string} projectPath The directory of the project to run 49 | * @returns {Promise} 50 | */ 51 | async function startServer(projectPath, timeoutMs = 10e3) { 52 | return new Promise((resolve, reject) => { 53 | // Can't do `npm run serve` because it won't exit when we send it the 54 | // SIGINT signal 55 | server = spawn( 56 | process.execPath, 57 | [p("./node_modules/sirv-cli/bin.js"), p(projectPath, "dist"), "--dev"], 58 | { cwd: p(projectPath) }, 59 | ); 60 | 61 | serverExit = waitForExit(server); 62 | 63 | let timeout; 64 | if (timeoutMs > 0) { 65 | timeout = setTimeout(() => { 66 | reject(new Error("Timed out waiting for server to get set up")); 67 | }, timeoutMs); 68 | } 69 | 70 | function onData(data) { 71 | data = data.toString("utf8"); 72 | 73 | if (DEBUG) { 74 | process.stdout.write(stripAnsi(data)); 75 | } 76 | 77 | let match = data.match(urlRegex); 78 | if (match) { 79 | cleanup(); 80 | resolve(new URL(match[0])); 81 | } 82 | } 83 | 84 | function onExit(code) { 85 | cleanup(); 86 | reject( 87 | new Error("Server unexpectedly exited with error code: " + code), 88 | ); 89 | } 90 | 91 | function cleanup() { 92 | server.stdout.off("data", onData); 93 | server.off("exit", onExit); 94 | clearTimeout(timeout); 95 | } 96 | 97 | server.stdout.on("data", onData); 98 | server.once("exit", onExit); 99 | }); 100 | } 101 | 102 | /** @type {(framework: string, url: string) => Promise} */ 103 | async function runTest(framework, url = "http://localhost:8080") { 104 | async function _runTestImpl(compat = false) { 105 | const prefix = compat ? "compat-" : ""; 106 | const bundlerId = `#${prefix}bundler`; 107 | const countId = `#${prefix}count`; 108 | const incrementId = `#${prefix}increment`; 109 | const expectedFramework = `${prefix}${framework}`; 110 | 111 | await page.waitForSelector(bundlerId); 112 | let actualFramework = await page.$eval(bundlerId, (el) => el.textContent); 113 | assert.equal(actualFramework, expectedFramework); 114 | 115 | let count = await page.$eval(countId, (el) => el.textContent); 116 | assert.equal(count, "0"); 117 | 118 | await page.click(incrementId); 119 | 120 | count = await page.$eval(countId, (el) => el.textContent); 121 | assert.equal(count, "1"); 122 | } 123 | 124 | await page.goto(url); 125 | await _runTestImpl(); 126 | await _runTestImpl(true); 127 | } 128 | 129 | before(async () => { 130 | if (!DEBUG) { 131 | browser = await puppeteer.launch({ headless: "new" }); 132 | } else { 133 | browser = await puppeteer.launch({ 134 | headless: false, 135 | devtools: true, 136 | }); 137 | } 138 | }); 139 | 140 | beforeEach(async () => { 141 | page = await browser.newPage(); 142 | page.setDefaultTimeout(10e3); 143 | }); 144 | 145 | afterEach(async () => { 146 | await page?.close(); 147 | 148 | if (server) { 149 | // Log a message if server takes a while to close 150 | let logMsg = () => console.log("Waiting for server to exit..."); 151 | let t = setTimeout(logMsg, 5e3); 152 | 153 | try { 154 | server?.kill("SIGINT"); 155 | await serverExit; 156 | } catch (error) { 157 | console.error("Error waiting for server to exit:", error); 158 | } finally { 159 | clearTimeout(t); 160 | } 161 | 162 | if (DEBUG) { 163 | console.log("Server:", { 164 | pid: server?.pid, 165 | spawnfile: server?.spawnfile, 166 | spawnargs: server?.spawnargs, 167 | connected: server?.connected, 168 | killed: server?.killed, 169 | exitCode: server?.exitCode, 170 | signalCode: server?.signalCode, 171 | }); 172 | } 173 | } 174 | 175 | server = null; 176 | serverExit = null; 177 | page = null; 178 | }); 179 | 180 | after(async () => { 181 | browser?.close(); 182 | browser = null; 183 | }); 184 | 185 | it("vite works", async () => { 186 | const url = await startServer("projects/vite"); 187 | await runTest("Vite", url.href); 188 | }); 189 | 190 | it("rollup works", async () => { 191 | const url = await startServer("projects/rollup"); 192 | await runTest("Rollup", url.href); 193 | }); 194 | 195 | it("parcel works", async () => { 196 | const url = await startServer("projects/parcel"); 197 | await runTest("Parcel", url.href); 198 | }); 199 | 200 | it("webpack works", async () => { 201 | const url = await startServer("projects/webpack"); 202 | await runTest("Webpack", url.href); 203 | }); 204 | 205 | it("rspack works", async () => { 206 | const url = await startServer("projects/rspack"); 207 | await runTest("Rspack", url.href); 208 | }); 209 | 210 | it("esbuild works", async () => { 211 | const url = await startServer("projects/esbuild"); 212 | await runTest("esbuild", url.href); 213 | }); 214 | }); 215 | --------------------------------------------------------------------------------