├── .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 |
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 |
12 |
13 |
14 |
15 |
Get Started building Vite-powered Preact Apps
16 |
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 |
--------------------------------------------------------------------------------